Loops Inside Loops


Nested loops

If statements and while-loops can have any kind of statement inside them, including another If-statement or while-loop. That is an example of a substitution rule. There are no special forms of statement. Where one kind of statement can occur, another kind could also occur.

Here is an example of a loop inside a loop.

  Displayln "i  j".
  Let i = 1.
  While i <= 4 do
    Let j = 1.
    While j <= 3 do
      Displayln $(i) ++ "  " ++ $(j).
      Relet j = j + 1.
    %While
    Relet i = i + 1.
  %While

When the above sequence of statements is run, it displays

i  j
1  1
1  2
1  3
2  1
2  2
2  3
3  1
3  2
3  3
4  1
4  2
4  3
The outer loop has control variable i, which counts from 1 to 4. The inner loop, with control variable j, counts from 1 to 3 for each value of i.

A loop inside a loop is called a nested loop.


Choosing control variables

Each loop has one or more control variables. If you write one loop inside another, it is very important that those two loops use different control variables, so that they do not interfere with each other. In the example, above, the outer loop has control variable i and the inner loop has control variable j.


Initializing for the inner loop

Generally, you initialize for a loop just before the loop starts. A common programming mistake with nested loops is to initialize for the inner loop in the wrong place, in front of the outer loop instead of in front of the inner loop. Let's take the previous example, but move the initialization of j to the beginning of the outer loop.

  Displayln "i  j".
  Let i = 1.
  Let j = 1.
  While i <= 4 do
    While j <= 3 do
      Displayln $(i) ++ "  " ++ $(j).
      Relet j = j + 1.
    %While
    Relet i = i + 1.
  %While

When this modified sequence of statements is run, you get the following displayed.

i  j
1  1
1  2
1  3
Nothing more is displayed. To see why, do a hand simulation. (We do not cross out values here, for clarity.) DISPLAY indicates a place in the simulation where the values of i and j are displayed.
i    j
1    1
DISPLAY
1    2
DISPLAY
1    3
DISPLAY
1    4
2    4
3    4
4    4
When j becomes 4 the inner loop ends, and i is increased to 2. But the value of j is not changed. So the second time the inner loop starts, it begins with j = 4, and does not perform its body any times.


Case study: Checking whether one string is a substring of another

In a previous chapter you wrote a function that determines whether one string is a substring of another one. Let's look at that problem again. The idea is that isSubstring("zar", "lizard") has value true since "zar" occurs inside "lizard". There are two issues.

  1. The string (such as "zar") might occur at any position in the longer string. We need to try them all. One way to do that is to try each position, given by its start. For example, trying position 1 in "lizard" asks whether "zar" occurs at the beginning of the string. That is, is "zar" = "liz"? Trying position 2 asks whether "zar" occurs starting at the second character, etc.

  2. For each position, we need to check whether the string occurs at that location. That involves checking each character. For example, to see whether "zar" occurs at the beginning of "zather", you compare the first two characters (both 'z'), then the second two characters (both 'a') and finally the third two characters ('r' and 't'). You are only done if all of the characters match. That suggests another loop.

A skeleton of the definition of isSubstring is as follows. Notice the calculation of the last value of i to try. That is where the string being sought is at the last possible place. A flag tells when the loop is done.

  Define
    isSubstring(needle: String, haystack: String): Boolean by

    isSubstring(needle, haystack) = found |
      Let needleLen   = length(needle).
      Let haystackLen = length(haystack).
      Let i = 1.
      Let found = false.
      While (not found) and (i <= haystackLen - needleLen + 1) do
        if needle occurs in haystack starting at the i-th
        character then make found = true.
        Relet i = i + 1.
      %While
  %Define

But the part written in English needs to be expanded on. It is just a matter of checking each character. It should compare character 1 of needle with character i of haystack, then character 2 of needle with character i + 1 of haystack, etc. The inner loop wants to stop when it sees that there is a mismatch, because it only takes one character to be wrong to make a mismatch. Only after the inner loop has tried all of the characters without finding a mismatch does it make found = true.

  Define
    isSubstring(needle: String, haystack: String): Boolean by

    isSubstring(needle, haystack) = found |
      Let needleLen   = length(needle).
      Let haystackLen = length(haystack).
      Let i = 1.
      Let found = false.
      While (not found) and (i <= haystackLen - needleLen + 1) do
        Let j = 1.
        Let mismatch = false.
        While (not mismatch) and (j <= needleLen) do
          If needle_j =/= haystack_(i + j - 1) then
            Relet mismatch = true.
          %If 
          Relet j = j + 1.
        %While
        If not mismatch then
          Relet found = true.
        %If
        Relet i = i + 1.
      %While
  %Define

Avoiding nested loops using functions

For some functions or procedures, nested loops yield a simple and elegant way of expressing an algorithm. Often, though, they yield programs that are difficult to understand. Remember that we are striving for simplicity and elegance. Often, you can get a more elegant definition by moving the nested loop into another function. The inner loop of our isSubstring function is really just asking whether needle is a prefix of the substring of haystack that starts at the i-th character. So why not say that?

  Define
    isSubstring(needle: String, haystack: String): Boolean by

    isSubstring(needle, haystack) = found |
      Let needleLen   = length(needle).
      Let haystackLen = length(haystack).
      Let i = 1.
      Let found = false.
      While (not found) and (i <= haystackLen - needleLen + 1) do
        If needle == haystack_*[i,..., i + needleLen - 1]) then
          Relet found = true.
        %If
        Relet i = i + 1.
      %While
  %Define

What we have done is moved some of the work into the == and _* operators, which both perform repetitions. (Just because you, personally, did not create them does not mean they don't need to do a loop!)

You will find that you can always eliminate nested loops by using functions or procedures, and doing so often makes the program easier to understand and easier to get working.


Problems

  1. [solve] A pair of positive integers x and y is pythagorean if x2 + y2 is a perfect square. For example, the pair 3 and 4 is a pythagorean pair because 32 = 42 = 9 + 16 = 25, and 25 is a perfect square since 25 = 52.

    Write a program that displays all pythagorean pairs x and y, where x < 50 and y < 50. Also show, for each such pair, sqrt(x2 + y2).

    (Note. You can tell whether a number is a prefect square by taking its square root, rounding it to the nearest integer, and checking whether the square of the rounded square root is the same as the starting number. Here are definitions of two functions: issqrt(n) is the closest integer to the square root of n, and isPerfectSquare(n) yields true if n is a perfect square. Library function real(n) convert integer n to a real number and round(x) rounds to the nearest integer. Add these to your program.

    Define
      isqrt(n: Integer): Integer by
    
      isqrt(n) = round(sqrt(real(n)))
    %Define
    
    Define
      isPerfectSquare(n: Integer): Boolean by
    
      isPerfectSquare(n) = result |
        Let s: Integer      = isqrt(n).
        Let result: Boolean = (s*s == n).
    %Define
    


Summary

You can write a loop inside a loop.

Be sure to initialize for the inner loop just in front of the inner loop, so that its control variables are initialized every time it starts.


Review

It is a good idea to strive for simplicity and elegance when creating software.

Loop control variables are variables that the loop body changes. Be sure to initialize the loop control variables before the loop.