Unit 3: Iteration
(Credit to Chris Brown for the original version of these notes.)
In this unit we will learn about a new control structure in our programs. Loops allow our programs to re-use the same code over and over again, (hopefully) making progress each time through towards whatever the goal of our program is. This idea, more generally, is called iteration, and it’s an important way of getting the computer to do a lot of work so that we don’t have to!
1 The idea of a loop
1.1 Motivating example: checking user input
Consider a program that asks a Midshipman for their alpha number. We’d probably do this with code like the following:
int alpha;
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
However, if the user is inattentive or malicious, they might enter an invalid alpha number - which (for now) would mean anything less than 170000
or greater than 209999
. We might want to ensure that doesn’t happen, by extending our code to something like:
int alpha;
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
if (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
}
That’s better, but wait: Clearly a determined (or especially inattentive) user might still enter an invalid alpha the second time. So we might just add more and more and more of these checks, like:
int alpha;
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
if (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
if (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
if (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
if (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
}
}
}
}
Well this is clearly getting ridiculous. Fortunately, since every one of these if conditions and blocks are exactly the same, C provides us a way to do this perfectly with a single block, like follows:
int alpha;
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
while (alpha < 170000 || alpha >= 210000) {
fputs("Enter your alpha number: ", stdout);
alpha = readnum(stdin);
}
Essentially, the while
loop is just like an if
statement with no else
block, except after executing the body, instead of going on to the next part of the program, the control loops back to the condition and does the whole thing again. This keeps going until the condition returns 0 (meaning false).
Loops allow us to tell the computer to do something over and over again. This way, a short program can do a lot of work — which is the real power behind a computer!
1.2 Syntax of a while loop
The syntax of a while
-loop is much like the syntax of an if
-statement without an else block: We have while
followed by a test condition and a block of code. As long as the test condition is true (“while” the test condition is true), we keep executing the block of code (referred to as the body of the loop).
A typical task that we’ll implement with a loop is finding the sum of bunch of integers input by the user. We’ll assume that the user will enter a negative number to indicate that he’s done inputting data. (The negative number is just an end marker - it should not be included in the sum.) The typical way of doing this is:
int sum;
int k;
sum = 0;
k = readnum(stdin);
while (k >= 0) {
sum = sum + k;
k = readnum(stdin);
}
writenum(sum, stdout);
fputs("\n", stdout);
The section of code that sets sum = 0
and reads the first value for k
performs initialization, i.e. work before the loop that prepares you for the loop. The variable sum
must have the value zero before the loop begins; otherwise the whole concept is blown. The first value for k
needs to be read in from the user before we begin the loop, since that value will be checked in the loop’s test condition. Here’s the whole program.
1.3 Beware the infinite Loop!
At some point or another, we all do it. We write a program with an “infinite” loop. For example, you might accidentally write something like this:
int x = 0;
int y = 0;
while(x = 0 || x*y < 10) {
fputs("Enter x and y: ", stdout);
x = readnum(stdin);
y = readnum(stdin);
}
Can you spot the problem? It’s very tricky! I accidentally wrote x = 0
rather than x == 0
. The result is that because the ||-expression is evaluated left-to-right, x gets set to 0 in the first part of the OR, and that makes the second part true regardless of what the y
value is.
When you find yourself running a program in an infinite loop like this, the only remedy is to kill the program (for example, by hitting Ctrl-C).
2 While loop examples
Let’s look at a few more examples of using while
loops to solve a variety of problems. There will also be a few “detail” issues that’ll come up.
2.1 The Euclidean GCD Algorithm
The greatest common divisor (GCD) of two positive integers a
and b
is the largest integer that evenly divides both a
and b
. You may remember that some math teacher along the way told you that an answer like 3/6 is wrong, because it can be reduced to 1/2. How? By dividing out by the GCD of the numerator and the denominator - 3 in this case. So an algorithm that computes the GCD of two numbers is useful … at least for placating math teachers. It’s actually useful in an unimaginable number of other contexts, in things as diverse as encryption for online-banking to controlling the motions of robots.
The most common way to solve this problem today was was invented thousands of years ago. It is called the “Euclidean Algorithm” because it appears in Euclid’s book “The Elements”, which contained most of the mathematics known in the ancient Greek world.
The basic process (algorithm) for computing the GCD of a
and b
is this: Assuming a
is the larger of the two, compute the remainder when a
is divided by b
, and replace a
with b
and b
by the remainder. Keep doing this until you get a remainder of zero, at which point b
is the GCD. For example, if a
is 42 and b
is 30, then
a | b | remainder |
---|---|---|
42 | 30 | 12 |
30 | 12 | 6 |
12 | 6 | 0 |
… and therefore 6 is the GCD. We can do this with a loop, which will look something like this:
int a, b, r;
a = readnum(stdin);
b = readnum(stdin);
while(/*no idea what goes here yet!*/) {
r = a % b; // remember "%" is remainder
a = b;
b = r;
}
We have two questions now: When do we stop? And Where is the answer when we stop? We could try to be clever and figure it out abstractly, but that’s difficult. Let’s just see what happens with the code we do have in the example from above:
Before iteration 1: r = ?, a = 42, b = 30
Before iteration 2: r = 12, a = 30, b = 12
Before iteration 3: r = 6, a = 12, b = 6
Before iteration 4: r = 0, a = 6, b = 0 Stop! No 4th iteration!
As you can see, our clue to stop is that r
is zero (or, you could say that b
being zero is our clue), and you can see that the answer resides in the variable a
. So, we could complete our loop like this:
int a, b;
fputs("Enter two numbers: ", stdout);
a = readnum(stdin);
b = readnum(stdin);
while (b != 0) {
int r;
r = a % b;
a = b;
b = r;
}
writenum(a);
fputs(" is the GCD.\n", stdout);
2.2 Computing compound interest
Consider the problem of computing compound interest. The user enters an annual interest rate r
, and we want to figure out how much they’d have after 10 years at that rate with an investment of $100.00 in the account at the beginning of each year. The bulk of the program might look like the following.
double T;
T = 0.0; // Before year 1
T = (T + 100.0) * (1.0 + r/100.0); // After year 1
T = (T + 100.0) * (1.0 + r/100.0); // After year 2
T = (T + 100.0) * (1.0 + r/100.0); // After year 3
T = (T + 100.0) * (1.0 + r/100.0); // After year 4
T = (T + 100.0) * (1.0 + r/100.0); // After year 5
T = (T + 100.0) * (1.0 + r/100.0); // After year 6
T = (T + 100.0) * (1.0 + r/100.0); // After year 7
T = (T + 100.0) * (1.0 + r/100.0); // After year 8
T = (T + 100.0) * (1.0 + r/100.0); // After year 9
T = (T + 100.0) * (1.0 + r/100.0); // After year 10
Now, to do this right, we’d like the user to enter the annual interest rate r
and the number of years you’ll keep money in the account y
, and we’d like our program to figure out how much you’d have after y
years at that rate with an investment of $100.00 in the account at the beginning of each year.
Notice that, before we have loops, we can’t do this calculation. Without loops, we would need one line in our program for each year. If the user entered 100 years, we’d need to type in a 100+ line line program. To make the program work for any value of y
, you need a loop.
First, let’s try to rewrite the above block of code, in which the number of years is fixed at 10, using a loop. Fortunately, the above code breaks into a loop pretty easily:
double T;
T = 0.0; // Initialization for the loop
while (/* I need to loop 10 times! */) {
T = (T + 100.0) * (1.0 + r/100.0); // After year ?
}
We need to keep track of how many times we’ve gone through the loop body, and once we’ve gone through the loop body 10 times, we stop. Let’s introduce an int
variable years
to keep track of how many years have gone by. This of course will match up exactly with the number of iterations we have gone through the loop.
double T;
T = 0.0; // We don't have any money yet
int years;
years = 0; // No years have gone by yet
while (years < 10) // stop when 10 years have gone by
{
// update T and years for another year gone by
T = (T + 100.0) * (1.0 + r/100.0);
years = years + 1;
}
Now, what about making this work for any number of years? Here’s a complete solution.
2.3 A simple calculator
Let’s consider writing a program that reads from the user a simple arithmetic expression using + and -, like 3 + 4 - 7 + 2 - 4 =
, and prints out the result of the expression. Clearly you’re going to want a loop to solve this, but how? We’ll go through the steps to one possible solution, slowly, to illustrate the problem-solving process.
Our program needs to read in these numbers and +/- signs and add or subtract them to get a final answer. What variables will we need to do that? We should have variables for each number we read in and each operator too, which will have type int
and char
respectively; let’s call then nextnum
and nextop
. And then we also want to store the running total
for the final answer, which has to start at zero. This gives us:
int total;
int nextnum;
char nextop;
total = 0;
Keep in mind, we can always add variables later or move these around, but this is a good start.
The next question to think of is how the program should work — what needs to be done inside our loop? It makes sense to read and incorporate one more number in the loop each time through, which means we’ll also have to read one more operation each time through the loop.
Putting this together, we start to see the beginnings of a program:
int total;
int nextnum;
char nextop;
total = 0;
while (/* don't know what goes here yet! */) {
nextnum = readnum(stdin); // this goes SOMEWHERE in the loop
nextop = readchar(stdin); // this goes SOMEWHERE in the loop
// update total by adding or subtracting nextnum
}
Now we start to fill it in. Keep in mind, the order of the statements inside the loop might be wrong. We have to figure that out as we go along too! First let’s see what the code for updating the total is. You’re going to want to either add or subtract, and which one is determined by whether the operator is +
or -
. So we can replace the // upadate total
comment with:
if (nextop == '+') {
total = total + nextnum;
} else {
total = total - nextnum;
}
What about the while
condition? What tells our program that the loop is over? Looking at a sample input like 4 + 2 - 3 + 7 =
, you see that the very last “operator” is actually an =
sign, which indicates that the loop should terminate. Adding that to our program, here’s what we have so far:
int total;
int nextnum;
char nextop;
total = 0;
while (nextop != '=') {
nextnum = readnum(stdin); // this goes SOMEWHERE in the loop
nextop = readchar(stdin); // this goes SOMEWHERE in the loop
// this goes SOMWHERE in the loop
if (nextop == '+') {
total = total + nextnum;
} else {
total = total - nextnum;
}
}
Great! Now all the pieces are in place, but the program is not quite right. You could try running it on an example such as 3 - 2 + 8 =
, and you would see that the result is -9
, not 9
like it should be. Try tracing through the program execution by hand. What do you notice?
There are two problems, actually. The first issue is that nextop
is not initialized before the while
loop begins, so the first time it is checked whether it is a =
sign, we can’t say for sure what will happen! So we should initialize nextop
with some character to start with. When you think about it, the first number is always added, so it makes sense to initialize nextop
with +
at the beginning.
The second issue is that each operation really applies to the next number to be added or subtracted, but from the way the loop is written, it’s applied to the previous number. The fix here is easy: just change the order so that reading the next operation happens after the if
statement that does the adding or subtracting.
Putting this together, here’s what we get:
int total;
int nextnum;
char nextop;
total = 0;
nextop = '+';
while (nextop != '=') {
nextnum = readnum(stdin);
if (nextop == '+') {
total = total + nextnum;
} else {
total = total - nextnum;
}
nextop = readchar(stdin);
}
3 Loop shortcuts
Here we’ll look at two important shortcuts that make it easier to write certain kinds of loops. As with our other shortcuts, these aren’t technically needed, but they make our code much easier to write and understand, and that’s pretty important!
3.1 Shortcut #1: for loops
One of the most common looping situations you’ll come across is this: You’ve got a number n
, and you need to go through a loop n
times. Consider, for example, the very simple problem of writing a program that reads in double
x
and int
n
from the user and prints out x
to the n
th power. (Forget, for the moment, that cmath
contains a function to do this.) Here’s how we’d do it with while-loops:
double power;
int i;
power = 1.0;
i = 0;
while (i < n) {
power = power * x;
i = i + 1;
}
If you think the test “i < n
” looks strange, remember this: the variable i
counts the number of times we’ve gone through the loop body. We start having gone through 0 times, and we want to stop when we’ve gone through n
times. In other words, while we’ve looped fewer than n
times, keep looping!
Any time we count our way through a loop like this, we’ll have three basic pieces of code:
- the initialization of our loop counter before the loop begins
- the test-condition that determines whether we keep looping
- the update (i.e. increment) of our loop counter at the end of each loop iteration
This set-up is so common that C has a special syntax allowing us to express it more succinctly, the for
loop. The previous code would be written as follows with a for loop:
double power;
int i;
power = 1.0;
i = 0;
while (i < n) {
power = power * x;
i = i + 1;
}
double power;
int i;
power = 1.0;
for (i = 0; i < n; i = i + 1) {
power = power * x;
i = i + 1;
}
The for
-loop is just a compact way of writing a while loop which has the three steps outlined above: initialization, test-condition, and update. A for
-loop can always be written as a while loop in the following way:
for (A; B; C) {
statement1;
statement2;
...
statenemtk;
}
{
A;
while (B) {
statement1;
statement2;
...
statementk;
C;
}
}
As I said before, we usually use for-loops to loop n
times, where n
is some value we know from earlier on in the program. This for
-statement will usually look like this:
for (int i=0; i < n; ++i) {
CODE
}
Making sense of this brings up 2 more C shortcuts that we’ve ignored until now.
3.2 Shortcut #2: Initializing variables in the declaration
So far, we’ve usually made declaring a variable and initializing that variable (i.e. giving the variable it’s initial value in the program) separate steps. However, as we learned in the last unit, there’s a shortcut you can use to declare and initialize on a single line of code, like so:
double x = 1.0;
And you can even declare several variables in one statement, initializing some of them and not others, like:
int a, b=3, c;
In the context of a for
loop this is nice, because something like the counter i
in
for(int i=0; i < n; i = i+1) {
CODE
}
really only gets introduced for the for
-loop. Thus, it’s nice to be able to declare it and initialize it all in one statement, so it fits in the first slot of the for
-statement.
This shortcut also brings up something important about scope and for loops. If you look back at the correspondence between for-loops and while-lops, you’ll see that the corresponding while loop is encased in a block of its own. The point is that any variable you declare in the first slot of the for
-statement goes out of scope after you leave the for-loop. Thus, something like:
for(int i=0; i < 12; ++i) {
writenum(i, stdout);
fputs("\n", stdout);
}
writenum(i, stdout);
fputs("\n", stdout);
shouldn’t compile - after all, the i
doesn’t exist as far as that second writenum
is concerned. This is usually a nice feature. After all, you only introduced the new variable i
to iterate through the numbers 0 up to 11. After you’re done with that iteration, there’s no point to having i
around.
Here’s a program that illustrates how that can be nice.
#include "si204.h"
int main() {
for (int i=0; i<25; ++i) {
fputc('*', stdout);
}
fputs(" E L E P H A N T S N E E D S P A C E ", stdout);
for (int i=0; i<25; ++i) {
fputc('*', stdout);
}
fputs("\n", stdout);
}
See how we didn’t need to use a different variable for the two for-loops? That’s because their scope does not extend beyond the for-loop itself.
3.3 Shortcut #3: ++ and –
Incrementing (adding 1) and decrementing (subtracting 1) variables is something that goes on all the time in programming. Therefore, C has a shortcut: ++x
sets x
to x+1
, and --x
sets x
to x-1
.
Note 1: These are called “pre-increment” or “pre-decrement” operators because the ++
comes before the name of the variable you’re changing. You can also do something like x++
(the post-increment operator), which also sets x
to x+1
but with a different return value. The pre-increment version returns the value after the increment, whereas the post-increment version returns the original value. This is best illustrated with an example:
int x = 10;
writenum(x, stdout); // prints 10, original value
writenum(++x, stdout); // prints 11, updated value
writenum(x, stdout); // prints 11, the new value for x
int y = 10;
writenum(y, stdout); // prints 10, original value
writenum(++y, stdout); // prints 10, still the original value!
writenum(y, stdout); // prints 11, the new value for y
See the difference on the second line? But usually we won’t rely on the return value anyway - if you’re just wanting to increase the value of x
by one, ++x
and x++
both do the job.
Note 2: If you want to change the value by more than +/-1, there are more general operators that do that for you:
a += b |
is equivalent to | a = a + b |
a -= b |
is equivalent to | a = a - b |
So ++a
is equivalent to a += 1
, which is equivalent to a = a + 1
. These all do exactly the same thing; which version you choose to use in your program should be based on what makes it most clear.
For the moment, let’s just say this: Don’t use ++ and – in expressions, just use them as standalone statements that provide a shortcut to something like x = x + 1
. This is because you have to be careful about which value you get in the expression - is it the value before or after the increment. It’s confusing to read such things, and let’s just avoid the confusion. Remember, it’s not a contest to see who can write a program with the fewest keystrokes! (Though admittedly, that’s kind of fun.)
4 Odds & Ends: Reading files and exiting early
The topics in this section don’t really have anything to do with loops necessarily, but they’re important to understand so you can get to work on your first programming project. And more generally, these things are useful when you start to write bigger or more sophisticated programs — which is what loops allow you to do!
4.1 Exiting main() early
The “return 0;
” at the end of your program exits your program with an code of 0
to indicate to the operating system that “everything worked fine”. In fact, inside main
you can stick a return statement like this wherever you want, and returning a value other than 0 (it should be a small positive number such as 1) is a good way to indicate that some error has occurred in your program.
For example, maybe you want to write a program that reads an integer k
from the user and writes out 1/k
. If the user enters zero, of course, there’s a problem. Now we’ll just castigate the user and exit the program if he does that!
#include "si204.h"
int main() {
// Get number from user
int k;
fputs("Enter a non-zero integer: ", stdout);
k = readnum(stdin);
// Deal with bad input
if (k == 0) {
fputs("ERROR: can't divide by zero\n", stdout);
return 1;
}
// Write out decimal approximation of 1/k
fputs("1/", stdout);
writenum(k, stdout);
fputs(" is approximately ", stdout);
writenum(1.0 / k, stdout);
fputs("\n", stdout);
return 0;
}
An alternative to return 1
is to call exit(1)
, which does exactly the same thing, except that you can call exit
from contexts other than inside the main block, as we will see in a few weeks. Here is the complete documentation on the exit
function.
4.2 Reading from files
Up to this point, we have to specify either stdout
or stdin
in every input or output command that we write. These are objects of type stream
that refer to (respectively) the “standard output” to the terminal, or the “standard input” from the terminal.
You may have guessed that there are more streams other than stdin
and stdout
— and you would be right! We’ll learn much more about this in the next unit, but for now let’s just say how you open a stream to read from a file.
The command to create a stream based on a filename is fopen
. This function takes two arguments, both of which should be cstring
s. The first argument is the name of the file to open, and the second argument is the “mode”, which for reading a file should be the string literal "r"
for read.
The fopen
command returns a stream
object, which you can then use anywhere you would otherwise use stdin
. For example, to open a file called nums.txt
and read the first two integers in that file, you could do:
stream filein = fopen("nums.txt", "r");
int x, y;
x = readnum(filein);
y = readnum(filein);
fclose(filein);
fputs("The first two numbers in nums.txt are ", stdout);
writenum(x, stdout);
fputs(" and ", stdout);
writenum(y, stdout);
fputs(".\n", stdout);
Notice that we should use the fclose
function to close the file safely whenever you’re finished reading from it.
The filename you give to fopen
can really be any string, not just one that’s “hard-coded” into your program like in the example above. But if you read the string from what the user types in, there’s a chance that they will mistype the filename, and then you try to open a file that doesn’t really exist.
If that happens, fopen
does not print any error message or anything else. Instead, it returns a special stream
value that (when cast as an int
) is equal to 0
. Why equal to zero? Because that’s the value in C that indicates something is “false”. Putting this together with what you just read about exiting main()
early, here’s how to write a program that reads the first string in a file, or prints an error message if that file doesn’t exist.
#include "si204.h"
int main() {
cstring filename;
fputs("Enter the name of the file: ", stdout);
readstring(filename, stdin);
// try to open the file
stream filein = fopen(filename, "r");
// check if there was an error in opening the file
if (filein == 0) {
fputs("ERROR: file could not be opened.\n", stdout);
return 1;
}
// if we get here, the file must have opened okay.
cstring firstword;
readstring(firstword, filein);
// close the file now
fclose(filein);
// report the first word
fputs("The first word in the file is ", stdout);
fputs(firstword, stdout);
fputs(".\n", stdout);
return 0;
}
5 Nested Loops & Stepwise Refinement
5.1 Nested Loops
The body of a loop is a block of code like any other - like the body of “main”, or like a then-block or like an else-block. So, that means that it can contain any of the C constructs we’ve talked about so far … including more loops. Loops inside loops are referred to as “nested”, and you’ll see a lot of them. Perhaps the simplest program with a nested loop is one that prints out a square of *’s to the screen. Here’s an example:
#include "si204.h"
int main() {
// Get n from the user
int n;
fputs("Please enter n: ", stdout);
n = readnum(stdin);
// print the square
// the outer loop does each row
for (int row=0; row < n; ++row) {
// the inner loop goes along each column
for (int col=0; col < n; ++col) {
fputs("* ", stdout);
}
// newline moves to the next row
fputs("\n", stdout);
}
return 0;
}
See how it involves a loop nested within a loop? Notice too, though, that each loop has a well-defined meaning. The outer loop body prints a single row, the inner loop body prints a single * within that row.
One of the best ways to deal with the complexities of writing programs using nested loops is to follow the design paradigm of stepwise refinement, which basically has you writing programs in rough steps which are then continually refined until they end up as actual C code. Let’s consider an example to see this in action.
5.2 Printing out a Hankel matrix
The Hankel matrix defined by the numbers 1, 2, …, 2n-1 is
1 | 2 | 3 | … | n |
2 | 3 | 4 | … | n+1 |
… | ||||
n | n+1 | n+2 | … | 2n-1 |
I want you to write a program that will read in a value n from the user, you may assume that n is less than 50, and print out the Hankel matrix defined by 1, 2, …, 2n-1 on the screen. When your program runs, it should look something like this:
Enter value n, where n < 50: 20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Think about what you’re going to have to do in order to ensure that the columns line up correctly, given that you have single and double digit numbers!
The key to solving this problem effortlessly is to gradually develop a solution. If you click the forward and backward arrows below, you’ll see one way of following stepwise refinement for the Hankel matrix problem.
#include "si204.h"
int main() {
// read in the parameter n
// write Hankel matrix 1,...,2*n
return 0;
}
#include "si204.h"
int main() {
// read in the parameter n
// write Hankel matrix 1,...,2*n
for (int row=1; row <= n; ++row) {
// write row entries row, ..., row + n
}
return 0;
}
#include "si204.h"
int main() {
// read in the parameter n
// write Hankel matrix 1,...,2*n
for (int row=1; row <= n; ++row) {
// write row entries row, ..., row + n
for (int col=1; col <= n; ++col) {
// write entry (row,col) in two spaces
}
}
return 0;
}
#include "si204.h"
int main() {
// read in the parameter n
// write Hankel matrix 1,...,2*n
for (int row=1; row <= n; ++row) {
// write row entries row, ..., row + n
for (int col=1; col <= n; ++col) {
// write entry (row,col) in two spaces
int val = row + (col - 1);
if (val < 10) {
fputs(" ", stdout);
}
writenum(val, stdout);
fputs(" ", stdout);
}
fputs("\n", stdout);
}
return 0;
}
#include "si204.h"
int main() {
// read in the parameter n
int n;
fputs("Enter value n, where n < 50: ", stdout);
n = readint(stdin);
// write Hankel matrix 1,...,2*n
for (int row=1; row <= n; ++row) {
// write row entries row, ..., row + n
for (int col=1; col <= n; ++col) {
// write entry (row,col) in two spaces
int val = row + (col - 1);
if (val < 10) {
fputs(" ", stdout);
}
writenum(val, stdout);
fputs(" ", stdout);
}
fputs("\n", stdout);
}
return 0;
}
This is what step-wise refinement is about. We’ve been doing it all along, in a sense, but it’s much more interesting when you have nested structures like loops and if statements, as opposed to a simple sequence of flat steps.
5.3 Polynomial Evaluation
Let’s consider another problem: I want to write a program that will do polynomial evaluation. That is, it will read in a value for x and a polynomial in x and print out the result of evaluating the polynomial at the given value. To make it a bit easier on ourselves, let’s assume that each term is given as a coefficient times x raised to some power. Thus, we would write 1.1*x^2 - 3.0*x^1 + 2.1*x^0
for the polynomial \(1.1x^2 - 3.0x + 2.1\). The user will indicate that she’s done entering the polynomial by typing an equals sign. A typical run of the program might look like this:
Enter value for x:
3.1
Enter polynomial to be evaluated:
0.75*x^5 - 1.8*x^3 + 0.3*x^2 + 1.1*x^1 =
Result is 167.388
In essence this problem is not much different from the simple calculator problem from before. The main difference is that instead of sums and differences of simple numbers, we have sums and differences of terms in a polynomial. Once again, a nice way to attack this problem is by stepwise refinement, where we gradually develop a solution by specifying in deeper and deeper detail how we’ll solve the problem, until our detail is so great we actually have a complete C program. Clicking through the arrows below will show you one way of solving this one through stepwise refinement.
#include "si204.h"
int main() {
// read in x value
// read and evaluate polynomial
// write result
return 0;
}
#include "si204.h"
int main() {
// read in x value
/********************************/
/* read and evaluate polynomial */
/********************************/
while (/* ??? */) {
// read next term c*x^n
// evaluate next term c*x^n
// update value of total
}
// write result
return 0;
}
#include "si204.h"
int main() {
// read in x value
/********************************/
/* read and evaluate polynomial */
/********************************/
while (/* ??? */) {
// read next term c*x^n
/****************************/
/* evaluate next term c*x^n */
/****************************/
// compute x^n
// multiply by c
// update value of total
}
// write result
return 0;
}
#include "si204.h"
int main() {
// read in x value
double x;
fputs("Enter value for x: ", stdout);
x = readnum(stdin);
/********************************/
/* read and evaluate polynomial */
/********************************/
while (/* ??? */) {
// read next term c*x^n
/****************************/
/* evaluate next term c*x^n */
/****************************/
// compute x^n
double xn = 1.0;
for (int i=0; i < n; ++i) {
xn = xn * x;
}
// multiply by c
double term = xn * c;
// update value of total
if (op == '+') {
total = total + term;
} else {
total = total - term;
}
}
// write result
fputs("Result is ", stdout);
writenum(total, stdout);
fputs("\n", stdout);
return 0;
}
#include "si204.h"
int main() {
// read in x value
double x;
fputs("Enter value for x: ", stdout);
x = readnum(stdin);
/********************************/
/* read and evaluate polynomial */
/********************************/
fputs("Enter polynomial to be evaluated:\n", stdout);
while (op != '=') {
// read next term c*x^n
double c;
int n;
c = readnum(stdin);
readchar(stdin); // '*'
readchar(stdin); // 'x'
readchar(stdin); // '^'
n = readnum(stdin);
/****************************/
/* evaluate next term c*x^n */
/****************************/
// compute x^n
double xn = 1.0;
for (int i=0; i < n; ++i) {
xn = xn * x;
}
// multiply by c
double term = xn * c;
// update value of total
if (op == '+') {
total = total + term;
} else {
total = total - term;
}
}
// write result
fputs("Result is ", stdout);
writenum(total, stdout);
fputs("\n", stdout);
return 0;
}
#include "si204.h"
int main() {
// read in x value
double x;
fputs("Enter value for x: ", stdout);
x = readnum(stdin);
/********************************/
/* read and evaluate polynomial */
/********************************/
fputs("Enter polynomial to be evaluated:\n", stdout);
// initialization of for loop
char op = '+';
double total = 0.0;
while (op != '=') {
// read next term c*x^n
double c;
int n;
c = readnum(stdin);
readchar(stdin); // '*'
readchar(stdin); // 'x'
readchar(stdin); // '^'
n = readnum(stdin);
/****************************/
/* evaluate next term c*x^n */
/****************************/
// compute x^n
double xn = 1.0;
for (int i=0; i < n; ++i) {
xn = xn * x;
}
// multiply by c
double term = xn * c;
// update value of total
if (op == '+') {
total = total + term;
} else {
total = total - term;
}
// read next op
op = readchar(stdin);
}
// write result
fputs("Result is ", stdout);
writenum(total, stdout);
fputs("\n", stdout);
return 0;
}
5.4 Stepwise refinement from the inside out
When you’re writing a larger program, it’s nice to be able to compile and run something during the intermediate stages, just to make sure what you’re doing is all right. One way to do this is to continually refine your program so that it solves more and more of your initial problem. In other words, you start by solving an easier problem. Then you add a little more of the complexity of the original problem and you refine your program to a working program for this slightly less easy problem that’s closer to the one you really want to solve. This way, you gradually work towards a solution to the full problem.
Consider the following problem: I want to figure out which state I should live in. The determining factor is the population density: some people like to live near other people in more urban settings perhaps, and others like to be spread further apart. So (ignoring the fact that every state has a mixture of urban and rural spaces) we want to determine the state with the closest population density to some target number.
We’ll use the data on this website which shows the population data, for each of the 50 states plus Washington, D.C. and Puerto Rico, for the last hundred years. I copied and pasted the table on that page into a text document that you can download here.
The program will work by reading in the desired population density, and then displaying the state with the closest density to that desired target. A final run should look something like this:
Desired population density (people per sq mile):
500
You should live in Delaware with 460.8 people per square mile.
This is a challenging problem for a number of reasons! We have to think about how the program will work on a logical level, but also get all the details right on how to read in, process, and store the data in the file format we have.
So, the question is this: how do I go about designing a program that solves a non-trivial problem like this? Let’s start with the basic structure:
int main() {
// Get target density from the user
// Open data file pop.txt
// Read in (and ignore!) the beginning with the header line
// and overall USA population data
// Loop over each state
{
// Process that state's info
// Determine if each state is closest
}
// Close the data file
// print out the name of the closest state
return 0;
}
The goal now is to get to some working program as quickly as possible, even though our initial version definitely won’t solve the full problem. For starters, let’s just get the initial reading-in and writing-out parts. This also forces us to think about what variables we need at the outer scope, which is a good thing! Here’s the initial, very incomplete, program:
#include "si204.h"
int main() {
// Get target density from the user
double target;
fputs("Desired population density (people per sq mile): ", stdout);
target = readnum(stdin);
// variables for the best state and its density.
cstring best_state;
double best_density;
strcpy(best_state, "ERROR");
best_density = 0.0;
// Open data file pop.txt
// Read in (and ignore!) the beginning with the header line
// and overall USA population data
// Loop over each state
{
// Process that state's info
// Determine if each state is closest
}
// Close the data file
// print out the name of the closest state
fputs("You should live in ", stdout);
fputs(best_state, stdout);
fputs(" with ", stdout);
writenum(best_density, stdout);
fputs(" people per square mile.\n", stdout);
return 0;
}
Now we should start to read in the data. But that’s tough, so let’s make it easier! To start with, let’s pretend that there’s no header information, and the file only contains data for a single state. So we make a file pop1.txt
that looks like this:
Alaska
Population 64,356 55,036 59,278 72,524 128,643 226,167 300,382 401,851 550,043 626,932 710,231
People per sq. mile 0.1 0.1 0.1 0.1 0.2 0.4 0.5 0.7 1.0 1.1 1.2
Density Rank 52 52 52 52 52 52 52 52 52 52 52
There are a bunch of numbers here, but we really only care about two things: the name of the state on the first line, and the last number on the third line which gives the population density in 2010. We can use a cstring
variable to read and discard all of the surrounding info.
Here’s the working — but still very incomplete! — program that results:
#include "si204.h"
int main() {
// Get target density from the user
double target;
fputs("Desired population density (people per sq mile): ", stdout);
target = readnum(stdin);
// variables for the best state and its density.
cstring best_state;
double best_density;
strcpy(best_state, "ERROR");
best_density = 0.0;
// Open data file pop.txt
stream popin = fopen("pop1.txt", "r");
// Read in (and ignore!) the beginning with the header line
// and overall USA population data
// Loop over each state
{
/*****************************/
/* Process that state's info */
/*****************************/
cstring state;
double density;
cstring garbage; // this will be used to read over unwanted data
readstring(state, popin);
// second line: 1 word and 11 values
for (int i=0; i<12; ++i) {
readstring(garbage, popin);
}
// third line: 4 words, 10 unwanted values, then the density
for (int i=0; i<14; ++i) {
readstring(garbage, popin);
}
density = readnum(popin);
// fourth line: 2 words, 11 values
for (int i=0; i<13; ++i) {
readstring(garbage, popin);
}
// Determine if each state is closest
// (in this version, just copy this state's info as the "best")
strcpy(best_state, state);
best_density = density;
}
// Close the data file
fclose(popin);
// print out the name of the closest state
fputs("You should live in ", stdout);
fputs(best_state, stdout);
fputs(" with ", stdout);
writenum(best_density, stdout);
fputs(" people per square mile.\n", stdout);
return 0;
}
This is progress! Of course, the program keeps saying we should live in Alaska… Do you see what problem is going to crop up when we try this with other states? The program is assuming that the state’s name is only a single word, so something like this:
District of Columbia
Population 331,069 437,571 486,869 663,091 802,178 763,956 756,510 638,333 606,900 572,059 601,723
People per sq. mile 5423.1 7167.6 7975.1 10861.7 13140.0 12513.9 12392.0 10456.2 9941.3 9370.6 9856.5
Density Rank 1 1 1 1 1 1 1 1 1 1 1
won’t work. So we refine our program to make it handle multiple-word state names, by making use of the strcat
function which concatenates one string onto the end of another, reading in more parts of the state’s name until we see the word “Population”. This is tricky to be sure, but the important thing is that we are able to focus on just this one aspect and get it fully working and tested before moving on. Imagine how much more difficult it would be to tackle this hurldle after writing the entire program!
Here’s what that part of the loop looks like when we fix it to read in any single state:
/*****************************/
/* Process that state's info */
/*****************************/
cstring state;
double density;
cstring garbage; // this will be used to read over unwanted data
readstring(state, popin);
// add to state name until we see the word "Population"
readstring(garbage, popin);
while (strcmp(garbage, "Population") != 0) {
strcat(state, " ");
strcat(state, garbage);
readstring(garbage, popin);
}
// second line: 11 values
for (int i=0; i<11; ++i) {
readstring(garbage, popin);
}
Now we’re ready to read in the actual complete data file. But let’s not try to tackle too many things at once! For the next version, we’ll read in the entire file, but ignore all of the proper logic of which state is selected. (So the program will always return the last entry in the file, which is Puerto Rico.) Again, this isn’t the program we want, but it’s making progress towards the goal, and we have another complete, working program. All that really changes is the part to ignore the header lines, plus the for loop itself:
// Open data file pop.txt
stream popin = fopen("pop.txt", "r");
// Read in (and ignore!) the beginning with the header line
// and overall USA population data
{ // start a block so we can declare variables locally!
cstring garbage; // this will be used to read over unwanted data
// count how many words need to be ignored in the header
int initial_ignore = (3+11) + 2 + (1+11) + (4+11);
for (int i=0; i < initial_ignore; ++i) {
readstring(garbage, popin);
}
}
// Loop over each state
// there are 50 states + DC and PR
for (int statenum=0; statenum < 52; ++statenum) {
/*****************************/
/* Process that state's info */
/*****************************/
Now we’re almost done! All that’s left is to get the logic right so that the state with the closest population density to our target
is returned at the end. To do this, we just need to compare how close each state is to the target, to how close the best_state
is to the target. That means changing these lines at the end of the for loop:
/**************************************/
/* Determine if this state is closest */
/**************************************/
double distance;
if (density < target) {
distance = target - density;
} else {
distance = density - target;
}
if (distance < best_distance) {
// update the best state to this state
strcpy(best_state, state);
best_density = density;
best_distance = distance;
}
You can see the complete final program here. This still wasn’t easy! Even the most careful stepwise refinement technique will still run into challenging coding problems. The main benefit here is that we can test and debug as we go along, so that we’re never solving two problems at once. If you tried to write this program all at once, you might never actually finish it!
6 Do-while loops (optional)
This section is optional reading if you want to learn more about C programming. You won’t be required to understand do/while loops or to use them in your code. But if you read a lot of C code, you will probably come across
do
sooner or later, so you might want to know about it. (You are also free to use do/while yourself if you think there’s a good reason to do so.)
We’ve spent a lot of time with while-loops and for-loops. Both of these loops test whether to continue, and then go through the loop body, i.e. the test is done at the beginning of the loop. A “do-while” loop allows you to put the test at the end of the loop. This is convenient for certain tasks. For example, we looked at a program that reads expressions like
1 + 5 + 3 + 48 + 32 =
and prints out the resulting sum. This can be done nicely with a do-while
#include "si204.h"
int main() {
int sum;
int next;
char op;
sum = 0;
do {
next = readnum(stdin);
sum = sum + next;
op = readchar(stdin);
} while (op != '=');
writenum(sum, stdout);
fputs("\n", stdout);
return 0;
}
#include "si204.h"
int main() {
int sum;
int next;
char op;
sum = 0;
// op has to be initialized!
// otherwise the loop might
// not be entered the 1st time
op = '+';
while (op != '=') {
next = readnum(stdin);
sum = sum + next;
op = readchar(stdin);
}
writenum(sum, stdout);
fputs("\n", stdout);
return 0;
}
In case you don’t find that compelling, here’s another example. Suppose we want to keep reading in int
s until we read a negative number.
int n;
do {
n = readnum(stdin);
} while (n >= 0);
int n;
// read the first time here
n = readnum(stdin);
while (n >= 0) {
// read the rest of the time
n = readnum(stdin);
}
7 Practice Problems and Questions
7.1 While Loops
- Computing Averages: this just builds on the sum example from above.
- Real Ceasar-Shift Encryption: take a look at this problem from Unit 2 for a description of Cesar Shift Encryption. Now we’ll do it for messages of any length!
- Keeping track of the maximum
- Write a program that allows the user to enter a sequence of “moves” and prints out their position after the moves. There are many ways you could do this, but here is one solution.
- Finding the largest file in a directory listing.
Here is a sample listing of one of my directories. The number right before the date on each line is the number of bytes in that file. Your challenge is to read in a directory listing like that one and print the name and size of the largest file.
- Volleyball scoring. You read in the names of two team names and who wins each rally, and report when one team wins the game. There are three versions, with increasing complexity of the programs:
- Financial savings calculator: Figures out how many years and months it would take you to save up to a specific amount of money, given your starting balance, monthly contribution, and interest rate. This is a good example of proper output formatting as well as the use of a while loop.
7.2 For Loops
- Write n stars: This is about as simple as it gets!
- Integration by end-point approximation: Simple end-point integration approximates the area under a curve between
x = a
and x = b
by the sum of n
evenly spaced rectangles whose hieghts are the function values at x
-values given by the left endpoints of the bases. To remind yourself of how this works check out this picture.
Read in a number n and print out the value of \[\sum_{k=0}^n {n \choose k}\] i.e. the sum as k goes from 0 to n of n choose k.
The formula for n choose k is \[\frac{n\cdot(n-1)\cdot(n-2)\cdots(n-k+1)}{k\cdot(k-1)\cdot(k-2)\cdots 1}\]
So, for example 7 choose 3 is (7*6*5)/(3*2*1) = 35.
Notice that this number will always be an integer! So do all of your computations using type int
, and don’t do any division unless you’re sure you’ll get exact results, i.e. no remainder. Take a look at this solution to the problem.
7.3 File input and error conditions
- Adding and subtracting numbers in a file. You’ll want to test using the file in4.txt.
Suppose a file called grades.txt
contains an integer for the number of students, and then one line for each student. Each line has their name (with multiple parts), their alpha code, and then a list of grades ended by a semicolon. For example:
3
John Jeffery Jones m040101 85, 95, 22, 54;
Sandra Smith m040201 99, 98;
Xavier Xenon Junior m050400 78, 80, 82, 84, 86, 88, 90;
Your program should read this information in, and print out the name of the student with the highest average for the given grades. For the input above, your program would print out
Sandra Smith had the highest average.
A good way to approach this problem would be to write three successive programs:
A program that reads in students whose name is just one word, and whose list of grades always consists of a single score. For example, input for this might look like:
3
Jones m040101 85
Smith m040201 99
Xenon m050400 78
A program that reads in students whose names may consist of multiple words, but whose list of grades always consists of a single score. For example, input for this might look like:
3
John Jeffery Jones m040101 85
Sandra Smith m040201 99
Xavier Xenon m050400 78
Hint: Use the strcat
function as seen on the C functions reference page.
A program to solve the full problem.
Here is a sample listing of one of my directories. The number right before the date on each line is the number of bytes in that file. Your challenge is to read in a directory listing like that one and print the name and size of the largest file.
- Write n stars: This is about as simple as it gets!
- Integration by end-point approximation: Simple end-point integration approximates the area under a curve between
x = a
andx = b
by the sum ofn
evenly spaced rectangles whose hieghts are the function values atx
-values given by the left endpoints of the bases. To remind yourself of how this works check out this picture. Read in a number n and print out the value of \[\sum_{k=0}^n {n \choose k}\] i.e. the sum as k goes from 0 to n of n choose k.
The formula for n choose k is \[\frac{n\cdot(n-1)\cdot(n-2)\cdots(n-k+1)}{k\cdot(k-1)\cdot(k-2)\cdots 1}\]
So, for example 7 choose 3 is (7*6*5)/(3*2*1) = 35.
Notice that this number will always be an integer! So do all of your computations using type
int
, and don’t do any division unless you’re sure you’ll get exact results, i.e. no remainder. Take a look at this solution to the problem.
7.3 File input and error conditions
- Adding and subtracting numbers in a file. You’ll want to test using the file in4.txt.
Suppose a file called grades.txt
contains an integer for the number of students, and then one line for each student. Each line has their name (with multiple parts), their alpha code, and then a list of grades ended by a semicolon. For example:
3
John Jeffery Jones m040101 85, 95, 22, 54;
Sandra Smith m040201 99, 98;
Xavier Xenon Junior m050400 78, 80, 82, 84, 86, 88, 90;
Your program should read this information in, and print out the name of the student with the highest average for the given grades. For the input above, your program would print out
Sandra Smith had the highest average.
A good way to approach this problem would be to write three successive programs:
A program that reads in students whose name is just one word, and whose list of grades always consists of a single score. For example, input for this might look like:
3
Jones m040101 85
Smith m040201 99
Xenon m050400 78
A program that reads in students whose names may consist of multiple words, but whose list of grades always consists of a single score. For example, input for this might look like:
3
John Jeffery Jones m040101 85
Sandra Smith m040201 99
Xavier Xenon m050400 78
Hint: Use the strcat
function as seen on the C functions reference page.
A program to solve the full problem.
Suppose a file called grades.txt
contains an integer for the number of students, and then one line for each student. Each line has their name (with multiple parts), their alpha code, and then a list of grades ended by a semicolon. For example:
3
John Jeffery Jones m040101 85, 95, 22, 54;
Sandra Smith m040201 99, 98;
Xavier Xenon Junior m050400 78, 80, 82, 84, 86, 88, 90;
Your program should read this information in, and print out the name of the student with the highest average for the given grades. For the input above, your program would print out
Sandra Smith had the highest average.
A good way to approach this problem would be to write three successive programs:
A program that reads in students whose name is just one word, and whose list of grades always consists of a single score. For example, input for this might look like:
3 Jones m040101 85 Smith m040201 99 Xenon m050400 78
A program that reads in students whose names may consist of multiple words, but whose list of grades always consists of a single score. For example, input for this might look like:
3 John Jeffery Jones m040101 85 Sandra Smith m040201 99 Xavier Xenon m050400 78
Hint: Use the
strcat
function as seen on the C functions reference page.A program to solve the full problem.