Chapter 3 - Solving Problems With Arrays - Think Like A Programmer
Chapter 3 - Solving Problems With Arrays - Think Like A Programmer
In the previous chapter, we limited ourselves to scalar variables, that is, The primary attributes of the array follow directly from the definition.
variables that can hold only one value at a time. In this chapter, we’ll look Every value stored in an array is of the same type, whereas other aggre-
at problems using the most common aggregate data structure, the array. gate data structures can store values of mixed types. An individual ele-
Although arrays are simple structures with fundamental limitations, their ment is referenced by a number called a subscript; in other data struc-
use greatly magnifies the power of our programs. tures, individual elements might be referenced by name or by a key
value.
In this chapter, we will primarily deal with actual arrays, that is, those
declared with the built-in C++ syntax, such as: From these primary attributes, we can derive several secondary at-
tributes. Because each of the elements is designated by a number in a se-
int tenIntegerArray[10]; quence starting from 0, we can easily examine every value in an array. In
other data structures, this may be difficult, inefficient, or even impossi-
However, the techniques we discuss apply just as well to data struc- ble. Also, whereas some data structures, such as linked lists, can be ac-
tures with similar attributes. The most common of these structures is a cessed only sequentially, an array offers random access, meaning we can
vector. The term vector is often used as a synonym for any array of a sin- access any element of the array at any time.
gle dimension, but we’ll use it here in the more specific sense of a struc-
ture that has the attributes of an array without a specified maximum These primary and secondary attributes determine how we can use
number of elements. So for our discussions, an array is of a fixed size, arrays. When dealing with any aggregate data structure, it’s good to have
while a vector can grow or shrink automatically as needed. Each of the a set of basic operations in mind as you consider problems. Think of these
problems we discuss in this chapter includes some restriction that allows basic operations as common tools—the hammers, screwdrivers, and
us to use a structure with a fixed number of elements. Problems without wrenches of the data structure. Not every mechanical problem can be
such restrictions, however, could be adapted to use a vector. solved with common tools, but you should always consider whether a
problem can be solved with common tools before making a trip to the
Moreover, the techniques used with arrays can often be used with hardware store. Here’s my list of basic operations for arrays.
data structures that do not have every attribute listed above. Some tech-
niques, for example, don’t require random access, so they can be used Store
with structures like linked lists. Because arrays are so common in pro-
gramming, and because array techniques are frequently used in non-ar-
This is the most basic of operations. An array is a collection of variables, int tenIntegerArray[10] = {4, 5, 9, 12, -4, 0, -57, 30987, -287, 1};
and we can assign a value to each of those variables. To assign the integer int secondArray[10];
5 to the first element (element 0) in the previously declared array, we just for (int i = 0; i < 10; i++) secondArray[i] = tenIntegerArray[i];
say:
That operation is available to most aggregate data structures. The sec-
tenIntegerArray[0] = 5; ond situation is more specific to arrays. Sometimes we want to copy part
of the data from one array to a second array, or we want to copy the ele-
As with any variable, the values of the elements inside our array will ments from one array to a second array as a method of rearranging the
be random “garbage” until particular values are assigned, so arrays order of the elements. If you have studied the merge-sort algorithm,
should be initialized before they are used. In some cases, especially for you’ve seen this idea in action. We’ll see examples of copying later in this
testing, we will want to assign a particular value to every element in the chapter.
array. We can do that with an initializer when the array is declared.
Retrieval and Search
int tenIntegerArray[10] = {4, 5, 9, 12, -4, 0, -57, 30987, -287, 1};
With the ability to put values into the array, we also need the ability to get
We’ll see a good use of an array initializer shortly. Sometimes, instead them out of the array. Retrieving the value from a particular location is
of assigning a different value to each element, we just want every ele- straightforward:
ment in the array to be initialized to the same value. There are some
shortcuts for assigning a zero to every element in the array, depending on int num = tenIntegerArray[0];
the situation or the compiler used (the C++ compiler in Microsoft Visual
Searching for a Specific Value
Studio, for example, initializes every value in any array to zero unless
otherwise specified). At this stage, however, I would always explicitly ini-
Usually the situation isn’t that simple. Often we don’t know the location
tialize an array wherever initialization is required in order to enhance
we need, and we instead have to search the array to find the location of a
readability, as in this code, which sets every element in a 10-element ar-
specific value. If the elements in the array are in no particular order, the
ray to –1:
best we can do is a sequential search, where we look at each element in
the array from one end to the other until we find the desired value. Here’s
int tenIntegerArray[10];
a basic version.
for (int i = 0; i < 10; i++) tenIntegerArray[i] = -1;
We can make a copy of the array. There are two common situations in ➌ int targetValue = 12;
which this might be useful. First, we might want to heavily manipulate ➍ int targetPos = 0;
the array but still require the array in its original form for later process- while ((intArray[targetPos] != targetValue) && (targetPos <
ing. Putting the array back in its original form after manipulation may be ➎ARRAY_SIZE))
difficult, or even impossible, if we’ve changed any of the values. By copy- targetPos++;
ing the entire array, we can manipulate the copy without disturbing the
original. All we need to copy an entire array is a loop and an assignment In this code, we have a constant that stores the size of the array ➊, the
statement, just like the code for initialization: array itself ➋, a variable to store the value we are looking for in the array
➌, and a variable to store the location where the value is found ➍. In this of the highest, is just a matter of switching the “greater-than” comparison
example, we use our ARRAY_SIZE constant to limit the number of iterations ➍ to a “less-than” comparison (and changing the name of the variable so
over our array ➎, so that we won’t run past the end of the array when we don’t confuse ourselves). This basic structure can be applied to all
targetValue is not found among the array elements. You could “hard-wire” sorts of situations in which we want to look at every element in the array
the number 10 in place of the constant, but using the constant makes the to find the value that most exemplifies a particular quality.
code more general, thus making it easy to modify and reuse. We’ll use an
ARRAY_SIZE constant in most of the code in this chapter. Note that if target- Sort
Value is not found in intArray, then targetPos will be equal to ARRAY_SIZE af-
Sorting means putting data in a specified order. You have probably al-
ter the loop. This is enough to signify the event because ARRAY_SIZE is not a
ready encountered sorting algorithms for arrays. This is a classic area for
valid element number. It will be up to the code that follows, however, to
performance analysis because there are so many competing sorting algo-
check that. Also note that the code makes no effort to handle the possibil-
rithms, each with performance characteristics that vary depending on
ity that the target value appears more than once. The first time the target
features of the underlying data. The study of different sorting algorithms
value appears, the loop is over.
could be the subject of an entire book by itself, so we’re not going to ex-
plore this area in its full depth. Instead, we’re going to focus on what is
Criterion-Based Search
practical. For most situations, you can make do with two sorts in your
Sometimes the value we are looking for isn’t a fixed value but a value toolbox: a fast, easy-to-use sort and a decent, easy-to-understand sort that
based on the relationship with other values in the array. For example, we you can modify with confidence when the situation arises. For fast and
might want to find the highest value in the array. The mechanism to do easy, we’ll use the standard library function qsort, and when we need
that is what I call “King of the Hill,” in reference to the playground game. something to tweak, we’ll use an insertion sort.
Have a variable that represents the highest value seen so far in the array.
Fast-and-Easy Sorting with qsort
Run through all the elements in the array with a loop, and each time you
encounter a value higher than the previous highest value, the new value
The default fast sort for C/C++ programmers is the qsort function in the
knocks the previous king off the hill, taking his place:
standard library (the name suggests that the underlying sort employs a
quicksort, but the implementer of the library is not required to use that
const int ARRAY_SIZE = 10;
algorithm). To use qsort, we have to write a comparator function. This
int intArray[ARRAY_SIZE] = {4, 5, 9, 12, -4, 0, -57, 30987, -287, 1};
function will be called by qsort whatever it needs to compare two ele-
➊ int highestValue = intArray[0];
ments in the array to see which should appear earlier in sorted order.
➋ for (int i = 1; i < ARRAY_SIZE; i++) {
The function is passed two void pointers. We haven’t discussed pointers
➌if (intArray[i] ➍> highestValue) highestValue = intArray[i];
yet in this book, but all you need to know here is that you should cast
}
those void pointers to pointers to the element type in your array. Then the
function should return an int, either positive, negative, or zero, based on
The variable highestValue stores the largest value found in the array so
whether the first element is larger, smaller, or equal to the second ele-
far. At its declaration, it is assigned the value of the first element in the
ment. The exact value returned doesn’t matter, only whether it is posi-
array ➊, which allows us to start the loop at the second element in the ar-
tive, negative, or zero. Let’s clear up this discussion with a quick example
ray (it allows us to start with i at 1 instead of 0) ➋. Inside the loop, we
of sorting an array of 10 integers using qsort. Our comparator function:
compare the value at the current position with highestValue, replacing
highestValue if appropriate ➌. Note that finding the lowest value, instead
int compareFunc(➊const void * voidA, const void * voidB) { you had an array of data that you wanted to order based on the data in
➋int * intA = (int *)(voidA); another array. When you have to write your own sort, you will want a
int * intB = (int *)(voidB); straightforward sorting routine that you believe in and can crank out on
➌return *intA - *intB; demand. A reasonable suggestion for a go-to sort is an insertion sort. The
} insertion sort works the way many people would sort cards when playing
bridge: They pick up the cards one at a time and insert them in the appro-
The parameter list consists of two const void pointers ➊. Again, this is priate place in their hands to maintain the overall order, moving the
always the case for the comparator. The code inside the function begins other cards down to make room. Here’s a basic implementation for our
by declaring two int pointers ➋ and casting the two void pointers to the integer array:
int pointer type. We could write the function without the two temporary
variables; I’m including them here for clarity. The point is, once we are ➊ int start = 0;
done with those declarations, intA and intB will point at two elements in ➋ int end = ARRAY_SIZE - 1;
our array, and *intA and *intB will be two integers that must be com- ➌ for (int i = start + 1; i <= end; i++) {
pared. Finally, we return the result of subtracting the second integer from for (➍int j = i; ➎j > start && ➏intArray[j-1] > intArray[j]; j--) {
the first ➌. This produces the result we want. If *intA > *intB, for example, ➐int temp = intArray[j-1];
we want to return a positive number, and *intA – *intB will be positive if intArray[j-1] = intArray[j];
*intA > *intB. Likewise, *intA – *intB will be negative if *intB > *intA and intArray[j] = temp;
will be zero when the two integers are equal. }
}
With the comparator function in place, a sample use of qsort looks like
this: We start by declaring two variables, start ➊ and end ➋, indicating the
subscript of the first and last elements in the array. This improves the
const int ARRAY_SIZE = 10; readability of the code and also allows the code to be easily modified to
int intArray[ARRAY_SIZE] = {87, 28, 100, 78, 84, 98, 75, 70, 81, 68}; sort just a portion of the array, if desired. The outer loop selects the next
qsort(➊intArray, ➋ARRAY_SIZE, ➌sizeof(int), ➍compareFunc); “card” to be inserted into our ever-increasing sorted hand ➌. Notice that
the loop initializes i to start + 1. Remember in the “find the largest
As you can see, the call to qsort takes four parameters: the array to be value” code, we initialized our highest-value variable to the first element
sorted ➊; the number of elements in that array ➋; the size of one element in the array and started our loop with the second element in the array.
in the array, usually determined, as it is here, by the sizeof operator ➌; This is the same idea. If we have only one value (or “card”), then by defi-
and finally, the comparator function ➍. If you haven’t had much experi- nition it is “in order” and we can begin by considering whether the sec-
ence passing functions as parameters to other functions, note the syntax ond value should come before or after the first. The inner loop puts the
used for the last parameter. We are passing the function itself, not calling current value in its correct position by repeatedly swapping the current
the function and passing the result of the call. Therefore, we simply state value with its predecessor until it reaches the correct location. The loop
the name of the function, with no parameter list or parentheses. counter j starts at i ➍, and the loop decrements j so long as we haven’t
reached the lower end of the array ➎ and haven’t yet found the right
Easy-to-Modify Sorting with Insertion Sort
stopping point for this new value ➏. Until then, we use three assignment
statements to swap the current value down one position in the array ➐. In
In some cases, you will need to write your own sorting code. Sometimes
other words, if you had a hand of 13 playing cards and had already sorted
the built-in sort just won’t work for your situation. For example, suppose
the leftmost 4 cards, you could put the 5th card from the left in the cor- cate data integrity problems. As part of a validation report, we might
rect position by repeatedly moving it down one card until it was no write a loop to count the number of negative values in the array:
longer of a lower value than the card to its left. That’s what the inner loop
does. The outer loop does this for every card starting from the leftmost. const int ARRAY_SIZE = 10;
So when we’re done, the entire array is sorted. int countNegative = 0;
for (int i = 0; i < ARRAY_SIZE; i++) {
An insertion sort is not the most efficient sort for most circumstances, if (vendorPayments[i] < 0) countNegative++;
and to tell the truth, the previous code is not even the most efficient way }
to perform an insertion sort. It is reasonably efficient for small to moder-
ately sized arrays, however, and it is simple enough that it can be memo- Solving Problems with Arrays
rized—think of it as a mental macro. Whether you choose this sort or an-
other, you should have one decent or better sorting routine that you can Once you have the common operations understood, solving an array
code yourself with confidence. It’s not enough to have access to someone problem is not much different than solving problems with simple data, as
else’s sorting code that you don’t fully understand. You don’t want to tin- we did in the previous chapter. Let’s take one example and run all the
ker with the machinery if you’re not sure how everything works. way through it using the techniques of the previous chapter and any of
the common operations for arrays that we might need.
Compute Statistics
const int ARRAY_SIZE = 10; In this problem, we’re asked to retrieve one of the values from an ar-
int gradeArray[ARRAY_SIZE] = {87, 76, 100, 97, 64, 83, 88, 92, 74, 95}; ray. Using the techniques of searching for analogies and starting with
double sum = 0; what we know, we might hope that we can apply some variation of the re-
for (int i = 0; i < ARRAY_SIZE; i++) { trieval technique we have already seen: finding the largest value in an ar-
sum += gradeArray[i]; ray. That code works by storing the largest value seen thus far in a vari-
} able. The code then compares each subsequent value to this variable, re-
double average = sum / ARRAY_SIZE; placing it if necessary. The analogous method here would be to say we’d
store the most frequently seen value thus far in a variable and then re-
As another simple example, consider data validation. Suppose an ar- place the value in the variable whenever we discovered a more common
ray of double values called vendorPayments represents payments to ven- value in the array. When we say it like that, in English, it almost sounds as
dors. Only positive values are valid, and therefore negative values indi- if it could work, but when we think about the actual code, we discover the
problem. Let’s take a look at a sample array and size constant for this ➎if (currentFrequency > highestFrequency) {
problem: highestFrequency = currentFrequency;
mostFrequent = surveyData[i];
const int ARRAY_SIZE = 12; }
int surveyData[ARRAY_SIZE] = {4, 7, 3, 8, 9, 7, 3, 9, 9, 3, 3, 10}; ➏currentFrequency = 0;
}
The mode of this data is 3 because 3 appears four times, which is more }
often than any other value. But if we’re processing this array sequentially,
as we do for the “highest value” problem, at what point do we decide that There is no right or wrong way to write pseudocode, and if you use
3 is our mode? How do we know, when we have encountered the fourth this technique, you should adopt your own style. When I write pseu-
and final appearance of 3 in the array, that it is indeed the fourth and fi- docode, I tend to write legal C++ for any statement I’m already confident
nal appearance? There doesn’t seem to be any way to discover this infor- about and then spell out in English the places where I still have thinking
mation with a single, sequential processing of the array data. to do. Here, we know that we will need a variable (mostFrequent) to hold
the most frequently found value so far, which at the end of the loop will
So let’s turn to one of our other techniques: simplifying the problem.
be the mode once we’ve written everything correctly. We also need a vari-
What if we made things easier on ourselves by putting all occurrences of
able to store how often that value occurs (highestFrequency) so we have
the same number together? So, for example, what if our sample array
something to compare against. Finally, we need a variable we can use to
survey data looked like this:
count the number of occurrences of the value we’re currently tracking as
we sequentially process the array (currentFrequency). We know we need to
int surveyData[ARRAY_SIZE] = {4, 7, 7, 9, 9, 9, 8, 3, 3, 3, 3, 10};
initialize our variables. For currentFrequency, it logically has to start at 0,
but it’s not clear how we need to initialize the other variables yet, without
Now both of the 7s are together, the 9s are together, and the 3s are to-
the other code in place. So let’s just drop in question marks ➊ to remind
gether. With the data grouped in this manner, it seems that we should be
us to look at that again later.
able to sequentially process the array to find the mode. Processing the ar-
ray by hand, it’s easy to count the occurrences of each value, because you
The loop itself is the same array-processing loop we’ve already seen,
just keep counting down the array until you find the first number that’s
so that’s already in final form ➋. Inside the loop, we increment the vari-
different. Converting what we can do in our head into programming
able that counts the occurrences of the current value ➌, and then we
statements, however, can be tricky. So before we try writing the code for
reach the pivotal statement. We know we need to check to see whether
this simplified problem, let’s write some pseudocode, which is program-
we’ve reached the last occurrence of a particular value ➍. The pseu-
ming-like statements that are not entirely English or C++ but something in
docode allows us to skip figuring out the logic for now and sketch out the
between. This will remind us what we’re trying to do with each statement
rest of the code. If this is the last occurrence of the value, though, we
we need to write.
know what to do because this is like the “highest value” code: We need to
see whether this value’s count is higher than the highest seen so far. If it
int mostFrequent = ➊?;
is, this value becomes the new most frequent value ➎. Then, because the
int highestFrequency = ➊?;
next value read will be the first occurrence of a new value, we reset our
int currentFrequency = 0;
counter ➏.
➋ for (int i = 0; i < ARRAY_SIZE; i++) {
➌currentFrequency++;
➍if (surveyData[i] IS LAST OCCURRENCE OF A VALUE) {
Let’s return to the if statement logic we skipped. How do we know }
whether this is the last occurrence of a value in the array? Because the }
values in the array are grouped, we know whether a value is the last oc-
currence when the next value in the array is something different: in C++ In this book, we won’t talk much about pure style issues, such as docu-
terms, when surveyData[i] and surveyData[i + 1] are not equal. mentation (commenting) style, but since we are using pseudocode on this
Furthermore, the last value in the array is also the last occurrence of problem, I want to mention a tip. I’ve noticed that the lines I leave as
some value, even though there’s not a next value. We can check for this “plain English” in the pseudocode are the lines that benefit most from a
by checking to see whether i == ARRAY_SIZE - 1, in which case this is the comment in the final code, and the plain English itself makes a great com-
last value in the array. ment. I’ve demonstrated that in the code here. You might forget the exact
meaning behind the conditional expression in the if statement ➋, but the
With all of that figured out, let’s think about those initial values for comment on the preceding line ➊ clears things up nicely.
our variables. Remember with the “highest value” array-processing code,
we initialized our “highest so far” variable to the first value in the array. As for the code itself, it does the job, but remember that it requires our
Here, the “most frequently seen” value is represented by two variables, survey data to be grouped. Grouping the data might be a job in itself, ex-
mostFrequent for the value itself and highestFrequency for the number of oc- cept—what if we sorted the array? We don’t actually need the data to be
currences. It would be great if we could initialize mostFrequent to the first sorted, but sorting will accomplish the grouping we need. Because we
value that appears in the array and highestFrequency to its frequency don’t intend to do any special kind of sorting, let’s just add this call to
count, but there’s no way to determine the first value’s frequency until we qsort to the beginning of our code:
get into the loop and start counting. At this point, it might occur to us that
the first value’s frequency, whatever it is, would be greater than zero. qsort(surveyData, ARRAY_SIZE, sizeof(int), compareFunc);
Therefore, if we set highestFrequency to zero, once we reach the last occur-
rence of the first value, our code will replace mostFrequent and highestFre- Note that we’re using the same compareFunc we wrote earlier for use
quency with the numbers for the first value anyway. The completed code with qsort. With the sorting step in place, we have a complete solution to
looks like this: the original problem. So our work is done. Or is it?
Refactoring
int mostFrequent;
int highestFrequency = 0;
Some programmers talk about code that gives off “bad smells.” They are
int currentFrequency = 0;
talking about working code that is free of bugs but still problematic in
for (int i = 0; i < ARRAY_SIZE; i++) {
some way. Sometimes this means code that is too complicated or has too
currentFrequency++;
many special cases, making the program difficult for a programmer to
➊// if (surveyData[i] IS LAST OCCURENCE OF A VALUE)
modify and maintain. In other cases, the code isn’t as efficient as it could
➋if (i == ARRAY_SIZE - 1 || surveyData[i] != surveyData[i + 1]) {
be, and while it works for test cases, the programmer worries that perfor-
if (currentFrequency > highestFrequency) {
mance will break down with larger cases. That’s my concern here. The
highestFrequency = currentFrequency;
sorting step is nearly instantaneous for our tiny test case, but what if the
mostFrequent = surveyData[i];
array is huge? Also, I know that the quicksort algorithm, which qsort may
}
be using, has its lowest performance when there are lots of duplicate val-
currentFrequency = 0;
ues in the array, and the whole point of this problem is that all of our val-
ues are in the range 1–10. I therefore propose to refactor the code.
Refactoring means improving working code, not changing what it does our 7 counter. We subtract 1 from 7 to get 6 because the counter for 7s is
but how it does it. I want a solution that is highly efficient for even huge in position [6] in histogram, and histogram[6] is incremented.
arrays, assuming that the values are in the range of 1–10.
With the histogram data in place, we can write the rest of the code.
Let’s think again about the operations we know how to do with arrays. Note that the histogram code was written separately so that it could be
We’ve already explored several versions of the “find the highest” code. tested separately. No time is saved by writing all of the code at once in a
We know that applying the “find the highest” code directly to our survey- situation where the problem is easily separated into parts that can be in-
Data array won’t produce useful results. Is there an array to which we dividually written and tested. Having tested the above code, we now
could apply the “stock” version of “find the highest” and get the mode of search for the largest value in the histogram array:
the survey data? The answer is yes. The array we need is the histogram of
the surveyData array. A histogram is a graph showing how often different ➊ int mostFrequent = 0;
values appear in an underlying dataset; our array will be the data for for (int i = 1; i < MAX_RESPONSE; i++) {
such a histogram. In other words, we’ll store, in a 10-element array, how if (histogram[i] > ➋histogram[mostFrequent]) ➌mostFrequent = i;
often each of the values 1 through 10 appears in surveyData. Here’s the }
code to create our histogram: ➍ mostFrequent++;
const int MAX_RESPONSE = 10; Although this is an adaptation of the “find the highest” code, there is a
➊ int histogram[MAX_RESPONSE]; difference. Although we are searching for the highest value in the his-
➋ for (int i = 0; i < MAX_RESPONSE; i++) { togram array, ultimately, we don’t want the value itself, but the position.
histogram[i] = 0; In other words, with our sample array, we want to know that 3 occurs
} more often than any other value in the survey data, but the actual num-
➌ for (int i = 0; i < ARRAY_SIZE; i++) { ber of times 3 occurs isn’t important. So mostFrequent will be the position
➍histogram[surveyData[i] - 1]++; of the highest value in histogram, not the highest value itself. Therefore,
} we initialize it to 0 ➊ and not the value in location [0]. This also means
that in the if statement, we compare against histogram[mostFrequent] ➋
On the first line, we declare the array to hold our histogram data ➊. and not mostFrequent itself, and we assign i, not histogram[i], to mostFre-
You’ll note we declare the array with 10 elements, but the range of our quent ➌ when a larger value is found. Finally, we increment mostFrequent
survey responses is 1–10, and the range of subscripts for this array is 0–9. ➍. This is the reverse of what we did in the earlier loop, subtracting 1 to
Thus, we’ll have to make adjustments, putting the count of 1s in get the right array position. If mostFrequent is telling us that the highest ar-
histogram[0] and so on. (Some programmers would choose to declare the ray position is 5, for example, it means that the most frequent entry in the
array with 11 elements, leaving location [0] unused, to allow each count survey data was 6.
to go into its natural position.) We explicitly initialize the array values to
zero with a loop ➋, and then we are ready to count the occurrences of The histogram solution scales linearly with the number of elements in
each value in surveyData with another loop ➌. The statement inside the our surveyData array, which is as good as we could hope for. Therefore, it’s
loop ➍ has to be read carefully; we are using the value in the current lo- a better solution than our original approach. This doesn’t mean that the
cation of surveyData to tell us which position in histogram to increment. To first approach was a mistake or a waste of time. It’s possible, of course, to
make this clear, let’s take an example. Suppose i is 42. We inspect have written this code without going through the previous version, and
surveyData[42] and find (let’s say) the value 7. So we need to increment we can be forgiven for wishing that we had driven directly to our destina-
tion instead of taking the longer route. However, I would caution against would not scale well if the number of punctuation symbols increased. We
slapping yourself on the forehead on those occasions when the first solu- can use an array to solve this problem instead of the switch statement.
tion turns out not to be the final solution. Writing an original program First, we need to permanently assign the punctuation symbols to an array
(and remember this means original for the programmer writing it) is a in the same order they appear in the coding scheme:
learning process and can’t be expected to always progress in a straight
line. Also, it’s often the case that taking a longer path on one problem const char punctuation[8] = {'!', '?', ',', '.', ' ', ';', '"', '\''};
helps us take a shorter path on a later problem. In this particular case,
note that our original solution (while it doesn’t scale well for our particu- Notice that this array has been declared const because the values in-
lar problem) could be the right solution if the survey responses weren’t side will never change. With that declaration in place, we can replace the
strictly limited to the small range of 1–10. Or suppose that you are later entire switch statement with a single assignment statement that refer-
asked to write code that finds the median of a set of integer values (the ences the array:
median is the value in the middle, such that half of the other values in the
set are higher and half of the other values are lower). The histogram ap- outputCharacter = punctuation[number - 1];
proach doesn’t get you anywhere with the median, but our first approach
Because the input number is in the range 1–8, but array elements are
for the mode does.
numbered starting from 0, we have to subtract 1 from the input number
The lesson here is that a long journey is not a waste of time if you before referencing the array; this is the same adjustment we made in the
learned something from it that you wouldn’t have learned by going the histogram version of the “Finding the Mode” program. You can use the
short way. This is another reason why it’s helpful to methodically store all same array to go in the other direction. Suppose instead of decoding the
of the code that you write so that you can easily find and reuse it later. message, we had to encode a message—that is, we were given a series of
Even the code that turns out to be a “dead end” can become a valuable characters to convert into numbers that could be decoded using the rules
resource. of the original problem. To convert a punctuation symbol into its number,
we have to locate the symbol in the array. This is a retrieval, performed
Arrays of Fixed Data using the sequential search technique. Assuming the character is to be
converted and stored in the char variable targetValue, we could adapt the
In most array problems, the array is a repository for data external to the sequential search code as follows:
program, such as user-entered data, data on a local disk, or data from a
server. To get the most out of the array tool, however, you need to recog- const int ARRAY_SIZE = 8;
nize other situations in which an array can be used. It’s often useful to int targetPos = 0;
create an array where the values never change after the initialization. while (punctuation[targetPos] != targetValue && targetPos < ARRAY_SIZE)
Such an array can allow a simple loop or even a direct array lookup to re- targetPos++;
place a whole block of control statements. int punctuationCode = targetPos + 1;
In the final code for the “Decode a Message” problem on page 52, we Note that just as we had to subtract 1 from number in the previous ex-
used a switch statement to translate the decoded input number (in the ample to get the right array position, we have to add 1 to the array posi-
range 1–8) to the appropriate character when in “punctuation mode” be- tion in this example to get our punctuation code, converting from the
cause the connection between the number and the character was arbi- array’s range of 0–7 to our punctuation code range of 1–8. Although this
trary. Although this worked fine, it made that section of code longer than code is not as simple as a single line, it’s still much simpler than a series
the equivalent code for the uppercase and lowercase modes, and the code of switch statements, and it scales well. If we were to double the number
of punctuation symbols in our coding scheme, it would double the num- This code uses two arrays of fixed values. The first array stores the
ber of elements in the array, but the length of the code would stay the gross sales threshold for each business category ➊. For example, a busi-
same. ness with $65,000 in yearly gross sales is in category II because this
amount exceeds the $50,000 threshold of category II but is less than the
In general, then, const arrays can be used as lookup tables, replacing a $150,000 threshold of category III. The second array stores the cost of a
burdensome series of control statements. Suppose you are writing a pro- business license for each category ➋. With the arrays in place, we initial-
gram to compute the cost of a business license in a state where the license ize category to 0 ➌ and search through the categoryThresholds array, stop-
cost varies as the gross sales figures of the business vary. ping when the threshold exceeds the gross sales or when we run out of
categories ➍. In either case, when the loop is done, category will be cor-
Table 3-1: Business License Costs
rectly assigned 1–4 based on the gross sales. The last step is to use cate-
gory to reference the license cost from the licenseCost array ➎. As before,
Business category Sales threshold License cost we have to make a small adjustment from the 1–4 range of the business
categories to the 0–3 range of our array.
I $0 $25
Non-scalar Arrays
II $50,000 $200
So far, we’ve just worked with arrays of simple data types, such as int and
III $150,000 $1,000 double. Often, however, programmers must deal with arrays of compound
data, either structures or objects (struct or class). Although the use of
IV $500,000 $5,000 compound data types necessarily complicates the code somewhat, it
doesn’t have to complicate our thinking about array processing. Usually
the array processing just involves one data member of the struct or class,
With this problem, we could use arrays both to determine the business
and we can ignore the other parts of the data structure. Sometimes,
category based on the company’s gross sales and to assign the license cost
though, the use of compound data types requires us to make some
based on the business category. Suppose a double variable, grossSales,
changes to our approach.
stores the gross sales of a business, and based on the sales figure, we
want to assign the proper values to int category and double cost:
For example, consider the problem of finding the highest of a set of
student grades. Suppose that instead of an array of int, we have an array
const int NUM_CATEGORIES = 4;
of data structures, each representing a student’s record:
➊ const double categoryThresholds[NUM_CATEGORIES ] =
{0.0, 50000.0, 150000.0, 500000.0};
struct student {
➋ const double licenseCost[NUM_CATEGORIES ] =
int grade;
{50.0, 200.0, 1000.0, 5000.0}; int studentID;
➌ category = 0; string name;
➍ while (category < NUM_CATEGORIES && };
categoryThresholds[category] <= grossSales) {
category++; One nice thing about working with arrays is that it is easy to initialize
} a whole array with literal values for easy testing, even with an array of
➎ cost = licenseCost[category - 1]; struct:
const int ARRAY_SIZE = 10; is the most general because tracking the array position allows us to re-
student studentArray[ARRAY_SIZE] = { trieve any of the data for that student later:
{87, 10001, "Fred"},
{28, 10002, "Tom"}, ➊ int highPosition = 0;
{68, 10010, "Veronica"} we aren’t directly tracking the highest grade, when it’s time to compare
}; the highest grade against the current grade, we use highPosition as a ref-
erence into studentArray ➋. If the grade in the current array position is
This declaration means that studentArray[0] has an 87 for its grade, higher, the current position in our processing loop is assigned to highPosi-
10001 for its studentID, and “Fred” for a name, and so on for the other nine tion ➌. Once the loop is over, we can access the name of the student with
elements in the array. As for the rest of the code, it could be as simple as the highest grade using studentArray[highPosition].name, and we can also
copying the code from the beginning of this chapter, and then replacing access any other data related to that student record.
every reference of the form intArray[subscript] with
studentArray[subscript].grade. That would result in the following: Multidimensional Arrays
int highest = studentArray[0].grade; So far, we’ve only discussed one-dimensional arrays because they are the
for (int i = 1; i < ARRAY_SIZE; i++) { most common. Two-dimensional arrays are uncommon, and arrays with
if (studentArray[i].grade > highest) highest = studentArray[i].grade; three or more dimensions are rare. That’s because most data is one-di-
the student that matches the current value in highest, or, instead of track- {0.0, 50000.0, 150000.0, 500000.0},
ing the highest grade, track the location in the array where the highest {50.0, 200.0, 1000.0, 5000.0}
grade is found, much as we did with histogram earlier. The latter approach };
It’s difficult to discern any advantage from combining the two arrays to the first element in the array, as usual ➋. It may occur to you that the
into one. None of our code is simplified because there is no reason to first time through the nested loops, both of our loop counters will be 0, so
process all of the data in the table at once. What is clear, though, is that we will be comparing this initial value of highestSales to itself. This
we have lowered the readability and ease of use for our table data. In the doesn’t affect the outcome, but sometimes novice programmers will at-
original version, the names of the two separate arrays make it clear what tempt to avoid this tiny inefficiency by putting in a second if statement in
data is stored in each. With the combined array, we programmers will the inner loop body:
have to remember that references of the form licenseData[0][] refer to
the gross sales thresholds of the different business categories, while refer- if (agent != 0 || month != 0)
ences of the form licenseData[1][] refer to business license costs. if (sales[agent][month] > highestSales)
highestSales = sales[agent][month];
Sometimes it does make sense to use a multidimensional array,
though. Suppose we are processing the monthly sales data for three sales This, however, is considerably less efficient than the previous version
agents, and one of the tasks is finding the highest monthly sales, from any because we would be performing 50 extra comparisons while avoiding
agent. Having all of the data in one 3 x 12 array means we can process the only one.
entire array at once, using nested loops:
Notice also that I have used meaningful names for the loop variables:
const int NUM_AGENTS = 3; agent for the outside loop ➌ and month for the inside loop ➍. In a single
const int NUM_MONTHS = 12; loop that processes a one-dimensional array, little is gained by a descrip-
➊ int sales[NUM_AGENTS][NUM_MONTHS] = { tive identifier. In a double loop that processes a two-dimensional array,
{1856, 498, 30924, 87478, 328, 2653, 387, 3754, 387587, 2873, 276, 32}, however, the meaningful identifiers help me keep my dimensions and
{5865, 5456, 3983, 6464, 9957, 4785, 3875, 3838, 4959, 1122, 7766, subscripts straight because I can look up and see that I am using agent in
2534}, the same dimension where I used NUM_AGENTS in the array declaration.
{23, 55, 67, 99, 265, 376, 232, 223, 4546, 564, 4544, 3434}
}; Even when we have a multidimensional array, sometimes the best ap-
proach is to deal with just one dimension at a time. Suppose, using the
➋ int highestSales = sales[0][0]; same sales array as the previous code, we wanted to display the highest
for (➌int agent = 0; agent < NUM_AGENTS; agent++) { agent monthly sales average. We could do this using a double loop, as we
for (➍int month = 0; month < NUM_MONTHS; month++) { have previously, but the code would be clearer to read and easier to write
if (sales[agent][month] > highestSales) if we treated the whole array as three individual arrays and processed
}
Remember the code we’ve been repeatedly using to compute the aver-
}
age of an array of integers? Let’s make that into a function:
With the function in place, we can modify the basic “find the largest The last thing to note about this code is the use of the temporary vari-
number” again to find the agent with the highest monthly sales average: able, agentAverage. Because the average monthly sales for the current
agent is potentially referenced twice, once in the conditional expression
double highestAverage = ➊arrayAverage(sales[0], 12); of the if statement and then again in the assignment statement in the
for (int agent = 1; agent < NUM_AGENTS; agent++) { body, the temporary variable eliminates the possibility of calling arrayAv-
double agentAverage = ➋arrayAverage(sales[agent], 12); erage twice for the same agent’s data.
if (agentAverage > highestAverage)
highestAverage = agentAverage; This technique of considering a multidimensional array as an array of
} arrays follows directly from our core principle of breaking problems up
cout << "Highest monthly average: " << highestAverage << "\n"; into simpler components and in general makes multidimensional array
problems a lot easier to conceptualize. Even so, you may be thinking that
The big new idea here is shown in the two calls to arrayAverage. The the technique looks a little tricky to employ, and if you’re like most new
first parameter accepted by this function is a one-dimensional array of C++ programmers, you are probably a little wary of addresses and be-
int. In the first call, we pass sales[0] for the first argument ➊, and in the hind-the-scenes address arithmetic. The best way around those feelings, I
second call, we pass sales[agent] ➋. So in both cases, we specify a sub- think, is to make the separation between the dimensions even stronger,
script for the first dimension of our two-dimensional array sales, but not by placing one level of array inside a struct or class. Suppose we made an
for the second dimension. Because of the direct relationship between ar- agentStruct:
rays and addresses in C++, this reference indicates the address of the first
element of the specified row, which can then be used by our function as struct agentStruct {
the base address of a one-dimensional array consisting of just that row. int monthlySales[12];
};
If that sounds confusing, look again at the declaration of the sales ar-
ray, and in particular, the initializer. The values are laid out in the initial- Having gone to the trouble of making a struct, we might think about
izer in the same order they will be laid out in memory when the program adding other data, like an agent identification number, but this will get
is executing. So sales[0][0], which is 1856, will come first, followed by the job done in terms of simplifying our thought processes. With the
sales[0][1], 498, and so on through the last month for the first agent, struct in place, instead of creating a two-dimensional array of sales, we
sales[0][11], 32. Then the values for the second agent will begin, starting create a one-dimensional array of agents:
values.
Now when we make our call to the array-averaging function, we aren’t
It’s important to note that this technique works because of the order employing a C++ specific trick; we’re just passing a one-dimensional ar-
we’ve placed the data into the array. If the array were organized along ray. For example:
the other axis, that is, by month instead of by agent, we couldn’t do what
int highestAverage = arrayAverage(agents[1].monthlySales, 12);
we are doing here. The good news is that there is an easy way to make
Deciding When to Use Arrays tation ➋, even though surveyData is declared as a pointer. Note that be-
cause this array is dynamically allocated, at the end of the program when
An array is just a tool. As with any tool, an important part of learning how we no longer need the array, we have to make sure to deallocate it:
to use an array is learning when to use it—and when not to use it. The
sample problems discussed so far assumed the use of arrays in their de- delete[] surveyData;
scriptions. In most situations, though, we won’t have this detail spelled
out for us, and we must instead make our own determination on array The delete[] operator, rather than the usual delete operator, is used
use. The most common situations in which we must make this decision for arrays. Although it won’t make any difference with an array of inte-
are those in which we are given aggregate data but not told how it must gers, if you create an array of objects, the delete[] operator ensures that
be stored internally. For example, in the problem where we found the the individual objects in the array are deleted before the array itself is
mode, suppose the line that began Write code that processes an array of deleted. So you should adopt the habit of always using delete[] with dy-
survey data ..., had read Write code that processes a collection of survey namically allocated arrays.
data .... Now the choice of using an array or not would be ours. How
would we make this decision? Having the responsibility of cleaning up dynamic memory is the bane
of the C++ programmer, but if you program in the language, it is some-
Remember that we cannot change the size of an array after it has been thing you simply must do. Beginning programmers often shirk this re-
created. If we ran out of space, our program would fail. So the first con- sponsibility because their programs are so small and execute for such
sideration is whether we will know, at the place in our program where short periods of time that they never see the harmful effects of memory
we need an aggregate data structure, how many values we will store or at leaks (memory that is no longer used by the program but never deallo-
least a reliable estimate on the maximum size. This doesn’t mean we have cated and therefore unavailable to the rest of the system). Don’t develop
to know the size of the array when we write the program. C++, as well as this bad habit.
most other languages, allows us to create an array that is sized at run-
time. Suppose the mode problem was modified so that we didn’t know Note that we can use the dynamic array only because the user tells us
ahead of time how many survey responses we would have, but that num- the number of survey responses beforehand. Consider another variant
ber came to the program as user input. Then we could dynamically de- where the user begins by entering survey responses without telling us the
clare an array to store the survey data. number of responses, indicating that there are no more responses by en-
tering a –1 (a data entry method known as a sentinel). Can we still use an
int ARRAY_SIZE; array to solve this problem?
cout << "Number of survey responses: ";
cin >> ARRAY_SIZE; This is a gray area. We could still use an array if we had a guaranteed
➊ int *surveyData = new int[ARRAY_SIZE]; maximum number of responses. In such a case, we could declare an ar-
for(int i = 0; i < ARRAY_SIZE; i++) { ray of that size and assume that we are safe. We might still have concerns
cout << "Survey response " << i + 1 << ": "; over the long term, though. What if the size of the survey pool increases
➋cin >> surveyData[i]; in the future? What if we want to use the same program with a different
} survey taker? More generally, why build a program with a known limita-
tion if we can avoid it?
We declare the array using pointer notation, initializing it through an
invocation of the new operator ➊. Because of the fluidity between pointer Better, then, to use a data collection without a fixed size. As discussed
and array types in C++, the elements can then be accessed using array no- earlier, the vector class from the C++ standard template library acts like
an array but grows as necessary. Once declared and initialized, the vector entered value before processing. In this case, we want to avoid adding the
can be processed exactly the same way as an array. We can assign a value sentinel value, –1, to our vector. The survey results are added to the vec-
to a vector element or retrieve a value using standard array notation. If tor using the push_back method ➍. After the data entry loop is completed,
the vector has filled its initial size and we need to add another element, we retrieve the size of the vector using the size method ➎. We could also
we can do so using the push_back method. Solving the modified problem have counted the number of elements ourselves in the data entry loop,
with a vector looks like this: but since the vector is already tracking its size, this avoids duplicate ef-
fort. The rest of the code is the same as the previous version with the ar-
➊ vector<int> surveyData; ray and the fixed number of responses, except that we have changed the
➋ surveyData.reserve(30); names of the variables.
int surveyResponse;
cout << "Enter next survey response or -1 to end: "; All this discussion of vectors, though, overlooks an important point. If
➌ cin >> surveyResponse; we are reading the data directly from the user, rather than being told that
while (surveyResponse != -1) { we are starting with an array or other data collection, we may not need
➍surveyData.push_back(surveyResponse); an array for the survey data, only one for the histogram. Instead, we can
cout << "Enter next survey response or -1 to end: "; process the survey values as we read them. We need a data structure only
cin >> surveyResponse; when we need to read in all the values before processing or need to
} process the values more than once. In this case, we don’t need to do
} histogram[i] = 0;
In this code, we first declare the vector ➊ and then reserve space for int mostFrequent = 0;
30 survey responses ➋. The second step is not strictly necessary, but re- for (int i = 1; i < MAX_RESPONSE; i++) {
serving a small amount of space that is in excess of the likely number of if (histogram[i] > histogram[mostFrequent]) mostFrequent = i;
values to it. We read the first grade before the data entry loop ➌, a tech- mostFrequent++;
nique we first used in the previous chapter that allows us to check each
Although this code was easy to write, given the previous versions as a dom access. If we need only sequential access, we might consider a differ-
guide, it would have been even easier just to read the user data into an ent structure.
array and use the previous processing loop verbatim. The benefit to this
process-as-you-go approach is efficiency. We avoid unnecessarily storing You might notice that many of the programs in this chapter fail on this
each of the survey responses, when we need to store just one response at last criterion; we access the data sequentially, not randomly, and yet we
a time. Our vector-based solution was inefficient in space: It took more are using an array. This leads to the great, common-sense exception to all
space than required without providing a corresponding benefit. of these rules. If an array is small, then none of the previous objections
Furthermore, reading all of the survey responses into the vector required holds much weight. What constitutes “small” may vary based on the plat-
a loop on its own, separate from the loops to process all of the survey re- form or application. The point is, if your program needs a collection of as
sponses and find the highest value in the histogram. That means the vec- few as 1 or as many as 10 items, each of which requires 10 bytes, you
tor version does more work than the version above. Therefore, the vector have to consider whether the potential waste of 90 bytes that could result
version is also inefficient in time: It does more work than required with- from allocating an array of the maximum required size is worth search-
out providing a corresponding benefit. In some cases, different solutions ing for a better solution. Use arrays wisely, but don’t let the perfect be the
offer trade-offs, and programmers must decide between space efficiency enemy of the good.
and time efficiency. In this case, however, the use of the vector makes the
program inefficient all around. Exercises
3-5. Have the previous program convert the ciphertext back to the plaintext to
verify the encoding and decoding.
3-6. To make the ciphertext problem even more challenging, have your pro-
gram randomly generate the cipher array instead of a hard-coded const
array. Effectively, this means placing a random character in each element
of the array, but remember that you can’t substitute a letter for itself. So
the first element can’t be A, and you can’t use the same letter for two sub-
stitutions—that is, if the first element is S, no other element can be S.
3-7. Write a program that is given an array of integers and determines the
mode, which is the number that appears most frequently in the array.
3-8. Write a program that processes an array of student objects and deter-
mines the grade quartiles—that is, the grade one would need to score as
well as or better than 25% of the students, 50% of the students, and 75%
of the students.
3-9. Consider this modification of the sales array: Because salespeople come
and go throughout the year, we are now marking months prior to a sales
agent’s hiring, or after a sales agent’s last month, with a –1. Rewrite your
highest sales average, or highest sales median, code to compensate.