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
6. Recursion

6. Recursion

In this lecture, we’ll discuss recursion. Although we expect that you have some experience with recursion from a previous programming course, we will view it through the lenses that we have developed over the past couple of lectures. First, we’ll consider how we can use specifications to aid in the development of recursive methods. We’ll see some common paradigms that arise for developing recursive methods in Java. Next, we’ll think about how to visualize the execution of recursive methods on the call stack and how we can use this to help us reason about their complexity. Recursion will play a central role throughout CS 2110, as many of the data structures and algorithms that we will consider later in the course are most naturally expressed recursively.

Recursive Methods

A core component for the development of programs is an ability to repeatedly execute a sequence of instructions until some condition is met. One way that we realize this repeated execution is with loops. We call this approach iteration. As we saw, we can use loop invariants to reason about the correctness of iterative code. Another strategy for realizing repetition is recursion.

Definition: Recursive Method

A method is recursive when it can be invoked from within its own definition.

By making a call to itself, a method can trigger an additional execution of its body. Varying the parameters of this “inner” method call allows progress to be made, similar to how we update the values of one or more loop variables within each loop iteration.

Remark:

When we are discussing the invocation of recursive methods, it is convenient to use the terminology of "outer" and "inner" calls to the method. The "outermost" call is the initial invocation of the method (made by the client), whereas "inner" calls are those that are made from within the method itself. We'll soon see that "inner" calls appear in higher call frames in our illustration of the runtime stack.

In recursion, we guard the number of repetitions using a base case, which is an execution path through a recursive method that does not make any recursive calls. Rather, it computes and returns the result of the method directly. Typically, base cases handle the simplest, or “smallest” inputs to the method, whereas recursive cases (those that make recursive calls during their execution) handle more complicated, or “larger” inputs.

To help illustrate the components of recursive methods, we’ll develop a simple one that computes the factorial of a number. If you’re unfamiliar, the factorial function, usually denoted in math with an exclamation point, takes in a non-negative integer \(n\) and returns the product of all positive integers less than or equal to \(n\). For example, \(4! = 1 \cdot 2 \cdot 3 \cdot 4 = 24\). We can write an iterative implementation of the factorial function as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  int i = 1;
  int product = 1;
  /* loop invariant: product = (i-1)! */
  while (i <= n) {
    product *= i;
    i++;
  }
  return product;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  int i = 1;
  int product = 1;
  /* loop invariant: product = (i-1)! */
  while (i <= n) {
    product *= i;
    i++;
  }
  return product;
}

Now, let’s consider a recursive definition of this method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  if (n <= 1) {
    return 1;
  } 
  int f = factorial(n - 1);
  return f * n;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  if (n <= 1) {
    return 1;
  } 
  int f = factorial(n - 1);
  return f * n;
}

We can use Java’s conditional operator to simplify this definition:

1
2
3
4
5
6
7
8
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  return (n <= 1) ? 1 : (n * factorial(n - 1));
}
1
2
3
4
5
6
7
8
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  return (n <= 1) ? 1 : (n * factorial(n - 1));
}

This latter recursive definition is very concise, collapsing the entire calculation of the return value into the evaluation of a single (recursive) expression. This is a fairly common phenomenon. Often, finding a way to express a computation recursively will lead to much more concise, “elegant” code. This is beneficial for maintenance, since there are fewer variables and loops to reason about, and it is generally easier to read. However, this does not mean that recursive code is always better. We will see that the space complexity of recursion can be much worse. Additionally, the learning curve to recursion tends to be a bit steeper since it requires thinking more abstractly about the structure of the problem you are solving (as opposed to the more mechanical development of iterative code).

Now, let’s break down this recursive definition of factorial() to understand how it works. We’ll use the upper, more verbose definition as this is easier to reason about. Recursion works by taking a “large” problem and expressing it in terms of one of more “smaller” versions of the same problem. During execution, a recursive method calls itself to get the answers to the “smaller” problems and does a little extra work to turn this into the answer to the “large” problem. When it can’t break a problem down any further, the inputs should be simple enough that the answer to the problem becomes “obvious”, and it can be supplied directly. This gives rise to the base case(s) of the recursion.

For the factorial() function, our input is a non-negative integer \(n\). The problem gets “harder” when \(n\) grows larger, since the factorial function evaluates to a product with more terms. To develop a recursive implementation of factorial(), we need to (1) identify the base cases and (2) develop the recursive case.

The base case should correspond to the simplest possible inputs, meaning the smallest non-negative integers \(n\). What will be the value of factorial() in these cases. When \(n=0\), \(0!\) is defined as the product of all positive integers \( \leq 0 \). Since there are no such positive integers, mathematical conventions tell us that this product evaluates to the multiplicative identity 1. Similarly, \(1!\) is the product of only 1, so it evaluates to 1. We can build these answers directly into our factorial() definition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  if (n <= 1) {
    return 1;
  } 
  // TODO: recursive case
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12; // defensive programming
  if (n <= 1) {
    return 1;
  } 
  // TODO: recursive case
}

Now, our factorial() method meets its specifications for inputs \(n=0\) and \(n=1\), and we are left to reason about the larger inputs. We’ll do this recursively. When developing the recursive case, the implementer acts as their own client when they make a recursive method call. In doing this, they must trust that the method will conform to its specifications during the recursive “inner” call and use this to guarantee that the method will behave correctly for the “outer” call.

For this factorial example, we want to compute factorial(n) for value \(n \geq 2\). As the specifications say, this is the product of all positive integers less than or equal to \(n\), so \(1 \cdot 2 \cdot \dots \cdot (n-2) \cdot (n-1) \cdot n\). To come up with the recursive case, we’ll ask ourselves, “How could we use the return value of the factorial() function called on a smaller value than n to help us compute factorial(n)?” Let’s consider using factorial(n-1). From the spec, we see that factorial(n-1) will return the product of all positive integers less than or equal to \(n-1\), \(1 \cdot 2 \cdot \dots \cdot (n-2) \cdot (n-1)\). Comparing this to what factorial(n) must return, we see it’s only missing a factor of \(n\). Thus, multiplying factorial(n-1) * n will give us the correct return value. In our above implementation, we stored factorial(n-1) in a local variable f, which will help us visualize the behavior of this code in upcoming animations.

In summary, a strategy for developing a recursive method is to directly write out the simplest base cases. For larger cases, make a recursive call to the method, “trust the spec” to understand the post-condition of the recursive call, and then figure out any additional work that needs to be done to meet the post-condition of the method.

Remark:

At first, this process usually seems a bit strange, even magical, to students. How can we use a function that we haven't finished writing? We're assuming that the recursive call works correctly when we "trust the spec", but how do we know that it actually is correct? The answers to these questions fall a bit outside of our course scope, and are more a subject for CS 2800, as they are related to proofs by induction. In short, we can build up the argument for correctness systematically from "smaller" to "larger" inputs. The base cases directly establish the correctness for the "smallest" inputs. Then, the method invocations that call the base cases can rely on their correctness, establishing the correctness of slightly "larger" inputs. This reasoning can be repeated indefinitely.

Visualizing the execution of recursive methods can also help illustrate how they work. As recursive calls are made, the method invocations collect as call frames on the runtime stack. Whenever we reach a base case, the call stack begins to collapse until we obtain the return value of the original call. Here, we visualize the (un-simplified) call factorial(4).

previous

next

While this image of stacking up a bunch of recursive calls is helpful for visualizing the execution of the code (and will be helpful when we analyze time/space complexity in upcoming lectures), it is less crucial for developing recursive methods. Here, it is better to think only “one layer deep” in the recursion and “trust the spec” so you don’t get overwhelmed tracking many recursive calls.

Remark:

In a way, we build up the repetitions in a recursive implementation in the "opposite order" than in iteration. Iteration starts from "nothing" and slowly builds toward the answer in each iteration. The loop invariant allows us to interpret how much progress has been made along the way. Recursion starts from the full problem and "peels back" some of it to get a slightly smaller problem to solve in a recursive call. In this way, the problem gets whittled down little by little until we reach a base case that is solved directly. Then, as the recursive calls are "unwound", these smaller answers are patched together to build the final answer.

Recursion on Arrays

Let’s consider another method that accepts an array as its parameter.

1
2
3
4
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static double maxValue(double[] nums) { ... }
1
2
3
4
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static double maxValue(double[] nums) { ... }

As review from last week, you should be comfortable developing a loop invariant and an iterative definition of this method. Let’s instead develop a recursive definition. To start, we’ll identify the correct notion of “larger” and “smaller” inputs. For our maxValue() function, the problem becomes more complicated when the length of the array is increased, as there will be more array entries to check. The base case should correspond to the smallest input, which (based on the method pre-condition) will be an array with a single element. In this case, that element, nums[0] can be directly returned.

For the recursive case, our definition should make a call to maxValue(), passing in an array with fewer elements. Perhaps we can “pull off” the first entry of nums to be left with a smaller array, which we’ll call sub, to use as the argument. We ask,

How would knowing the maximum value in sub help us compute the maximum value in nums?

Q: How would knowing the maximum value in sub help us compute the maximum value in nums?

A: Once we know this maximum value in sub, which we'll call m, we need only compare m to nums[0]. If nums[0] > m, nums[0] must be larger than every entry of sub, so must be the maximum value of nums. Otherwise, we know that m is at least as large as every element of nums, so it is the maximum value.

Before we can complete the method definition, we need to address one somewhat subtle point: how do we actually pass this “smaller array” to the recursive call? As a first thought, we could allocate a new, smaller array object, copy over all but the first entry of nums, and pass a reference to this new array object as a parameter. However, this will require excessive time and space to do. We’d be performing more work to make the recursive call than the entirety of the iterative solution (since making the copy requires a linear scan over the entire array). Unlike Python, Java does not have a convenient syntax for “slicing” an array, so we cannot use this (note, this is probably a good thing, since slicing is also doing this wasteful copying behind the scenes). We’ll need an alternate approach, which comes in the form of array views.

The idea of an array view is to pass into our recursive call the entire array (as an alias reference, so no large copying is necessary), along with some extra (or auxiliary) information about which elements are left to consider. In the case of our maxValue() function, our recursive case “pulls off” entries from the start of the array, so the extra information that is needed is the index of the first element that is “in view”. This auxiliary information gets passed in as a separate parameter, which we’ll call begin. Since we are adding a parameter, we’ll need a new method signature along with a modified spec that references begin.

1
2
3
4
5
/** 
 * Returns the maximum value in array `nums[begin..]`. Requires that 
 * `0 <= begin < nums.length`.
 */
static double maxValueRecursive(double[] nums, int begin) { ... }
1
2
3
4
5
/** 
 * Returns the maximum value in array `nums[begin..]`. Requires that 
 * `0 <= begin < nums.length`.
 */
static double maxValueRecursive(double[] nums, int begin) { ... }

Note that the pre-condition of this method enforces that nums is non-empty, since it implies that 0 < nums.length. Also, note that now we should think about the method slightly differently; it performs an operation on an array view, not an entire array. Armed with this spec, we have everything we need to develop the recursive method. Give it a try yourself before revealing the code. You can use the following questions to guide your development.

We said that the base case should correspond to an array with a single element. How does this condition translate to an array view? What should we check to know we are in the base case?

Q: We said that the base case should correspond to an array with a single element. How does this condition translate to an array view? What should we check to know we are in the base case?

A: Our array view will contain only a single element when begin is the index of the last array element or begin == nums.length - 1.

For the recursive call, we wanted to “pull off” the first element of nums. How do we translate this to modifying the array view of the recursive call?

Q: For the recursive call, we wanted to "pull off" the first element of nums. How do we translate this to modifying the array view of the recursive call?

A: We should pass begin + 1 as the second argument to the recursive call, as this has the effect of removing the first element of the current view.

View the final maxValueRecursive() code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** 
 * Returns the maximum value in array `nums[begin..]`. Requires that 
 * `0 <= begin < nums.length`.
 */
static double maxValueRecursive(double[] nums, int begin) { 
  assert 0 <= begin && begin < nums.length;
  if (begin == nums.length - 1) {
    return nums[begin];
  }
  double m = maxValueRecursive(nums, begin + 1);
  return Math.max(nums[begin],m);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** 
 * Returns the maximum value in array `nums[begin..]`. Requires that 
 * `0 <= begin < nums.length`.
 */
static double maxValueRecursive(double[] nums, int begin) { 
  assert 0 <= begin && begin < nums.length;
  if (begin == nums.length - 1) {
    return nums[begin];
  }
  double m = maxValueRecursive(nums, begin + 1);
  return Math.max(nums[begin],m);
}
Note: We used the static max() method provided in Java's Math class, but a similar thing could be accomplished with an if-statement or a conditional expression.

The auxiliary begin parameter is somewhat of a nuisance to the client of the maxValue() method. We intended to provide clients a method that takes in only an array argument, and we introduced an additional parameter only to help with our recursive formulation. Thus, it makes sense to have both the methods maxValue() and maxValueRecursive() since they perform different roles: maxValue() gives a nice interface to the client, and maxValueRecursive() has a more useful signature for us as the implementer. Our implementation of maxValue() simply delegates to maxValueRecursive(), supplying its additional parameter.

1
2
3
4
5
6
7
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static double maxValue(double[] nums) {
  assert nums.length > 0; 
  return maxValueRecursive(nums, 0);
}
1
2
3
4
5
6
7
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static double maxValue(double[] nums) {
  assert nums.length > 0; 
  return maxValueRecursive(nums, 0);
}

This pattern of having two versions of a method, the client-side version with a simple signature and the implementer-side version with an expanded signature, is somewhat common in Java.

Remark:

When we talk about encapsulation and access control modifiers in a few lectures, we'll see that it is natural to mark maxValue() as public and maxValueRecursive() as private.

Step through the following animation to trace through one execution of maxValue(). In the animation, we use shading to highlight the array view of the method that is currently being executed.

previous

next

Another Example

So far the two examples of recursive methods that we have developed might have seemed a bit trivial. Both of these methods have natural iterative implementations that are just as natural to write (and nearly as short). In addition, we’ll see that the computational overhead of recursion will actually make the iterative implementations preferable. In this next example, which also utilizes array views, we’ll see the true power of recursion to develop an “elegant” solution. It is a good challenge to think about how to formulate this method iteratively.

The method that we will write checks whether, given a certain collection of coins of different denominations, it is possible to make a given amount of change. Here are the signature and specifications for this method.

1
2
3
4
5
/**
 * Returns true if there is a subset of entries from `coins` whose sum is equal 
 * to `total`, otherwise returns `false`. 
 */
static boolean canMakeChange(int total, int[] coins) { ... }
1
2
3
4
5
/**
 * Returns true if there is a subset of entries from `coins` whose sum is equal 
 * to `total`, otherwise returns `false`. 
 */
static boolean canMakeChange(int total, int[] coins) { ... }

For example, if coins = {1, 10, 10, 25, 25} and total = 45, then canMakeChange() should return true because two dimes (10) and one quarter (25) suffice. Alternatively, if total = 40, then canMakeChange() should return false; it’s not possible to return 40 cents using these five coins. We use these examples, among others, to develop some test cases for this method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@DisplayName("WHEN there is a subset of `coins` summing to `total`, "
             + "THEN `canMakeChange()` returns `true`.")
@Test
void testChangeTrue() {
  assertTrue(canMakeChange(1, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(10, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(25, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(11, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(20, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(26, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(45, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(71, new int[]{1, 10, 10, 25, 25}));
}

@DisplayName("WHEN there is no subset of `coins` summing to `total`, "
             + "THEN `canMakeChange()` returns `false`.")
@Test
void testChangeFalse() {
  assertFalse(canMakeChange(2, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(5, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(31, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(40, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(55, new int[]{1, 10, 10, 25, 25}));
}

@DisplayName("WHEN `total` is 0, THEN `canMakeChange()` returns true "
             + "regardless of the contents of `coins`.")
@Test
void testChangeZero() {
  assertTrue(canMakeChange(0, new int[]{1, 5, 10}));
  assertTrue(canMakeChange(0, new int[]{10, 25, 25, 25}));
  assertTrue(canMakeChange(0, new int[]{1}));
}

@DisplayName("WHEN `coins` is empty, THEN `canMakeChange()` only "
             + "returns true if `total` is 0.")
@Test
void testChangeEmpty() {
  assertTrue(canMakeChange(0, new int[0]));
  assertFalse(canMakeChange(1, new int[0]));
  assertFalse(canMakeChange(12, new int[0]));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@DisplayName("WHEN there is a subset of `coins` summing to `total`, "
             + "THEN `canMakeChange()` returns `true`.")
@Test
void testChangeTrue() {
  assertTrue(canMakeChange(1, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(10, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(25, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(11, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(20, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(26, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(45, new int[]{1, 10, 10, 25, 25}));
  assertTrue(canMakeChange(71, new int[]{1, 10, 10, 25, 25}));
}

@DisplayName("WHEN there is no subset of `coins` summing to `total`, "
             + "THEN `canMakeChange()` returns `false`.")
@Test
void testChangeFalse() {
  assertFalse(canMakeChange(2, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(5, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(31, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(40, new int[]{1, 10, 10, 25, 25}));
  assertFalse(canMakeChange(55, new int[]{1, 10, 10, 25, 25}));
}

@DisplayName("WHEN `total` is 0, THEN `canMakeChange()` returns true "
             + "regardless of the contents of `coins`.")
@Test
void testChangeZero() {
  assertTrue(canMakeChange(0, new int[]{1, 5, 10}));
  assertTrue(canMakeChange(0, new int[]{10, 25, 25, 25}));
  assertTrue(canMakeChange(0, new int[]{1}));
}

@DisplayName("WHEN `coins` is empty, THEN `canMakeChange()` only "
             + "returns true if `total` is 0.")
@Test
void testChangeEmpty() {
  assertTrue(canMakeChange(0, new int[0]));
  assertFalse(canMakeChange(1, new int[0]));
  assertFalse(canMakeChange(12, new int[0]));
}

The last two tests check some corner cases of this method. Now, let’s think about how we can develop a recursive definition of canMakeChange(). In this case, the problem gets easier when there are fewer numbers in coins, since there will be fewer combinations of coins we need to check. Thus “smaller” inputs contain fewer coins to consider. The “smallest” input, corresponding to the base case, has no ints in coins, and our last test case tells us exactly how this should be handled.

We’re left to think about the recursive case. We ask ourselves, “How would knowing whether it’s possible to make a certain amount of change with fewer coins help us determine this for more coins?” Let’s think about our example with total = 45 and coins = {1, 10, 10, 25, 25}. The first coin we’ll consider is the penny (1). Either the penny will not be included in a good change subset, or it will. If it is excluded, then we’ll need to find a subset of the rest of the coins that sums to 45 cents. If it is included, then we’ll need to find a subset of the rest of the coins that sums to \(45-1 = 44\) cents. We can determine whether this is possible using a recursive call on the remaining coins {10, 10, 25, 25}. Again, we’ll do this using an array view, which will require an additional helper method.

1
2
3
4
5
6
/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) { ... }
1
2
3
4
5
6
/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) { ... }

Then, our original method definition simply becomes the method call

1
2
3
4
5
6
7
/**
 * Returns true if there is a subset of entries from `coins` whose sum 
 * is equal to `total`, otherwise returns `false`. 
 */
static boolean canMakeChange(int total, int[] coins) {
  return canMakeChangeRecursive(total, coins, 0);
}
1
2
3
4
5
6
7
/**
 * Returns true if there is a subset of entries from `coins` whose sum 
 * is equal to `total`, otherwise returns `false`. 
 */
static boolean canMakeChange(int total, int[] coins) {
  return canMakeChangeRecursive(total, coins, 0);
}

Take some time to develop canMakeChangeRecursive() using the ideas from above. Think about what condition will identify that we are in the base case and its return value. Then, think about to structure the recursive calls (hint: notice this is plural) and use their results to obtain the final return value. Once you have developed your solution, try coding it up and running the provided unit tests to see if it is correct.

View the final canMakeChangeRecursive() code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) {
  if (begin == coins.length) {
    return total == 0;
  }
  return canMakeChangeRecursive(total, coins, begin + 1) // don't use coin
    || canMakeChangeRecursive(total - coins[begin], coins, begin + 1); // use coin
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) {
  if (begin == coins.length) {
    return total == 0;
  }
  return canMakeChangeRecursive(total, coins, begin + 1) // don't use coin
    || canMakeChangeRecursive(total - coins[begin], coins, begin + 1); // use coin
}

For more practice, try drawing memory diagrams that trace the execution of your code on some small inputs. We will look at an example of something similar when we analyze the time and space complexities of this method.

Complexity of Recursive Methods

When analyzing the complexity of recursive methods, we need to account for the total time and space used across all of the recursive calls. To do this, we reason about the time/space complexity for the “non-recursive” parts of the method body (i.e., everything but the recursive calls), and then we reason about the structure of the recursive calls throughout the method’s execution. While this can be difficult in general and utilize math that is outside the scope of our course, the examples from this and upcoming lectures will provide a good overview.

Time Complexity

Let’s start by considering the time complexity of our factorial() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12;
  if (n <= 1) {
    return 1;
  } 
  int f = factorial(n - 1);
  return f * n;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Returns `n!`, the product of all positive integers less than or  
 * equal to `n`. Requires `0 <= n <= 12` (to prevent overflow).
 */
static int factorial(int n) {
  assert 0 <= n && n <= 12;
  if (n <= 1) {
    return 1;
  } 
  int f = factorial(n - 1);
  return f * n;
}
For the factorial() method, we perform \(O(1)\) operations within the method, and we make a single recursive call. In all, we will stack up these recursive calls factorial(n), factorial(n-1), etc. until we reach factorial(1), meaning a total of \(n\) calls will be made. We visualize these calls in a diagram on the right. Multiplying the \(O(1)\) time complexity per call by the \(O(n)\) calls gives an \(O(n)\) runtime.
Remark:

Technically, this runtime is expressed in terms of the value of the input rather than its size. This distinction is pretty subtle and discussed in more detail in CS 4820. For our purposes, we just want to be clear about how to interpret the parameters in our complexity classes (here n is the value passed into factorial()).

Remark:

What we are actually doing here (glossing over many of the details, which are beyond our scope) is developing a recurrence for the runtime complexity \(T(n)\); recurrences are a topic in CS 2800. The base cases of this recurrence are \(T(0) = T(1) = 1\), corresponding to the base cases of the method. The recurrence relation is \(T(n) = T(n-1) + O(1)\), which says that the amount of work done by factorial(n) is equal to the amount of work done by factorial(n-1) (the recursive call) plus the \(O(1)\) other work in the method. The closed formula satisfying this recurrence relation has asymptotic complexity \(O(n)\). You'll learn more about solving recurrences involving asymptotic complexities in CS 4820. In CS 2110, all the analyses of recursive methods we'll ask you to do will align closely with examples from the lecture notes.

Identical analyses also show that the maxValue() and maxValueRecursive() methods that we defined have an \(O(N)\) runtime, where \(N\) is nums.length; maxValueRecursive() makes a total of \(N - 1 = O(N)\) recursive calls, and \(O(1)\) work is done in each call. Note that if we used array copies as arguments rather than array views, we’d do \(O(N)\) non-recursive work in each invocation of maxValue(), resulting in an \(O(N^2)\) runtime.

The worst-case runtime analyses of canMakeChange() and canMakeChangeRecursive() are a little more involved. We’ll let \(N\) denote the length of the coins array. In each invocation of canMakeChangeRecursive(), we perform \(O(1)\) non-recursive work, so the runtime will be bounded by the maximum number of recursive calls that are made.

We’ll use the following animation to build up a visualization of the recursive call structure of this method, which we’ll use to bound the total number of recursive calls. Since we care about the worst-case runtime complexity, we deliberately chose a false instance, which ensures that we will not return early; every subset of coins must be checked. On the left of the visualization, we’ll show the parameters of each call, connected with arrows to show the path of execution. On the right, we’ll draw a diagram depicting the call frames. Each box represents a call frame labeled with its begin value. A call frame sits atop the call frame from which it was invoked.

previous

next

Let’s focus in on this final “call stack diagram” (not a technical term, but a nice descriptor):

We see that during the execution of canMakeChangeRecursive() with \(N = 3\), there is 1 call frame with begin = 0, 2 call frames with begin = 1, 4 call frames with begin = 2, and 8 call frames with begin = 3. In total, there are 15 call frames. More generally, this diagram will have \(N+1\) levels, and the level corresponding to begin = i will contain \(2^i\) call frames. Adding the number of call frames row-by-row from the bottom of this diagram, we see that the total number is \(1 + 2 + \dots + 2^N = 2^{N+1} - 1 = O(2^N)\). The \(O(1)\) non-recursive work done in each call frame results in an exponential \(O(2^N)\) runtime.

Space Complexity

Next, we’ll analyze the space complexity of our recursive methods. There are things that contribute to the total amount of memory allocated during a recursive method call.

  1. The local variables and objects that are allocated during the execution of the method.
  2. The memory space collectively occupied by the call frames that exist simultaneously on the runtime stack.

We must also remember the subtlety of space complexity: we cannot simply add up the size of all of the memory allocations, since some memory may be deallocated and then reused at a later point in the execution. This will be important for our analysis of canMakeChangeRecursive(). Before we get there, let’s analyze the space complexity of the factorial() and maxValueRecursive() methods.

Outside of the stack frames allocated by their recursive calls, each invocation of these methods uses only \(O(1)\) outside of their initial parameters. Note that neither method constructs any objects; the array nums is passed as a reference which requires \(O(1)\) space. Looking at our animations of these methods, we see that both of them create all of their stack frames before the first one completes its execution. Thus, there is a point when all \(O(N)\) stack frames are simultaneously allocated, and this gives rise to the overall \(O(N)\) space complexity of these methods.

Remark:

The additional memory used to allocate multiple call frames and keep track of our "location" within the recursive execution (and, to a lesser extent, the "wall-clock" runtime required to create and navigate these call frames) is the primary drawback of recursion versus iteration. Often, a noticeable performance boost can be achieved by re-writing recursive code to instead use iteration. Compilers can even do some of this re-writing when the recursion is structured in a nice way. If you are interested in learning more about this, look up "tail recursion" and "tail-call optimization."

Now, let’s consider the space complexity of canMakeChangeRecursive(). Recall that over the course of execution, \(O(2^N)\) call frames are created. However, these call frames are not all active at the same time. As shown in the animation, the two recursive calls in each method invocation are made sequentially; the first one returns and its frame is deallocated from the stack before the second one is invoked. There is at most one active call frame for each value of begin, as shown in our “call stack diagram”. We say that this recursive method has \(O(N)\) depth, since this bounds the number of active call frames.

Definition: Recursion Depth

The depth of a recursive method is the maximum number of its call frames that ever exist simultaneously on the runtime stack.

Therefore, the overall space complexity of canMakeChangeRecursive() is \(O(1) \cdot O(N) = O(N)\).

Main Takeaways:

  • Recursive methods call themselves. When writing a recursive method, identify the base cases and directly implement their solutions. Then, rely on the method spec to incorporate the return values of recursive calls into the larger solution.
  • When developing recursive methods on arrays (and other data structures) we can avoid unnecessary copies by adding auxiliary parameters to the method signature that describe a view of the array.
  • Often, it is better to develop recursive methods in a separate (private) helper method which have extra, useful parameters. The (public) method has a nicer signature and supplies the extra parameters to the helper method to start the recursion.
  • The time complexity of a recursive method depends on the total number of recursive calls that are made during its execution. The space complexity of a recursive method depends on the maximum depth of the runtime stack (i.e., the number of active call frames) during its execution.

Exercises

Exercise 6.1: Check Your Understanding
(a)
When writing solely a recursive method, what role(s) do you serve?
Check Answer
(b)

The Collatz function on the positive integers is defined by the formula

\[ \textrm{collatz}(n) = \begin{cases} n / 2 & n \textrm{ is even,} \\ 3n + 1 & n \textrm{ is odd.} \end{cases} \]

It is famously conjectured that by repeatedly applying the Collatz function, every positive integer will eventually be reduced to 1. We have verified the conjecture past \(n = 1e21\), but a complete proof is unknown. Consider the following recursive method that prints out a Collatz sequence starting from a given value \(n\).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * Prints the Collatz sequence starting from `n` and ending at the first 
 * occurrence of 1. Requires `1 <= n <= 1e9`.
 */
static void collatz(int n) {
  if (n == 2) {
    System.out.println(2);
    System.out.println(1);
    return;
  }
  System.out.println(n);
  if (n % 2 == 0) {
    collatz(n / 2);
  } else {
    collatz(3 * n + 1);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * Prints the Collatz sequence starting from `n` and ending at the first 
 * occurrence of 1. Requires `1 <= n <= 1e9`.
 */
static void collatz(int n) {
  if (n == 2) {
    System.out.println(2);
    System.out.println(1);
    return;
  }
  System.out.println(n);
  if (n % 2 == 0) {
    collatz(n / 2);
  } else {
    collatz(3 * n + 1);
  }
}
Which of the following are issues with the recursive method collatz()?
Check Answer
(c)

Consider the following recursive method.

1
2
3
4
5
6
7
static int f(int[] a, int i) {
  if (i >= a.length) {
    return 0;
  }
  a[i] += f(a, 2 * i + 1) + f(a, 2 * i + 2);
  return a[i];
}
1
2
3
4
5
6
7
static int f(int[] a, int i) {
  if (i >= a.length) {
    return 0;
  }
  a[i] += f(a, 2 * i + 1) + f(a, 2 * i + 2);
  return a[i];
}
Suppose we evaluated f(new int[]{1,4,9,5,28,15,11}, 0). What is the largest number of call frames for f() that are simultaneously on the stack during the evaluation? Include the frame for the original call itself.
Check Answer
How many total call frames for f() are created and destroyed during the evaluation? Include the frame for the original call itself.
Check Answer

15

What is the time complexity of f() in terms of \(N\) = a.length?
Check Answer
What is the space complexity of f() in terms of \(N\) = a.length?
Check Answer
Exercise 6.2: Identifying Base and Recursive Cases
Given the following method headers, identify base and recursive cases that could be used to implement the method recursively.
(a)
1
2
3
4
5
6
7
8
9
/** 
 * Returns the `n`th Fibonacci number F(n), where the Fibonacci 
 * sequence F is defined:
 *  F(0) = 0
 *  F(1) = 1
 *  F(n) = F(n-1) + F(n-2) for n > 1
 * Requires `n >= 0`.
 */
static int fibonacci(int n);
1
2
3
4
5
6
7
8
9
/** 
 * Returns the `n`th Fibonacci number F(n), where the Fibonacci 
 * sequence F is defined:
 *  F(0) = 0
 *  F(1) = 1
 *  F(n) = F(n-1) + F(n-2) for n > 1
 * Requires `n >= 0`.
 */
static int fibonacci(int n);
(b)
1
2
3
4
5
/** 
 * Returns `n` to the `m`th power, calculated using multiplication 
 * and recursion. Requires `m >= 0`.
 */
static int pow(int n, int m);
1
2
3
4
5
/** 
 * Returns `n` to the `m`th power, calculated using multiplication 
 * and recursion. Requires `m >= 0`.
 */
static int pow(int n, int m);
(c)
1
2
3
4
/** 
 * Returns the sum of the digits in `n`. Requires `n >= 0`.
 */
static int sumDigits(int n);
1
2
3
4
/** 
 * Returns the sum of the digits in `n`. Requires `n >= 0`.
 */
static int sumDigits(int n);
(d)
1
2
3
4
/** 
 * Returns the string with the characters of `s` in reverse order.
 */
static String reverse(String s);
1
2
3
4
/** 
 * Returns the string with the characters of `s` in reverse order.
 */
static String reverse(String s);
Exercise 6.3: Implementing Recursive Methods
Implement the following recursive methods.
(a)
Implement fibonacci(), pow(), sumDigits() and reverse() from the above exercise.
(b)
1
2
3
4
5
/**
 * Returns whether `s` is a *palindrome*, meaning `reverse(s).equals(s)`. 
 * Implement this method *without* using the `reverse()` method.
 */
static boolean isPalindrome(String s);
1
2
3
4
5
/**
 * Returns whether `s` is a *palindrome*, meaning `reverse(s).equals(s)`. 
 * Implement this method *without* using the `reverse()` method.
 */
static boolean isPalindrome(String s);
(c)

For the following exercise, recall the lecture method canMakeChange().

1
2
3
4
5
/**
 * Returns the number of ways to produce `total` cents using the available 
 * denominations in `coins`. 
 */
static int countWaysToMakeChange(int s, int[] coins);
1
2
3
4
5
/**
 * Returns the number of ways to produce `total` cents using the available 
 * denominations in `coins`. 
 */
static int countWaysToMakeChange(int s, int[] coins);
(d)

Use a recursive helper method to implement this method.

1
2
3
4
5
6
7
/**
 * Returns true if the elements of `sub` appear in the same order within 
 * `array`, possible with some gaps, and false otherwise. 
 * For example, if `array = {3, 9, 5, 9, 8, 10}`, inputting `{3, 9, 9, 10}` as 
 * `sub` returns true while inputting `{3, 8, 5, 10}` as `sub` returns false.
 */
static boolean containsSubarray(int[] array, int[] sub);
1
2
3
4
5
6
7
/**
 * Returns true if the elements of `sub` appear in the same order within 
 * `array`, possible with some gaps, and false otherwise. 
 * For example, if `array = {3, 9, 5, 9, 8, 10}`, inputting `{3, 9, 9, 10}` as 
 * `sub` returns true while inputting `{3, 8, 5, 10}` as `sub` returns false.
 */
static boolean containsSubarray(int[] array, int[] sub);
(e)

Binary numbers are represented in base-2, meaning they use only the digits 0 and 1. Each position in a binary number corresponds to a power of 2, just as each position in a decimal number corresponds to a power of 10. For instance, 0b1011 (0b tells us the number is in binary, which, for the purposes of this exercise, do not need to be included in the final String) is equal to \( 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 1 \times 2^0 = 11\).

1
2
3
4
/**
 * Returns the binary equivalent of `n` as a string.
 */
static String toBinary(int n);
1
2
3
4
/**
 * Returns the binary equivalent of `n` as a string.
 */
static String toBinary(int n);
(f)
1
2
3
4
5
/**
 * Returns the integer represented by the binary string `s`.
 * Requires `s` is a binary string consisting only of the characters '1' and '0'.
 */
static int fromBinary(String s);
1
2
3
4
5
/**
 * Returns the integer represented by the binary string `s`.
 * Requires `s` is a binary string consisting only of the characters '1' and '0'.
 */
static int fromBinary(String s);
(g)
Implement pow() from 6.2.b in \(O(\log m) \) time.
Exercise 6.4: Call Structure with Short-Circuiting
In Java, the logical operator || is short-circuiting. When a boolean expression of the form expr1 || expr2, for some boolean expressions expr1 and expr2, is evaluated, Java starts by evaluating expr1. If it finds that expr1 is true, then it skips the evaluation of expr2, since it knows that the logical-or of the two expressions will be true regardless of the value of expr2. When we account for logical short-circuiting, this can affect the call structure of a recursive-method. For example, consider the following alternate version of our canMakeChange() method that can return early before all the coins have been examined:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) {
  if (total == 0) {
    return true;
  } else if (begin == coins.length) {
    return false; 
  }
  return canMakeChangeRecursive(total, coins, begin + 1) // don't use coin
    || canMakeChangeRecursive(total - coins[begin], coins, begin + 1); // use coin
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

/**
 * Returns true if there is a subset of entries from `coins[begin..]` 
 * whose sum is equal to `total`, otherwise returns `false`. Requires
 * that `0 <= begin <= coins.length`.
 */
static boolean canMakeChangeRecursive(int total, int[] coins, int begin) {
  if (total == 0) {
    return true;
  } else if (begin == coins.length) {
    return false; 
  }
  return canMakeChangeRecursive(total, coins, begin + 1) // don't use coin
    || canMakeChangeRecursive(total - coins[begin], coins, begin + 1); // use coin
}
For each of the following invocations, how many total calls to canMakeChange() are made? What is the maximum recursive depth that is reached?
(a)
canMakeChange(10, new int[]{5,5,10})
(b)
canMakeChange(10, new int[]{1,5,5,10})
(c)
canMakeChange(10, new int[]{5,1,5,10})
(d)
canMakeChange(16, new int[]{1,1,5,5,10})
Exercise 6.5: Print Permutations
Implement a method that, when given an array of integers, prints every permutation of that array. A permutation is an array with the same elements, possibly in a different order. For example, the permutations of the array {1, 2, 3} are {1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, and {3, 2, 1}.

What are the space and time complexities of this method?
Exercise 6.6: Print Subsequences
Implement a method which given an array of integers array, print each subsequence of array. A subsequence of an array is a new sequence derived from the original array by deleting zero or more elements without changing the relative order of the remaining elements. For example, given the input {1, 2, 3}, the method should print each element in the array {{}, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}}.

What are the space and time complexities of this method?
Exercise 6.7: Tail Recursion
We say that a method is tail recursive if its recursive call is made as the very last operation before the return value. We like tail recursion since it is typically easier to "unroll" into an iterative implementation, which we know has improved performance and space complexity. In fact, some sophisticated compilers can detect tail-recursive code and automatically carry out this transformation. Recall the maxValue() method, which has space complexity of \(O(N)\). Here, you'll develop a tail recursive variant of this method.
(a)

Implement maxValueHelper() method, making sure to leverage tail recursion. This means that your method should not be doing any extra work after making its recursive call.

1
2
3
4
5
/** 
 * Returns the maximum value among the elements in `nums[begin..]` and the 
 * given value `acc`. Requires that `0 <= begin < nums.length`.
 */
static int maxValueHelper(int[] nums, int begin, int acc) { ... }
1
2
3
4
5
/** 
 * Returns the maximum value among the elements in `nums[begin..]` and the 
 * given value `acc`. Requires that `0 <= begin < nums.length`.
 */
static int maxValueHelper(int[] nums, int begin, int acc) { ... }
(b)

Use the maxValueHelper() method to give a tail recursive implementation of maxValue(). (You should be able to accomplish this with a single method call.)

1
2
3
4
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static int maxValueTailRecursive(int[] nums) { ... }
1
2
3
4
/** 
 * Returns the maximum value in array `nums`. Requires that `nums` is non-empty.
 */
static int maxValueTailRecursive(int[] nums) { ... }
(c)
Translate your implementation into an iterative approach. Rather than making a recursive call, update the values of the parameters in a loop whose guard evaluates to false once a base case is detected.
(d)
Describe the loop invariant for this iterative implementation.
(e)
Determine the time and space complexities of this iterative implementation.
When a compiler can perform automatic tail-call optimization, this allows us to use the expressive elegance of recursion while gaining the performance advantages of iteration.