Computer Science 111, Assignment 5
Tutorial on nested loops and efficiency of loops
cp ~nixon/cs111/hw05/* .
The pattern is generated using nested loops:
for ( int row = 0; row < 10; row++ )
{
for ( int column = 0; column < 15; column++ )
cout << " *";
cout << endl;
} // for row
Each iteration of the outer loop draws a complete row, which is generated by the inner loop. Each iteration of the inner loop prints out a space and an asterisk.
Observe that curly braces are needed for the outer for loop, because more than one statement needs to be executed for each iteration. The closing curly brace is commented with the word "for" and the name of the loop control variable. Such comments are especially helpful in programs that have lots of deeply nested loops.
For more about nested loops, please review Dale, pp. 296 to 302, in Chapter 6.
| 1 2 3 4 5 6 7 8 9 10 11 12
-------+--------------------------------------------------------------
1 | 1 2 3 4 5 6 7 8 9 10 11 12
2 | 2 4 6 8 10 12 14 16 18 20 22 24
3 | 3 6 9 12 15 18 21 24 27 30 33 36
4 | 4 8 12 16 20 24 28 32 36 40 44 48
5 | 5 10 15 20 25 30 35 40 45 50 55 60
6 | 6 12 18 24 30 36 42 48 54 60 66 72
7 | 7 14 21 28 35 42 49 56 63 70 77 84
8 | 8 16 24 32 40 48 56 64 72 80 88 96
9 | 9 18 27 36 45 54 63 72 81 90 99 108
10 | 10 20 30 40 50 60 70 80 90 100 110 120
11 | 11 22 33 44 55 66 77 88 99 110 121 132
12 | 12 24 36 48 60 72 84 96 108 120 132 144
In multiplicationTable1.cpp, the body of the table (i.e. the part of the table below the dashed line) is generated as follows:
for ( int row = 1; row <= 12; row++ )
{
cout << setw(7) << row << " |";
for ( int column = 1; column <= 12; column++ )
cout << setw(5) << (row * column);
cout << endl;
} // for row
whereas, in multiplicationTable2.cpp, the body of the table (i.e. the part of the table below the dashed line) is generated as follows:
for ( int row = 1; row <= 12; row++ )
{
cout << setw(7) << row << " |";
int product = 0;
for ( int column = 1; column <= 12; column++ )
{
product = product + row;
cout << setw(5) << product;
} // for column
cout << endl;
} // for row
In both versions, the setw manipulator is used to print a number with a specified column width by printing leading spaces in front of the number. For more information about setw, see Dale, Chapter 3, pp. 123 to 126.
Both versions use nested for loops, the outer loop printing out 12 rows, with the inner loop generating each individual row by printing out all the numbers in the row. In multiplicationTable1.cpp, the numbers to printed are generated by simply multiplying the row number by the column number. In multiplicationTable2.cpp, the numbers are generated by repeatedly adding the row number to the previous number in the row.
Examine the table to verify that the second algorithm is indeed valid.
In this program, the numbers are small enough that there is no significant difference in efficiency between addition and multiplication.
However, for large numbers with many digits, addition can be done much more efficiently than multiplication. Hence, if you are given a choice between two algorithms to solve some problem, and if one of the algorithms involves multiplication operations, and the other algorithm involves the same total number of arithmetic operations, but uses addition instead of multiplication, the algorithm involving addition will usually (though not always) be more efficient.
But there are other programs in which nested loops may, at first, seem appropriate, but in which the nested loops are in fact unnecessary and a waste of computer time. An example is polynomial1.cpp, which computes a polynomial. Suppose we want to compute the value of the following polynomial, where x = 2:
3 2
2x + 3x + 4x + 5
The most obvious algorithm would be to compute each term and add it to a cumulative sum, starting with the highest-order term (2 times x cubed), then the next term (3 times x squared), and so on. Thus, polynomial1.cpp uses nested for loop, with the outer loop computing the cumulative sum of terms and the inner loop computing the power of x for each term. In the inner loop, the power of x is computed as a cumulative product:
double powerOfX = 1;
for ( int i = 0; i < order; i++ )
powerOfX = powerOfX * x;
The inefficiency here is that the powers of x are computed from scratch within the computation of each term. Suppose that the order-3 term, containing x cubed, is computed first. In order to compute x cubed, it is necessary to compute x squared first. But then, when the order-2 term is computed, x squared is computed again. In a higher-degree polynomial, there would be a lot similarly redundant multiplications by x.
To avoid computing the powers of x from scratch in each term, one way would be to compute the lowest-order terms first, remembering the power of x for that term in a memory location designated for that purpose, then computing the power of x for the next higher-order term by simply multiplying the current power of x by x. Such an algorithm is used in polynomial2.cpp.
Note that polynomial2.cpp does NOT have nested loops.
However, polynomial2.cpp requires the lowest-order coefficients to be input first. Suppose, for whatever reason, that we have been told to write a program in which the user enters the highest-order coefficients first, as is done in polynomial1.cpp. We can still develop a more efficient algorithm than the one used in polynomial1.cpp.
Look now at polynomial3.cpp, which inputs the coefficients in the same order as polynomial1.cpp, but still does no more multiplications than polynomial2.cpp. Observe also that polynomial3.cpp does not have a powerOfX variable, and thus uses one less memory location than polynomial2.cpp.
To see how this is possible, observe that the polynomial:
3 2
2x + 3x + 4x + 5
cab be re-written as:
((2x + 3)x + 4)x + 5
Thus, instead of computing the polynomial as a sum of terms, we alternately add coefficients and multiply the entire polynomial-so-far by x.
Observe that polynomial2.cpp, like polynomial3.cpp, does NOT have nested loops.
4 4 4 4 4 4
pi = - - - + - - - + - - -- + ...
1 3 5 7 9 11
When you run the program, it displays a table of the first 11 approximations. The first line of the table displays the zeroth term, i.e. 4; the next line displays the sum of the zeroth plus first terms, i.e. 4 - 4/3; the next line displays the sum of the zeroth plus first plus second terms, i.e. 4 - 4/3 + 4/5; the next line displays the sum of 4 - 4/3 + 4/5 - 4/7; and so on. As you can see, none of the displayed series approximations are very close yet.
Look now at the source code. After initializing a cumulative sum to zero:
double sum = 0;
this program then contains a for loop, each iteration of which computes a term in the series, adds it to the sum, and displays a row of the table. The for loop has the following heading:
for ( int n = 0; n < 12; n++ )
The heading is followed by an opening curly brace. Note that curly braces are needed for this loop, because the body contains more than one statement.
Each term in the series is computed as follows:
double term = 4.0 / (1 + 2*n);
if ( n % 2 == 1 )
term = term * -1;
Then, each term is added to the cumulative sum:
sum = sum + term;
Then the corresponding row of the table is displayed.
Look now at the entire program and make sure you understand how it works.
Then compile piSeries2.cpp and run it. This program calculates the first 1,000,001 series approximations. It does not display them all, but displays only those series approximations for which n is a power of 10.
Look now at the source code of piSeries2.cpp. This program contains a for loop which iterates 1,000,001 times. In each iteration, a term is calculated and added to the cumulative sum. However, a row of the table is displayed only if n is a power of 10.
To test whether n is a power of 10, we compare n to powerOfTen, a variable which is initialized to 1 and multiplied by 10 each time n reaches the current value of powerOfTen. This method works only because n counts up by one each iteration of the loop and thus eventually reaches the next value of
powerOfTen.
(The algorithm used here would NOT work as a means of determining whether any number, in any circumstance, is a power of 10.)
double term = 4.0 / (1 + 2*n);
In piSeries3.cpp, the multiplication by 2 is eliminated. Instead, there is the following statement inside the loop:
double term = 4.0 / termDenominator;
where termDenominator is a variable which was initialized before the loop. It is incremented at the end of the loop body by adding 2 to it:
// Calculate termDenominator for next n:
termDenominator += 2;
Thus, each time through the loop, we add 2 to termDenominator. The first time through the loop, i.e. for n = 0, termDenominator equals 1. The next time through the loop, i.e. for n = 1, termDenominator is 3. For n = 2, termDenominator is 5. And so on.
In piSeries2.cpp, the same result was accomplished using the formula 1 + 2*n for each term's denominator. Note that this formula involves multiplication, whereas, in piSeries.cpp, we used only addition to calculate each term's denominator.
As we mentioned earlier, an algorithm involving addition or subtraction operations is generally to be preferred to an algorithm involving the same number of multiplication or division operations, because multiplication and division typically consumes much more processor time than addition and subtraction. In this particular example, it doesn't make any difference, because we are multiplying by 2, which can be done very efficiently in binary arithmetic. But, if we were multiplying by a number other than a power of 2, the time saved by eliminating unnecessary multiplication could be significant.
Now compile, run, and examine the application unnecessaryNesting1.cpp. This program takes an inordinately long time to print all seven rows of the table. It will print out the first four rows fairly promptly, but you may then have to wait up to a minute for the fifth row to be printed out. Then, don't bother waiting for the sixth row. Type [Ctrl]-C (press the C key while holding down the [Ctrl] key) to make the program quit.
In unnecessaryNesting1.cpp, there is an additional -- and unnecessary -- nested loop. Each time through the outer loop, the entire sum up to the nth term is calculated. In other words, each time through the outer loop, the program starts the whole calculation all over again, rather than using the previous result. No wonder it takes so long.
Now compile unnecessaryNesting2.cpp and run it. This program is not as slow as unnecessaryNesting1.cpp, but is still quite slow compared to piSeries2.cpp or piSeries3.cpp. In a misguided attempt to eliminate the multiplications in piSeries2.cpp, unnecessaryNesting2.cpp has replaced the following statement in piSeries2.cpp:
double term = 4.0 / (1 + 2*n);
with the following statements, which include a nested loop:
int termDenominator = 1;
for ( int i = 0; i < n; i++ )
termDenominator += 2;
double term = 4.0 / termDenominator;
Compare this with piSeries3.cpp. Note that the version in unnecessaryNesting2.cpp re-calculates termDenominator from scratch each time through the outer for loop, rather than making use of the result for the previous value of n, as is done in piSeries3.cpp. unnecessaryNesting2.cpp is, most definitely, NOT an improvement over the multiplication in piSeries2.cpp.
In general, nested loops should be used only when necessary, or when they at least serve to make a program shorter or otherwise simpler than a single-loop version would be. Nested loops should NOT be used in situations where a single loop would do the job just as well. Unnecessary use of nested loops is bad programming style, making your program unnecessarily complicated and often unnecessarily slow as well.
Also, it is not ALWAYS desirable to eliminate multiplications. Sometimes, multiplication is necessary, even some circumstances where the operation needs to be repeated many times. Only with experience will you learn to recognize when multiplication is necessary and when it is not.
There are other ways to display right-justified columns besides use of the setw manipulator. Look now at neatColumns2.cpp, which behaves exactly like neatColumns1.cpp but does not use the setw manipulator. The program neatColumns2.cpp is more complicated. But, when you compile it, the resulting executable file takes a little less disk space, because it does not contain the library functions defined in header file <iomanip>.
Of the two displayed columns, the left column always displays a number which has either one or two digits. Hence, to determine how many leading spaces to put in front of a number in that column, we need only to check whether the the number has only one digit, i.e. whether the number is less than 10. If so, the program outputs one extra leading space:
cout << " ";
if ( exponent < 10 )
cout << " "; // leading space
cout << exponent;
The right column contains a number which may have anywhere from one to five digits. To determine how many leading spaces we need to put in front of a numbers, we must first count the digits in that number. The digits are counted as follows:
int digitCount = 1;
int powerOfTen = 10;
while ( powerOfTen <= powerOfTwo ) {
digitCount++;
powerOfTen = powerOfTen * 10;
} // while
Then, in order to display a column with total width 11, the number of leading spaces we will need is 11 minus the number of digits:
int spaces = 11 - digitCount;
Our program then displays that many leading spaces, as follows, before displaying the value of powerOfTwo:
for ( int i = 0; i < spaces; i++ )
cout << " ";
cout << powerOfTwo << endl;
Look again at neatColumns2.cpp. Note that the variable spaces isn't really necessary. We could replace the following statements in neatColumns2.cpp:
int spaces = 11 - digitCount;
for ( int i = 0; i < spaces; i++ )
cout << " ";
with the following, as is done in neatColumns3.cpp:
for ( int i = digitCount; i < 11; i++ )
cout << " ";
Make sure you understand why the above two snippets of code behave exactly the same.
Observe that the condition of the outer loop is true. Thus, the loop is infinite. An infinite loop can be stopped by pressing Ctrl-C.
Look now at echoFor.cpp, a program which behaves exactly like echoWhile.cpp, but uses for loops instead of while loops. Observe that the outer for loop has no condition between the semicolons within the heading, and thus has a default condition of true. The inner for loop does have a condition, but has no statement before the first semicolon.
Because the loops in this program not only are not count-controlled but also do not involve a loop count variable for any other purpose either, they are the kinds of loops that are customarily written as while loops, not for loops.
In your code traces, use letters to distinguish multiple iterations of the same line of code. For example, the following line numbers in your code trace would all correspond to line 32 of echoWhile.cpp:
| Line number in code trace |
Iteration of outer loop |
Iteration of inner loop |
|---|---|---|
You have seen a similar technique used with code traces for non-nested loops, in the Assignment 3 example code traces. Use the Assignment 3 example code traces as a model, but with nested loops.
Back to: