14A. Understanding Recursion


Recursive thinking

Here are a few hints that help you write recursive function definitions.

  1. Always have a clear and precise contract before you embark on writing a definition of a recursive function. Put this contract into your toolbox right away so that you can consider using the function that you are writing.

  2. Break the problem down into cases. Solve easy cases in appropriate ways. For more complicated cases, try to break the problem down into pieces.

  3. When solving the pieces of more complicated cases, imagine that the function that you are working on is already available, and assume that it works according to its contract. Do not think about how the recursive calls work. The contract is all you need.


Hand simulation of recursion 2: avoiding unnecessary details

We have seen scan algorithms solved using loops. Here is the sum function expressed using recursion instead.

  //===============================================
  // sum(A,n) returns A[0] + A[1] + ... + A[n-1].
  //===============================================

  int sum(const int A[], const int n)
  {
    if(n == 0)
    {
      return 0;
    }
    else
    {
      return sum(A, n-1) + A[n-1];
    }
  }

That is still a scan algorithm, just looked at from another angle. Hand simulation of sum(A, 0) should be clear: the sum of no numbers is 0. Here is a simulation sum(A, n) where n > 0.

sum(A, n) = sum(A, n−1) + A[n−1]  from the definition of sum(A, n)
= (A[0] + A[1] + … + A[(n−1)−1]) + A[n−1]  because sum(A, n−1) is assumed to work
= A[0] + A[1] + … + A[n−1]  

Notice that the hand simulation assumes that sum(A, n−1) yields the correct answer, according to the contract for sum. That tells us that sum(A, n) respects its contract. (It does what its contract says it does.)


Making sure that recursion stops

A danger with recursion is that your function might not ever produce an answer. As an extreme example, suppose that you want to define a function f(n). Since you imagine that f is available to you and that it works, you write the definition as follows.

  int f(int n)
  {
    return f(n);
  }
But f(2) calls f(2). That call does another call to f(2). Each call involves creating a frame in the run-time stack. The frames will just pile up until you run out of memory, leading to an infinite recursion.

To avoid infinite recursion, do the following.

  1. Make sure that, if f(n) calls f(x), then x is smaller than n. For example, there is no trouble if f(2) calls f(1).

  2. Make sure that the parameter cannot keep getting smaller forever. For example, if the parameter must be a nonnegative integer, then you know that it cannot keep getting smaller and smaller without end.

  3. Be sure that your recursive calls respect the requirements of your function. For example, if the function requires its parameter to be a nonnegative integer, be sure that every call to the function, including recursive calls, pass it a nonnegative integer.

If a function has more than one parameter, you typically concentrate on one of the parameters, and make sure that it is smaller at the recursive calls, and cannot keep getting smaller forever.


Summary

The key to understanding recursion is to assume that recursive calls work according to the function contract. Hand simulation of a recursive function avoids doing hand simulation of recursive calls.

You need to be sure that your function does not go into an infinite recursion. If f (n) calls f (x), where the parameter of f  is a nonnegative integer, then be sure that x < n. If f  has more than one parameter, make sure that a similar rule is true for one of the parameters.

There are variations. For example, a recursive definition of f (a, b) might call f (a+1, b) when a < b. Then the difference, ba, is smaller than b − (a+1). If a parameter is a string, the string can be smaller in a recursive call.


Exercises

  1. Using recursion, write a definition of function reverse(A, a, b), which reverses the order of the A[a], … A[b], where A is an array of ints. This function changes what is in array A.

    Hint. Draw a picture of an example array. If a < b, so there are at least two values in A[a], … A[b], consider swapping the contents of A[a] and A[b]. Do that in your picture. Now, what do you need to do to finish the job?

    If there are fewer than two values, what do you need to do?

    Answer

  2. Computers use base 2 (binary) to represent numbers.

    Write a function that takes a nonnegative integer argument n and writes the binary representation of n on the standard output. For example, if n is 6 it writes 110 and if n is 13 it writes 1101.

    Hints.

    1. If n < 2 then the binary representation of n is just n.

      For n ≥ 2, you will want to work at the low-order end of a number. The binary representation of n is the binary representation of n/2 followed by n%2. For example, if bin(n) is the binary string that represents integer n, then

        bin(1)  = "1".
        bin(3)  = "11"    = bin(1)  followed by 1.
        bin(6)  = "110"   = bin(3)  followed by 0.
        bin(12) = "1100"  = bin(6)  followed by 0.
        bin(25) = "11001" = bin(12) followed by 1.
      

      Notice that you are not asked to define bin(n), and you are do not need to create strings. Your function should write the binary number to the standard output.

    2. If you use those ideas with a loop, you will get the bits in reverse order. The easy way to handle that is to use recursion.

    Answer

  3. Write a complete program that reads a decimal integer and writes the equivalent binary integer. Both the input and the output are in standard order, starting with the highest-order digit. The input comes from the standard input and the output goes to the standard output.

    Use type long for the integers.

    Answer