Patterns: Working with Arrays

Steven Zeil

Last modified: Apr 2, 2017
Contents:

1 Static & Dynamic Allocation

1.1 Static Allocation

const int MaxItems = 100;

// Names of items
std::string itemNames[MaxItems];


How Many Elements Do We Need?

If we don’t know how many elements we really need, we need to guess

1.2 Dynamic Allocation


Example: bidders.h

extern int nBidders;
extern Bidder* bidders;

void readBidders (std::istream& in);

Example: bidders.cpp

int nBidders;
Bidder* bidders;


/**
 * Read all bidders from the indicated file
 */
void readBidders (istream& in)
{
    in >> nBidders;                          ➀
    bidders = new Bidder[nBidders];          ➁
    for (int i = 0; i < nBidders; ++i)
    {
    	read (in, bidders[i]);               ➂
    }
}


void cleanUpBidders()
{
  delete [] bidders;
}

:Here we read the desired size of the arrays from the input

:Here we use that value to actually allocate arrays of the desired size

:Once that is done, we can access the arrays in the usual manner using the [].

1.3 Dynamic Array Declarations are Confusing

extern Bidder* bidders;

1.4 Why Are Arrays Different?

Because arrays are “really” pointers …

2 Partially Filled Arrays

In our prior examples, we have typically dealt with arrays that were just large enough to hold the data we wanted, or that were filled immediately to a certain fixed size and then, so long as we continued working with the array, that size never varied.

Now we turn to another common pattern: sometimes we add and remove elements to arrays as we progress with our algorithm. We might not even know ahead of time how many elements we will wind up with until we reach the end of the program.

Consider, for example, a spell checker that needs to accumulate a list of misspelled words from a document. That list would start out at zero. As we discover words in the document that are not in our dictionary, we would add them, one by one, to our list of misspellings. We won’t be able to predict just how long the list will get until we’ve finished the spellcheck.

To work with a partially filled array, we generally need to have access to

With that in mind, let’s look at some typical operations on partially filled arrays.

2.1 Adding Elements

Variations include

2.1.1 Add to the end**

void addToEnd (std::string* array, int& size, std::string value)
{
   array[size] = value;
   ++size;
}

2.1.2 Add to the middle

 
If we have this and we do

addElement (array, 3, 1, "Smith");

 
we should get this.


Add to the middle: implementation

addElement.cpp
// Add value into array[index], shifting all elements already in positions
//    index..size-1 up one, to make room.
//  - Assumes that we have a separate integer (size) indicating how
//     many elements are in the array
//  - and that the "true" size of the array is at least one larger 
//      than the current value of that counter

void addElement (std::string* array, int& size, int index, std::string value)
{
  // Make room for the insertion
  int toBeMoved = size - 1;
  while (toBeMoved >= index) {
    array[toBeMoved+1] = array[toBeMoved];
    --toBeMoved;
  }
  // Insert the new value
  array[index] = value;
  ++size;
}

2.1.3 Add in order

 

If we have this and we do

addInOrder (array, 3, "Clarke");

 
we should get this


Add in order implementation

This works:

int addInOrder (std::string* array, int& size,
    std::string value)
{
  // Find where to insert
  int pos = 0;
  while (pos < size && value > array[pos])
    ++pos;
  addElement (array, size, pos, value);
  return pos;
}


Add in order (streamlined)

This is a bit faster:

int addInOrder ((std::string* array, int& size,
    std::string value)
{
  // Make room for the insertion
  int toBeMoved = size - 1;
  while (toBeMoved >= 0 && value < array[toBeMoved]) {
    array[toBeMoved+1] = array[toBeMoved];
    --toBeMoved;
  }
  // Insert the new value
  array[toBeMoved+1] = value;
  ++size;
  return toBeMoved+1;
}

Try This:
You can try out the two addInOrder functions
here.

2.2 Searching for Elements


Sequential Search

Search an array for a given value, returning the index where found or -1 if not found.

seqSearch.cpp
int seqSearch(const int list[], int listLength, int searchItem)
{
   int loc;
   bool found = false;

   for (loc = 0; loc < listLength; loc++)
      if (list[loc] == searchItem)
        {
         found = true;
         break;
        }

   if (found)
      return loc;
   else
      return -1;
}

Sequential Search 2

Search an array for a given value, returning the index where found or -1 if not found.

int seqSearch(const int list[], int listLength, int searchItem)
{
    int loc;

    for (loc = 0; loc < listLength; loc++)
        if (list[loc] == searchItem)
            return loc;

    return -1;
}

Sequential Search (Ordered Data)

Search an array for a given value, returning the index where found or -1 if not found.


seqOrderedSearch

int seqOrderedSearch(const int list[], int listLength, int searchItem)
{
    int loc = 0;

    while (loc < listLength && list[loc] < searchItem)
      {
       ++loc;
      }
    if (loc < listLength && list[loc] == searchItem)
       return loc;
    else
       return -1;
}

Try This:
You can try out the sequential search and sequential ordered search functions here.

2.3 Removing Elements

Try This:
You can try out the removeElement function
here.

3 Arrays and Templates


arrayUtils - first draft

The functions we have developed in this section can be used in many different programs, so it might be useful to collect them into an “Array Utilities” module.

Question: The main function will not compile. Why?

**Answer:**

Overloading

One solution is to provide different versions of each function for each kind of array:

// Search an ordered array for a given value, returning the index where 
//    found or -1 if not found.
int seqOrderedSearch(const int list[], int listLength, int searchItem);
int seqOrderedSearch(const char list[], int listLength, char searchItem);
int seqOrderedSearch(const double list[], int listLength, double searchItem);
int seqOrderedSearch(const float list[], int listLength, float searchItem);
int seqOrderedSearch(const std::string list[], int listLength, std::string searchItem);

with nearly identical function bodies for each one.


Function Templates

A function template is a pattern for an infinite number of possible functions.


A Simple Template

swap.cpp
template <typename T>
void swap (T & x, T & y)
{
  T temp = x;
  x = y;
  y = temp;
}


Using A Function Template

#include <algorithm>
using namespace std;
   ⋮
string a = "abc";
string b = "bcde";
swap (a, b); // compiler replaces "T" by "string"
int i = 0;
int j = 2;
swap (i, j); // compiler replaces "T" by "int"


Some Other std Function Templates


Building a Library of Array Templates

Second try, using templates:


Function Templates Are Not Functions

Is it a problem that we have the bodies in a .h file?


Patterns versus Instances

In the same way that

4 Vectors


Keeping Information Together

One criticism of functions like

void addToEnd (std::string* array, int& size, 
               std::string value);
int addInOrder (std::string* array, int& size,
    std::string value);

is that they separate the array, the size, and the capacity


Wrapping arrays within structs

One solution: use a struct to gather the related elements together:

/// A collection of items
struct ItemSequence {
   static const int capacity = 500;
   int size;
   Item data[capacity];
};


A Better Version

Using dynamic allocation, we can be more flexible about the capacity:

struct ItemSequence {
   int capacity;
   int size;
   Item* data;

   ItemSequence (int cap);
   void addToEnd (Item item);
};


Implementing the Sequence

Implementing the member functions.


ItemSequence::ItemSequence (int cap) : capacity(cap), size(0) { data = new Item[capacity]; } void ItemSequence::addToEnd (Item item) { if (size < capacity) { data[size] = item; ++size; } else cerr << "Error: ItemSequence is full" << endl; }

This is, however, such a common pattern, that the designers of C++ had pity on us and did even better.


Vectors – the Un-Array

The vector is an array-like structure provided in the std header <vector>.


Declaring Vectors

std::vector<int> vi; // a vector of 0 ints
std::vector<std::string> vs (10); // a vector of 10
                      //   empty strings
std::vector<float> vf (5, 1.0); // a vector of 5 
                    //    floats, all 1.0

Accessing Elements in a Vector

Use the [] just as with an array:

	vector<int> v(10);
	for (int i = 0; i < 10; ++i)
	  {
	   int j;
	   cin >> j;
	   v[i] = j + 1;
	   cout << v[i] << endl;
	  }

Size of a Vector

void foo (vector<int>& v) {
  for (int i = 0; i < v.size(); ++i)
    {
     int j;
     cin >> j;
     v[i] = j + 1;
     cout << v[i] << endl;
    }
}

Adding to a Vector

Adding to the end:


Adding to the Middle

template <class Vector, class T>
void addElement (Vector& v, int index, T value)
{
  // Make room for the insertion
  int toBeMoved = v.size() - 1;
  v.push_back(value); // expand vector by 1 slot
  while (toBeMoved >= index) {
    v[toBeMoved+1] = v[toBeMoved];
    --toBeMoved;
  }
  // Insert the new value
  v[index] = value;
}


Adding to the Middle: v2

Actually, vectors provide a built-in operation for inserting into the middle:

string* a; // array of strings
vector<string> v;
int k, n;
   ⋮
v.insert (v.begin()+k, "Hello");
addElement (a, n, k, "Hello");

The last two statements do the same thing.


Add in Order to Vector

template <class Vector, class T>
int addInOrder (Vector& v, T value)
{
  // Make room for the insertion
  int toBeMoved = v.size() - 1;
  v.push_back(value); // expand vector by 1 slot
  while (toBeMoved >= 0 && value < v[toBeMoved]) {
    v[toBeMoved+1] = v[toBeMoved];
    --toBeMoved;
  }
  // Insert the new value
  v[toBeMoved+1] = value;
  return toBeMoved+1;
}

4.1 Vectors versus Arrays


Advantages of Vectors


Disadvantages of Vectors

5 Multi-Dimension Arrays


Multi-Dimension Arrays

 


Example

test2dimfixed.cpp
#include <iostream>

using namespace std;

const int NumRows = 4;
const int NumCols = 3;

int main (int argc, char** argv)
{
  double table[NumRows][NumCols];
  for (int row = 0; row < NumRows; ++row)
    for (int col = 0; col < NumCols; ++col)
      table[row][col] = 1.0 / (1.0 + row + col);

  for (int row = 0; row < NumRows; ++row)
    for (int col = 0; col < NumCols; ++col)
      cout << row << " " << col << " " 
	   << table[row][col] << endl;


  return 0;
} 

5.1 Arrays of Arrays

 

test2dimRowsCols.cpp
#include <iostream>

using namespace std;

const int NumCols = 3;

int main (int argc, char** argv)
{
  int NumCols;
  int NumRows;
  cout << "How many rows? " << flush;
  cin >> NumRows;
  cout << "How many columns? " << flush;
  cin >> NumCols;

  double** table = new double*[NumRows];
  for (int row = 0; row < NumRows; ++row)
    {
      table[row] = new double[NumCols];
      for (int col = 0; col < NumCols; ++col)
	table[row][col] = 1.0 + row + col;
    }
  for (int row = 0; row < NumRows; ++row)
    for (int col = 0; col < NumCols; ++col)
      cout << row << " " << col << " " 
	   << table[row][col] << endl;


  return 0;
} 

Linearized Arrays

We can map 2-D arrays onto a linear structure

int index (int i, j, numCols)
{
  return j + i * numColss;
}

 


Linearized Array Code

 

int* array;
array = new int[numRows*numCols];