16. 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

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]  

That tells us that sum(A, n) respects its contract. (It does what its contract says it does.)


SumPrimes

Here is an implementation of sumPrimes, previously implemented using a loop. Because of the filtering, there is one more case.

  //==============================================
  // sumPrimes(n) yields the sum of the prime
  // numbers that are less than or equal to n.
  // For example, sumPrimes(5) = 2 + 3 + 5 = 10.
  //==============================================

  int sumPrimes(const int n)
  {
    if(n < 2)
    {
      return 0;
    }
    else if(isPrime(n))
    {
      return sumPrimes(n-1) + n;
    }
    else
    {
      return sumPrimes(n-1);
    }
  }

Thought process.

  1. The smallest prime number is 2. When n < 2, the sum of the prime numbers up to n is 0.

  2. Suppose n is prime. For example, look at n = 11. By the contract for sumPrimes,

      sumPrimes(11) = 2 + 3 + 5 + 7 + 11,
      sumPrimes(10) = 2 + 3 + 5 + 7.
    
    Clearly, sumPrimes(11) = sumPrimes(10) + 11. When n is prime, n gets added into the sum.

  3. Suppose n is not prime. For example, look at n = 10. By the contract for sumPrimes,

      sumPrimes(10) = 2 + 3 + 5 + 7,
      sumPrimes(9)  = 2 + 3 + 5 + 7.
    
    Clearly, sumPrimes(10) = sumPrimes(9). When n is not prime, n does not get added into the total.


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.


ConvertToInt

This example uses arrays of characters, called null-terminated strings. If you understand arrays, you should be able to understand this example.

ConvertToInt(s), previously implemented with a loop, introduces a new issue. Using only what we know now about arrays, it is not possible to write convertToInt directly. We need a helper function with two parameters. Later, when we look at arrays in more detail, we will see how to write convertToInt directly using recursion.

Library function strlen(s) returns the length of null-terminated string s. For example, strlen("abc") yields 3. Include <cstring> to use strlen. (Note: Earlier, we said that you cannot ask an array how large it is. But strlen(s) does not really tell how large array s is. It tells the index of the first null character in s, and that is easy to do with a loop.)

  //===============================================
  // convertToInt2(s, n) yields the result of
  // converting the first n characters of s to an
  // integer. s must be a null-terminated string.
  //
  // For example, 
  //   convertToInt2("12345", 5) returns 12345,
  //   convertToInt2("12345", 4) returns 1234,
  //   convertToInt2("12345", 3) returns 123,
  //   convertToInt2("12345", 2) returns 12,
  //   convertToInt2("12345", 1) returns 1,
  //   convertToInt2("12345", 0) returns 0.
  //
  // Requirement: s must be a null-terminated string
  // that only contains decimal digits.
  //===============================================

  int convertToInt2(const char s[], const int n)
  {
    if(n == 0)
    {
      return 0;
    }
    else 
    {
      int lastDigit = s[n-1] - '0';
      return 10*convertToInt(s, n-1) + lastDigit;
    }
  }

  //===============================================
  // convertToInt(s) yields the integer described
  // by string s. For example, convertToInt("325")
  // = 325.
  //
  // Requirement: s must be a null-terminated string
  // that only contains decimal digits.
  //===============================================

  int convertToInt(const char s[])
  {
    return convertToInt2(s, strlen(s));
  }

Let's do a hand simulation of convertToInt("51449") using the rule that recursive calls are assumed to work correctly.

convertToInt("61449") = convertToInt2("61449", 5)
= 10*convertToInt2("6144", 4) + 9
= 10*6144 + 9  according to the contract for convertToInt2
= 61449

The simulation shows that convertToInt("61449") = 61449, which is the correct answer.


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 reverse(A, a, b), which reverses the order of the A[a], … A[b], where A is an array of ints A. This function changes what is in array A. Answer

  2. 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 > 1, 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 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