1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
4. Loop Invariants

4. Loop Invariants

In the previous lecture, we talked about specifications, documenting comments that describe the behaviors of different units of code. As we proceed in the course, these behaviors will become more complicated and often require one or more intricate loops to achieve. Therefore, it will be helpful to have techniques for reasoning about the behavior of our code within a “loopy” method. Loop invariants allow us to do this. More generally, invariants are properties that we can assert throughout our code’s execution.

Definition: Invariant

An invariant describes a property of a variable or a relationship between two or more variables in our code that is true at multiple pre-determined points of its execution.

By stipulating that an invariant must hold at these “checkpoints”, we force our code into a structure that is easier to reason about, document, and maintain. We’ll see that thinking about “loopy” procedures in terms of invariants will simplify the process of writing loops, allowing us to track multiple variables more easily and avoid off-by-one errors that are pervasive in hastily written code.

Prerequisites

Before we can talk about loop invariants, we’ll standardize some terminology for loops and briefly review Java’s loop syntax. We’ll also introduce some new notation and diagrams to help us talk about ranges of indices in arrays.

Loop Anatomy

Consider the following simple for-loop, which adds one to each entry of an int[] array, nums.

1
2
3
for (int i = 0; i < nums.length; i++) {
    nums[i] += 1;
}
1
2
3
for (int i = 0; i < nums.length; i++) {
    nums[i] += 1;
}

We call the code within the loop’s scope (i.e., the code that is between the curly braces delimiting the loop) its body. The code that precedes the body is the loop’s declaration. In a for-loop, the declaration includes three pieces, separated by semicolons.

Initialization:

The first piece, “int i = 0”, declares a loop variable, i and initializes its value to 0.

Definition: Loop Variable

A loop variable is a variable whose primary purpose is keeping track of where we are in the execution of a loop.

Loop Guard:

The second piece, “i < nums.length”, is a boolean expression that tells us when we proceed with another iteration of the loop. We call this condition the loop guard since it “guards” access to the loop body.

Definition: Loop Guard

The loop guard is the condition (i.e., boolean expression) that is evaluated at the start of the loop body. When it evaluates to true, our code enters and executes the loop body. When it evaluates to false, our code "falls through" the loop body and exits the loop.

Increment:

The third piece, “i++”, is code that updates the loop variable. It is executed at the end of the loop body.

These same pieces, loop variables, initialization, loop guard, loop body, and increment can also be found in a while-loop, just arranged slightly differently.

for-loop:
1
2
3
for(<initialization>; <loop guard>; <increment>) {
    <loop body>
}
1
2
3
for(<initialization>; <loop guard>; <increment>) {
    <loop body>
}
while-loop:
1
2
3
4
5
<initialization>
while(<loop guard>) {
    <loop body>
    <increment>
}
1
2
3
4
5
<initialization>
while(<loop guard>) {
    <loop body>
    <increment>
}

Re-writing our example code from above gives:

1
2
3
4
5
int i = 0;
while (i < a.length) {
    a[i] += 1;
    i++;
}
1
2
3
4
5
int i = 0;
while (i < a.length) {
    a[i] += 1;
    i++;
}

Since we are able to directly translate a for-loop into a while-loop, we will focus on while-loops for the rest of this lecture. In addition, since while-loops place the increment step within the loop body, we won’t distinguish it as a separate piece. Rather, we’ll refer to the entire inner scope of the loop (including the increment) as its body.

Range Notation

Many of the loops that we will discuss today and throughout the course will act on arrays or array-like (i.e., linearly ordered) sequences. When talking about these sequences, we will often want to refer to ranges, or contiguous subsequences, of their elements. We’ll use a standard notation for these ranges that borrows from mathematical interval notation. For example, the notation a[2..4] refers to the three-element subsequence consisting of a[2], a[3], and a[4]. Here, the .. notation denotes the “interval” of all indices between 2 and 4, and the surrounding square brackets indicate that this interval includes both endpoints. We use round brackets when we want to exclude an endpoint, so a[2..4) consists of only a[2] and a[3] (it excludes the endpoint a[4]), whereas a(2..4) consists of only a[3] (it excludes both endpoints).

We will sometimes use some shorthand when an endpoint falls at the boundary of the array. When we omit the left number in range notation, this means “starting from the first index”, so a[..3] would include a[0], a[1], a[2], and a[3]. Similarly, a(..3) would include a[1] and a[2] but exclude the endpoints a[0] and a[3]. When we omit the right number in range notation, this means “ending at the last index”, so a[4..] would consist of a[4] through and including a[a.length-1].

It is possible to use range notation to refer to an empty range, which is signified by any interval whose first included index is greater than its last included index. Some such notation is a[..0), a(2..2), or a[3..2]. A lot of these rules may seem a bit strange or arbitrary now, but as you start using them to document the code that you write, they should become more natural.

Array Diagrams

It will also be useful to draw pictures that visualize properties of various ranges of an array. We do this using array diagrams. These diagrams are different from the memory diagrams we have seen so far. They extract away a lot of lower-level details (such as the objects, types, runtime stack, memory heap, etc.) and leave only a visualization of the array itself. We use vertical lines to delineate ranges of the array and use labels at the top of these lines to clarify these delineations. The ranges (boxes) of the array are labeled with properties.

For example, if we have an array a with capacity 8 in which a[..3) are even and a[3..] are odd, we can visualize this with the array diagram:

Remark:

Note that the labels at the top of the array diagram are drawn offset from the vertical lines. This is crucial. Placing a number directly above a line would be incorrect because the lines denote the space "between" indices of the array. The numbers label array cells. We interpret the 3 to the right of the middle vertical line as saying, "the leftmost cell in the right range is a[3]". The labels can appear on either side of the line, depending on whether it is denoting the rightmost index in the left range or the leftmost index of the right range.

As we will see later, we’ll often have loop variables labeling array diagrams that represent the state of the array after some iteration of the loop. For example, the diagram

will appear in our analysis of the binary search algorithm and indicates that the entries in a[..l) are all less than v, the entries in a[r..] are all greater than or equal to v, and we don’t know anything about the entries in a[l..r).

Writing “Loopy” Code

Now that we have introduced some vocabulary and notation to reason about loops, let’s put it into practice to develop the following method from the perspective of loop invariants. While this method may seem simple, and while the procedure that we follow will likely feel excessive, it is important to practice techniques on small examples that we can then extend to more complicated examples that require them. Consider the following method specification:

1
2
3
4
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { ... }
1
2
3
4
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { ... }
Remark:

As review from the previous lecture, write some unit tests for this and the other methods that we develop during the lecture based on their specifications. You can use these unit tests to verify the correctness of the provided method definitions.

To compute the frequency of key, we will need to compare it to each element of a, which will require iteration over a. To start developing our loop, we should think about what local variables we will need to declare. In other words, what will we need to keep track of as we proceed with the iteration? In this case, we need to keep track of which entries of a we have checked and which entries we have not. If we choose to scan the entries in order, from left to right, we can accomplish this with an int variable i that represents the next index that we will inspect. We’ll also need to keep a running count of how many times we have seen key during our scan. We can do this with another int variable count.

Visualizing Loop Behavior

The next thing that we’ll do is use array diagrams to visualize the state of the array over the course of the loop. It’s usually easiest to start by drawing the state at the very beginning of the loop (just before we evaluate the loop guard for the first time) and at the very end of the loop (at the point where the loop guard evaluates to false).

For our frequencyOf() example, the “Pre” diagram is pretty basic. Since we haven’t looked at any entries of a, and since our method spec doesn’t assert any pre-conditions about a, we know nothing.

At the end of the loop, we expect that our count variable will hold the number of occurrences of key in a.

The last picture that we’ll draw will visualize the state of array and local variables of the method at the start of an arbitrary loop iteration. In this case, since the loop variable i is keeping track of our progress, we’ll consider the state of the program when i has a certain value. Since we are planning to scan over the array from left to right, we will have checked the array cells with indices 0 through i-1, in our range notation, a[..i), but will not yet have looked at a[i..]. What will be the value of count at this point in the method? It should be the number of times that key occurs among the elements we’ve checked, a[..i). Visually,

This picture illustrates the main property that is maintained (i.e., invariant) throughout the loop: whenever we evaluate the loop guard, count will equal the number of occurrences of key in a[..i). This property is the invariant of this loop.

Definition: Loop Invariant

A loop invariant describes a relationship involving the loop variables (and potentially other variables) that is true every time that the loop guard is evaluated. In other words, the loop invariant is true at the start and end of each loop iteration.

From Array Diagrams to Code

Let’s use our loop invariant to write the frequencyOf() method. We’ll start by placing the loop invariant in a multi-line comment at the top of a while-loop stub.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while ( ) {

     }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while ( ) {

     }
}

It is good programming practice to document your loop invariants, as this will give other developers (or even you in the future, when your loop code is not fresh in your mind) an easy way to understand the design of the loop. Now, we can use the loop invariant to help write each part of the loop: the initialization, loop guard, and loop body (plus increment).

Initialization: The loop invariant must be true when we enter the loop

Before we enter the loop, we must write initialization statements for all of the variables involved in the invariant (i and count). We must choose their initial values so that the loop invariant is true the first time that the loop guard is evaluated. Since we haven’t looked at any entries of a yet and we plan to scan a from left to right, the first index we will inspect is i = 0. Plugging this into the loop invariant, we see that we must initialize count to number of occurrences of key in a[..0). Since this is notation for the empty range, which cannot contain any occurrences of key, we initialize count = 0. Hooray! The loop invariant is true at the start.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while ( ) {

     }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while ( ) {

     }
}

Loop Guard: If the loop invariant is true when we exit the loop, we have computed what we wanted to.

We exit a loop the first time that the loop guard evaluates to false. In this case, we want to exit the loop once we have inspected all entries of a. Thus, the loop guard should evaluate to true as long as there remain entries of a that we have not looked at. In other words, the loop guard should be true as long as our next index to check i is a valid index, or i < a.length. Once i == a.length, this will signal that we have seen all entries of a, and our loop invariant tells us that count will be equal to the number of occurrences of key in a[..a.length). This is range notation for the entire array a, meaning count will hold the exact number we wanted to compute. Thus, we can return count after breaking out of the loop and satisfy the method post-condition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while (i < a.length) {

     }
     return count;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while (i < a.length) {

     }
     return count;
}

Loop Body: Our loop makes progress toward termination in each iteration and re-establishes the loop invariant before re-evaluating the loop guard.

We’ve gotten to the main portion of the loop, the body. We enter the body immediately after evaluating the loop guard, so the loop invariant will be true. During one iteration of the loop body, we need to accomplish two things. First, we need to make some measurable progress toward our goal. This will help guarantee that our loop will eventually terminate. Then, we also need to make sure that we re-establish the loop invariant. In our frequencyOf() example, we make progress by inspecting one more element of a, a[i], which allows us to increment i at the end of the body (the next element we’ll check moves one cell to the right). However, when we update the value of i, we may also need to update the value of count to preserve the loop invariant. How do we do this? If a[i] == key, we’ve found another occurrence, so we should increment count by 1. Otherwise, if a[i] != key, we don’t need to do anything; the value of count remains correct even after incrementing i. This suggests the following loop body.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while (i < a.length) {
        if (a[i] == key) {
            count++;
        }
        i++;
     }
     return count;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/** 
 * Returns the number of occurrences of `key` among the elements in array `a`.
 */
static int frequencyOf(int key, int[] a) { 
    int i = 0; // next index of `a` to check
    int count = 0; 
    /*
     * Loop invariant: `count` = number of occurrences of `key` in `a[..i)`
     */
     while (i < a.length) {
        if (a[i] == key) {
            count++;
        }
        i++;
     }
     return count;
}

Recapping This Example

Let’s revisit the invariant diagram that we drew earlier and add one more piece of information. Let’s draw a small arrow beneath the middle vertical line to visualize how this boundary moves during the loop execution. In this case, the line corresponds to the loop variable i, which increases in each loop iteration, so the arrow points to the right.

If we “rewind time” to the beginning of the loop, this vertical line will move to the left until it coincides with the left array boundary. We end up with our “Pre” array diagram, and the loop variable i now overlaps with 0, giving us the initialization i = 0. If we “advance time” to the end of the loop, this vertical line will move to the right until it coincides with the right array boundary. We end up with our “Post” array diagram, and the loop variable i now overlaps with a.length. This tells us that the loop guard becomes false when i == a.length, giving us the loop guard i < a.length. This illustrates the benefits of developing loop invariants and array diagrams; once you set things up correctly, everything you need to write the loop perfectly on the first try (no off-by-one indexing errors) is there for you, as long as you know where to look.

Altogether, the example that we just finished suggests the following strategy for developing a loop:

  1. Identify the local variables that you’ll need to track your progress during the loop (or at least attempt to… this may need to be refined as you consider later steps).
  2. Draw out the “Pre” and “Post” array diagrams using the method pre-conditions and post-condition.
  3. Draw the “Inv” array diagram that “hybridizes” the “Pre” and “Post” pictures and represents an intermediary state of the loop. This diagram should include all of your variables from step 1.
  4. Use the “Inv” array diagram to write the loop invariant, documenting it as a comment at the start of your loop.
  5. Slide the “Inv” diagram boundaries until it looks like the “Pre” diagram and use this (along with the loop invariant) to initialize the variables.
  6. Slide the “Inv” diagram boundaries until it looks like the “Post” diagram and use this to write the loop guard. Check if the loop post-condition is met once the loop guard becomes false. If not, add any necessary code after the loop.
  7. Develop the loop body so that it makes progress toward the post-condition. Make sure that it re-establishes the loop invariant by the end of the iteration.

For the rest of the lecture, we’ll look at two more examples developing “loopy” code with this process. We will continue to make extensive use of loop invariants throughout the course, and hopefully you will be convinced that they are a useful tool that you will adopt throughout your CS career.

More Examples

Argmin

Let’s define the following method according to its specification.

1
2
3
4
5
/** 
 * Returns an *index* of the minimum element in `a`. 
 * Requires that `a` contains at least one element. 
 */
static int argmin(double[] a) { ... }
1
2
3
4
5
/** 
 * Returns an *index* of the minimum element in `a`. 
 * Requires that `a` contains at least one element. 
 */
static int argmin(double[] a) { ... }

First, let’s think about the local variables that we’ll need in this method. Again, we’ll need to scan over the contents of array a in search of the smallest element, and we’ll use a loop variable i to keep track of the next index to visit. We’ll also need to keep track of the smallest value we’ve seen, since we can use this to identify the minimum element. We can do this with a double variable min. While it may seem like this is sufficient, we’ll need an additional variable if we’d like to compute the argmin using only one pass over the array. The method returns the index of the minimum element, not its value, so we’ll use an int variable loc to keep track of the location of the smallest value that we’ve seen.

Remark:

It's possible to write the argmin() method using only two local variables and a single pass over the array. We leave this as an exercise for you to think about (Exercise 4.4).

Thinking ahead a bit, we’ll need to initialize min to some value, and the most natural choice will be the first element of the array. Then, we can use our loop to scan over the remaining elements and compare them to min. Let’s use array diagrams to visualize the state of the array before, after, and during the loop. Our only pre-condition is that a is non-empty.

At the end of the loop, we want min to be the smallest value in a, and we want loc to be an index where this value occurs.

At the start of the i‘th iteration of the loop, we will have seen the first i entries of the array, min should hold the minimum value among these entries, loc should hold the index of this minimum value. We won’t know anything about the remaining entries.

Step through the following animation to see we can develop the method definition using the information from these diagrams, following the steps outlined in the previous section.

previous

next

Remark:

This method is a great example of an underspecified method; the spec does not clarify which index should be returned when the minimum value appears multiple times in a. It is possible that two different implementations of argmin() could both satisfy the specification but return different results. This idea is explored further in Exercise 4.5.

Partitioning

Let’s define the following method according to its specification.

1
2
3
4
5
6
/** 
 * Rearranges the elements of `a` so that all even elements appear before 
 * all odd elements. Returns the index of the first odd element, or returns
 * `a.length` if all elements are even.
 */
static int paritySplit(int[] a) { ... }
1
2
3
4
5
6
/** 
 * Rearranges the elements of `a` so that all even elements appear before 
 * all odd elements. Returns the index of the first odd element, or returns
 * `a.length` if all elements are even.
 */
static int paritySplit(int[] a) { ... }

This method partitions, or splits, the elements of the array into two distinct ranges, a range of even numbers and a range of odd numbers. How can we accomplish this task? Over the course of the method, we will need to inspect each element of the array to check its parity. We can again accomplish this by scanning the elements from left to right, using an int variable i to keep track of the next index to inspect. To carry out the partitioning, we can imagine “growing” two ranges from the edges of the array, a range of all the odd numbers starting from the left and a range of all the even numbers starting from the right. The variable i can represent the “frontier” of this left range a[..i), and we’ll need a second variable j to track the “frontier” of the right range a[j..].

Now, let’s think about how the state of the array changes. At the start of the method, we have no information about the contents of the array.

At the end of the loop, we will have successfully partitioned the array. The left and right ranges will have grown to meet in the middle and j will be the first index of the odd partition, which will be just to the right of the boundary between these ranges.

At the start of the i‘th iteration of the loop, we’ll know that every element to the left of i is even (i is the “frontier” of the even range on the left of the array) and every element to the right of j is odd (j is the “frontier” of the odd range on the right). We haven’t inspected any of the elements in a[i..j).

Remark:

Some of the choices we have made when setting up these array diagrams are arbitrary, for example which side of the lines that i and j each go on. It's important that we are clear about which choice we make, but making the opposite choice would also lead to a correct loop, just with slightly different initialization, indexing, and loop guard. See Exercise 4.7.

Within our paritySplit() method, we will be rearranging the entries of a, which we’ll do as a series of element swaps. To keep the body of this method focused, let’s extract this swapping code out into a separate helper method. The easiest way to swap the contents of two variables is to introduce a third temporary variable to hold one of the values while we swap over the other.

1
2
3
4
5
6
7
8
9
/** 
 * Swaps the entries `a[x]` and `a[y]`. Requires that `0 <= x < a.length`
 * and `0 <= y < a.length`.
 */
static void swap(int[] a, int x, int y) {
    int temp = a[x];
    a[x] = a[y];
    a[y] = temp;
}
1
2
3
4
5
6
7
8
9
/** 
 * Swaps the entries `a[x]` and `a[y]`. Requires that `0 <= x < a.length`
 * and `0 <= y < a.length`.
 */
static void swap(int[] a, int x, int y) {
    int temp = a[x];
    a[x] = a[y];
    a[y] = temp;
}

Step through the following animation to see we can develop the method definition using the information from these diagrams, following the steps outlined in the previous section.

previous

next

Main Takeaways:

  • All loops contain some common structural elements. We initialize loop variables to track their progress and a loop guard controls when we (re-)enter the loop body.
  • Invariants are relationships between variables in our code that we assert are true at certain points of its execution. Loop invariants involve loop variables and must hold whenever the loop guard is evaluated.
  • Range notation provides a convenient shorthand for documenting properties of arrays. Array diagrams are a visual representation of these properties.
  • When initializing loop variables, their values must make the loop invariant true before entering the loop.
  • When the loop guard becomes false and the loop invariant is true, we should have accomplished the task of the loop.
  • In each iteration of the loop body, we must make progress toward termination and re-establish the loop invariant.

Exercises

Exercise 4.1: Check Your Understanding
(a)
Which of the following is not true about the loop guard?
Check Answer
(b)
Suppose that data is an array with data.length == 8. How many elements are in the range data(3..)?
Check Answer
(c)

Consider the following “loopy” code that counts the number of 0s in int[] nums:

1
2
3
4
5
6
7
int count = 0;
/*
 * Loop invariant: `count` = # of zeros in `a[..i)`
 */
for (int i = 0; i < a.length; i++) {
  count += a[i] == 0 ? 1 : 0;
}
1
2
3
4
5
6
7
int count = 0;
/*
 * Loop invariant: `count` = # of zeros in `a[..i)`
 */
for (int i = 0; i < a.length; i++) {
  count += a[i] == 0 ? 1 : 0;
}
How many times will the loop guard evaluate to true?
Check Answer
At how many different points during the execution of this code are we guaranteed that the loop invariant will be true?
Check Answer
(d)

The following array diagram visualizes a loop invariant:

Which of the following is the correct initialization for these loop variables?
Check Answer
Which of the following is the correct loop guard?
Check Answer
Exercise 4.2: Loop Anatomy
Consider the following "loopy" method. Its variable names have been chosen poorly to partially obfuscate its behavior.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * A loopy method with some "mystery" behavior.
 */
int mystery(int[] a) {
  int x = 0;
  for (int i = 0; i < a.length; i++) {
    if (x == 0 && a[i] > 0) {
      x = i;
    }
  }
  return x;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * A loopy method with some "mystery" behavior.
 */
int mystery(int[] a) {
  int x = 0;
  for (int i = 0; i < a.length; i++) {
    if (x == 0 && a[i] > 0) {
      x = i;
    }
  }
  return x;
}
(a)
Rewrite the documentation for this method to precisely describe its behavior.
(b)

Identify the following components of this loopy method:

  • Its loop variable(s)
  • Its initialization
  • Its loop guard
  • Its increment
  • Its loop body
(c)
Rewrite the body of this method to use a while-loop instead of a for-loop.
(d)
Document this for-loop with a comment describing its invariant.
(e)
Can you think of a modification that we can make to the loop guard that will frequently allow this loop to run for fewer iterations?
(f)
Draw array diagram that visualizes the “Post”-condition of your modified loop.
Exercise 4.3: Developing Basic Loops
Complete the definition of each of the following methods. For each, follow the process described in Section 4.2.3, identifying the loop variables, visualizing their relationships with array diagrams, and using these diagrams to write the "loopy" code and document its invariant.
(a)
1
2
3
4
/**
 * Returns the sum of all entries in the given array of `nums`.
 */
static double sum(double[] nums) { ... }
1
2
3
4
/**
 * Returns the sum of all entries in the given array of `nums`.
 */
static double sum(double[] nums) { ... }
(b)
1
2
3
4
/**
 * Returns the sum of all *positive* entries in the given array of `nums`.
 */
static double sumPositives(double[] nums) { ... }
1
2
3
4
/**
 * Returns the sum of all *positive* entries in the given array of `nums`.
 */
static double sumPositives(double[] nums) { ... }
(c)
1
2
3
4
5
/**
 * Returns the number of elements in `nums` that are greater than the 
 * given `bound`.
 */
static int countAbove(int[] nums, int bound) { ... }
1
2
3
4
5
/**
 * Returns the number of elements in `nums` that are greater than the 
 * given `bound`.
 */
static int countAbove(int[] nums, int bound) { ... }
(d)
1
2
3
4
5
/**
 * Returns the maximum value of an element in `nums`.
 * Requires that `nums` contains at least one element. 
 */
static int max(int[] nums) { ... }
1
2
3
4
5
/**
 * Returns the maximum value of an element in `nums`.
 * Requires that `nums` contains at least one element. 
 */
static int max(int[] nums) { ... }
(e)
1
2
3
4
5
/**
 * Returns an *index* of the maximum element in `nums`. 
 * Requires that `nums` contains at least one element. 
 */
static int argmax(int[] nums) { ... }
1
2
3
4
5
/**
 * Returns an *index* of the maximum element in `nums`. 
 * Requires that `nums` contains at least one element. 
 */
static int argmax(int[] nums) { ... }
(f)
1
2
3
4
5
/**
 * Returns the range of elements in `nums` (i.e., its largest element 
 * minus its smallest element).
 */
static int range(int[] nums) { ... }
1
2
3
4
5
/**
 * Returns the range of elements in `nums` (i.e., its largest element 
 * minus its smallest element).
 */
static int range(int[] nums) { ... }
(g)
1
2
3
4
5
/**
 * Returns `true` if `nums` contains two *consecutive* entries with the same
 * value. Otherwise, returns `false`.
 */
static boolean hasConsecutiveDuplicates(int[] nums) { ... }
1
2
3
4
5
/**
 * Returns `true` if `nums` contains two *consecutive* entries with the same
 * value. Otherwise, returns `false`.
 */
static boolean hasConsecutiveDuplicates(int[] nums) { ... }
Exercise 4.4: Simplify argmin()
Redesign the loop invariant and re-implement the argmin() method using only two local variables. Think about which state needs to be stored and which can be easily recomputed (i.e., without a loop) when needed.
Exercise 4.5: argmin() Specification
Our argmin() method is underspecified. If the minimum element in a appears in multiple indices, the existing specification does not document which of those indices will be returned.
(a)
Carefully examine the given implementation of argmin() and write a refined specification that details which index is returned.
(b)
Refine the specification in a different way and give an alternate implementation of argmin() that conforms to your new specification.
(c)
Create test cases for the original underspecified argmin() method that will pass for both its original and your alternate implementations.
Exercise 4.6: Code Tracing
Carefully trace through the execution of the paritySplit() method on the following input:
In which order will its entries be at the end of its execution?
Exercise 4.7: Parity Split with Adjusted Indices
Reimplement the paritySplit() method with local variables i and j adjusted to match the following invariant diagram.
Exercise 4.8: Dutch National Flag
One famous problem that demonstrates the effectiveness of loop invariants is the Dutch National Flag problem proposed by computer scientist Edsger Dijkstra (more on him soon!). The input to the problem is an array arr with the only elements being 'r' (red), 'w' (white), and 'b' (blue). The goal is to partition the array in one pass into a red segment, followed by a white segment, followed by a blue segment. That is, arr should satisfy this postcondition:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class DutchNationalFlag {
    /**
     * Swaps the elements in `arr` at indices `x` and `y` in place.
     */
    static void swap(char[] arr, int x, int y) {
        char tmp = arr[x];
        arr[x] = arr[y];
        arr[y] = tmp;
    }

    /**
     * Rearranges the elements of `arr` so that all 'r' elements appear before 
     * all 'w' elements, which appear before all 'b' elements. Requires each 
     * element of `arr` to be either 'r', 'w', or 'b'.
     */
    static void dutchNationalFlagAlgorithm(char[] arr) { ... }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class DutchNationalFlag {
    /**
     * Swaps the elements in `arr` at indices `x` and `y` in place.
     */
    static void swap(char[] arr, int x, int y) {
        char tmp = arr[x];
        arr[x] = arr[y];
        arr[y] = tmp;
    }

    /**
     * Rearranges the elements of `arr` so that all 'r' elements appear before 
     * all 'w' elements, which appear before all 'b' elements. Requires each 
     * element of `arr` to be either 'r', 'w', or 'b'.
     */
    static void dutchNationalFlagAlgorithm(char[] arr) { ... }
}
For each of the following invariant diagrams, write an implementation of dutchNationalFlagAlgorithm() with a loop that maintains that invariant. Use our reasoning from paritySplit as inspiration.
(a)
(b)
(c)
(d)
Exercise 4.9: Counting Skyscrapers
In an array arr of positive integers, we call an entry a "skyscraper" if it is larger than every element to its left. That is, the entry in index i is a skyscraper if and only if arr[..i) < arr[i] (where, by convention, we mean that this inequality holds for every entry of the range arr[..i)). If we imagine the entries represent the heights of buildings, then the top floor of a skyscraper will be visible (not shadowed by another skyscraper) to a person standing to left of the array.
In the above example, the three entries beneath the pink rectangles are "skyscrapers", while the three entries beneath the gray rectangles (that lie in the shadows of the pink rectangles) are not "skyscrapers". Note that the first entry must always be a skyscraper, being greater than any previous elements (of which there are none). Design a loop invariant and for counting skyscrapers and use it to complete the definition of the following method.
1
2
3
4
5
/** 
 * Returns the number of indices `i` for which `a[..i) < a[i]`.
 * Requires that `a` is non-empty, and all entries of `a` are positive. 
 */
int countSkyscrapers(int[] a) { ... }
1
2
3
4
5
/** 
 * Returns the number of indices `i` for which `a[..i) < a[i]`.
 * Requires that `a` is non-empty, and all entries of `a` are positive. 
 */
int countSkyscrapers(int[] a) { ... }
Exercise 4.10: Diagramming Nested Loop Invariants
Consider the following method that determines whether an array contains duplicate elements.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** 
 * Returns whether `a` contains duplicate entries, distinct indices 
 * `i != j` with `a[i] == a[j]`.
 */
static boolean hasDuplicates(int[] a) {
  for (int i = a.length - 1; i >= 0; i--) {
    for (int j = 0; j < i; j++) {
      if (a[i] == a[j]) {
        return true;
      }
    }
  }
  return false;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/** 
 * Returns whether `a` contains duplicate entries, distinct indices 
 * `i != j` with `a[i] == a[j]`.
 */
static boolean hasDuplicates(int[] a) {
  for (int i = a.length - 1; i >= 0; i--) {
    for (int j = 0; j < i; j++) {
      if (a[i] == a[j]) {
        return true;
      }
    }
  }
  return false;
}
(a)
Rewrite the body of this method to use two nested while-loops.
(b)
First, let’s focus on the outer while-loop. Draw an array diagram to depict its loop “Inv"ariant. Then, draw two possible “Post”-condition diagrams, one for when we exit the loop and return false, and one for when we return true early from the loop.
(c)
Now, let’s focus on the inner while-loop. To do this, we’ll restrict to one particular iteration i of the outer loop and think about the inner loop iteration during this one outer loop iteration. Draw an array diagram to depict the inner loop “Inv"ariant. Then, draw two possible “Post”-condition diagrams, one for when we exit the loop via the loop guard, and one for when we return true early from the loop.
Exercise 4.11: Array Defragment
In older, mechanical hard drives, data was accessed by a moving magnetic head. Since the head needed to physically move to the location of the data at some fixed speed, a computer would slow down when the data was far away from the position of the head. As data was written and deleted from the drive, different sized gaps would arise in its storage. Occasionally, a user would defragment their hard drive, filling in these gaps to move the data closer together and improve the performance of the drive. The following method simulates the defragmentation process on an array of Strings.
1
2
3
4
5
6
7
8
/**
 * Returns a new, "defragmented" copy of the given `words` array in which all
 * `null` entries have been removed and all remaining entries have retained 
 * their same relative positions. For example, the input array {"apple", null, 
 * "cherry", null, null, "fig", null} would result in the output array 
 * {"apple", "cherry", "fig"}.
 */
static String[] defragment(String[] words) { ... }
1
2
3
4
5
6
7
8
/**
 * Returns a new, "defragmented" copy of the given `words` array in which all
 * `null` entries have been removed and all remaining entries have retained 
 * their same relative positions. For example, the input array {"apple", null, 
 * "cherry", null, null, "fig", null} would result in the output array 
 * {"apple", "cherry", "fig"}.
 */
static String[] defragment(String[] words) { ... }
Complete the definition of this method, using loop invariants to help develop its loops. The solution that we have in mind includes two passes over the words array, one to count the non-null entries (and determine the output array size) and one to copy the non-null entries to the output array.
Exercise 4.12: Merge Sorted Arrays
Consider the following method to merge two sorted arrays. We will use a variant of this method as a subroutine of the Merge Sort algorithm that we will discuss in a few lectures.
1
2
3
4
5
6
/**
 * Returns a sorted (in ascending order) array consisting of all of the entries
 * from both given arrays `a` and `b`. Requires that `a` and `b` are sorted
 * in ascending order.
 */
static int[] merge(int[] a, int[] b) { ... }
1
2
3
4
5
6
/**
 * Returns a sorted (in ascending order) array consisting of all of the entries
 * from both given arrays `a` and `b`. Requires that `a` and `b` are sorted
 * in ascending order.
 */
static int[] merge(int[] a, int[] b) { ... }
For example, given inputs,
the merge() method would return the 12-element array,
(a)
Take some time to write unit tests for the merge() method, making sure to cover different scenarios and corner cases that may arise.
(b)
Determine the loop variables for the merge() method and use these to draw diagrams to visualize the loop “Inv"ariant. Your diagram will need to include all three arrays (both inputs and the output).
(c)
Use your diagram from part (b) to complete the definition of the merge() method. Verify that your definition passes your unit tests from part (a).