Version: 0.3.38

2. Recursion#

Lecture:

Lecture 3.2 (slides)

Objectives:

Understand what is recursion, how to use it, and how measure efficiency of recursive algorithms

Concepts:

Recursion, recursive procedure, recursive data type

In Lecture 3.1, we have clarified what takes place at the machine level when one calls a procedure. We can now look at recursion, one of the most important concept in Computer Science 1If the topic picks your curiosity, there are many goods textbooks on recursion. See for instance: Rubio-Sánchez, Manuel (2017). Introduction to recursive programming. CRC Press. . We will look at two usage of recursions: Recursion in data types and recursion in algorithms.

2.1. What is Recursion?#

Recursion, from a general angle, occurs when something is defined in terms of itself. Another way to put this is that a whole is self-similar to its own parts. Recursion is a very general concept that surfaces in various domains, from nature to Mathematics, via Computer Science.

Nature offers many example of recursive structures. See for instance the Nautilus shell aside, whose structure repeats itself inside a spiral at smaller and smaller scale. The shell contains an smaller similar shell, which contains and even smaller shell, etc. Trees also have a recursive structure: Each branch splits itself into more branches, which also splits themselves into even more branches, etc.

Such recursive patterns generates interesting effects in visual Arts. Think for instance of Russian Dolls, which contains another smaller yet similar dolls. Another example is the Droste effect where an image contains itself. The key point is these self-similar inner parts.

In Mathematics recursion takes many forms. Fractals for instance, are complex geometrical patterns that are self-similar at different scales, as illustrated by Fig. 2.14. Another form of recursion—a simpler one—is recurrence relationships. Equation (2.1) defines the factorial function \(n! = n \times n-1 \times \ldots \times 1\) as a recurrence. That is to compute \(n!\), one must first calculate \((n-1)!\)

(2.1)#\[\begin{split}n! = \begin{cases} 1 & \textrm{if} \; n=1 \\ n \times (n-1)! & \textrm{otherwise}\\ \end{cases}\end{split}\]

In this course, we will focus on two key recursion: Recursion in data types, where a compound data type refer to itself, and recursion in algorithms, where an algorithm calls itself.

Important

Recursion is about self-similar structures

2.2. Recursive Data Types#

Recursion is about self-similarity. Applied to data types, that yields types that refer to themselves. We will study two examples: Lists and Trees. Each comes in many variations.

2.2.1. Lists#

List is a data structure that we can use to implement the Sequence ADT, as an alternative to arrays. The idea is to see a sequence as an element followed by another sequence. Listing 2.6 shows how one can declare such a data type in Java 2In Java the classical way to implement a record would be to create a class. Since version 14, Java offers the concept of record, but these are immutable. . We create a class List, with two fields. The field item contains a object of an arbitrary type T, whereas the field next holds another list.

Listing 2.6 A simple list, implemented as a Java class. The next attribute is typed with the class itself.#
 class List<T> {
     T item;
     List<T> next;
 }

This declaration yields structures that closely resemble Fig. 2.15. Each list (the dashed boxes) points to another list. This “chain” of objects stops when a list has null as next field.

../_images/lists.svg

Fig. 2.15 The recursive list structure: A list is an item followed by another list.#

2.2.2. Trees#

Tree is the other family of recursive data types we will look at. Just like a list, a tree carries an item, but, by contrast to the list, a tree has zero or more “next” trees—so called its children.

Listing 2.7 A basic tree data type: Each tree has many children#
class Tree<T> {
   T item;
   Sequence<Tree<T>> children;
}

The declaration on Listing 2.7 yields structure like the one shown in Fig. 2.16. Each item has links to zero, one, or more trees. The whole looks like tree up-side down, or like the roots of a tree.

../_images/trees.svg

Fig. 2.16 The recursive structure of trees: Each tree is made of smaller trees#

2.3. Recursive Algorithms#

Data types are not however the only use of recursion in Computer Science. We can also make recursive algorithms:

Consider the two formulas shown by Table 2.1. They both sum up the \(n\) first integers, but the left one uses a summation whereas the right one uses a recurrence relation. From an algorithmic standpoint that yields two alternative algorithms. On the left side, we use a loop that updates the intermediate variable sum. On the right side, we use a recursive procedure that mirrors the recurrence relation. Both compute the very same thing, but their efficiency are different.

Table 2.1 Two alternative algorithms to sum up the \(n\) first integers#
  1. Using Iteration

  1. Using Recursion

\[s_1(n) = \sum_{i=1}^{n} i\]
\[\begin{split}s_2(n) = \begin{cases} 1 & \textrm{if } n = 1 \\ n + s_2(n-1) & \textrm{otherwise} \\ \end{cases}\end{split}\]
int s1 (int n) {
    int sum = 0;
    for (int i=1 ; i<=n ; i++) {
        sum += i;
    }
    return sum;
}
int s2 (int n) {
    if (n == 1) return 1;
    return n + s2(n-1);
}

When designing algorithms, recursion is an alternative to iteration. Iteration implies the use of a loop, and some necessary intermediate variables. By contrast, recursion yields algorithms that invoke themselves different arguments.

Important

Recursion is strictly as expressive as iteration. Any algorithms using a loop has an equivalent recursive version, and vice versa.

2.3.1. How to Design “Recursive” Algorithms?#

Designing recursive algorithms boils down to detecting self-similar patterns. In general we will try to follows these steps:

  1. Find a self-similar sub problems / sub-structures ;

  2. Identify the base cases, whose answer is known up front ;

  3. Identify the recursive cases, where we call our algorithms with different arguments.

2.3.1.1. Example 1:#

Consider the sum of the first \(n\) integers we studied above. How can we come up with such a design? Fig. 2.17 portrays such a sum for \(n=8\). The problem is to count the squares. Note that the overall shape forms a triangle. If we remove the first bar on the left hand side, we are left with another smaller triangle.

Base cases:

When do we stop? When the \(n=1\), there is no more left hand side bar to be taken, and the result is one.

Recursive case:

The height of bar on the left, is the number we start from, \(n\). So the overall sum is n plus the size of the remaining triangle.

That gives us the following algorithm:

int sum(int n) {
   if (n == 1) return 1;   // base case
   return n + sum(n-1);    // Recursive case
}

2.3.1.2. Example 2#

What other self-similar patterns can we find? If we take out the left bar and the bottom line, we are also left with a smaller triangle, as shown on Fig. 2.18. These two together account for \(n + (n-1) = 2n-1\).

Base cases:

What are the case cases? There are two. As before, when the given number if 1 one, we still know the answer is 1. However, if \(n=2\) there is no left-over triangle to add and the answer is three.

Recursive cases:

What are the recursive cases? The sum of a triangle is the left and bottom bars, plus the “left-over” triangle.

That gives us the following algorithm

int sum(int n) {
   if (n <= 1) return 1;       // Base case #1
   if (n == 2) return 3;       // Base case #2
   return 2*n - 1 + sum(n-2);  // Recursive case
}

2.3.1.3. Example 3#

What other self-similar patterns can we find? We can break a triangle into smaller ones, as shown on Fig. 2.19. We break the large triangles into 3 triangles of size \(\frac{n}{2}\) and a smaller one in the middle.

Base cases:

When do we stop. If \(n =2\) this decomposition into triangles does not work anymore, but we know the sum is 3 in that case. Similarly, if \(n=1\), then the sum is 1.

Recursive cases:

Here we have to be more careful, because the sizes of the triangle depends on whether \(n\) is odd or even. When \(n\) is even, then it yields 3 triangles of size \(\frac{n}{2}\) and one triangle of height \(\frac{n-2}{2}\). If \(n\) is odd, it yields 3 triangles of height \(\frac{n-1}{2}\) plus one triangle of height \(\frac{n+1}{2}\).

That gives us the following recursive algorithm:

int sum(int n) {
    if (n <= 1) return 1;                        // Base case #1.
    if (n == 2) return 3;                        // Base case #2
    if (n % 2 == 0) {
       return 3 * sum(n/2) + sum((n-2)/2);       // Recursive case #1
    } else {
       return 3 * sum((n-1)/2) + sum((n+1)/2);   // Recursive case #2
    }
}

Important

Thinking “recursive” is a matter of practice. That said, there are some general steps:

  1. Break down the problem into self-similar sub problems. Look at the underlying data structure if any, is it recursive?

  2. Identify the base cases, that is, the cases that we can solve directly, without recursion.

  3. Work out the recursive cases, those that requires solving self-similar sub problems. What parameters do we need to pass?

2.3.2. Runtime Efficiency#

How can we measure the efficiency of recursive algorithms? The main difference is that the calculus requires solving a recurrence relationship, which captures the recursive nature. Let see an example.

Consider again our first algorithm that sums the \(n\) first integers, which I reproduce opposite.

Table 2.2 Breaking down the runtime efficiency of a recursive sum#

Line

Code

Cost

Runs

Total

2

if (n == 1)

1

1

1

3

return 1

1

?

?

4

return n + sum(n-1)

2 + ?

?

?

Our approach does not help much here. We have two challenges:

  • We do not know whether we will enter the conditional statement

  • To know the time spent computing sum(n), we need to know the time spent computing sum(n-1).

To work around these, we have to model the runtime as a recurrence relationship \(t(n)\) as follows. Let consider the base case and the recursive case separately. In the base case, we evaluate the conditional and we return a value. The total cost for that is 2. In the recursive case, we still evaluate the conditional, we compute n-1, compute sum(n-1), and add n. That is a total of \(3 + t(n-1)\). We write down this recurrence as follows:

\[\begin{split}t(n) = \begin{cases} 2 & \textrm{if } n = 1 \\ 3 + t(n-1) & \textrm{otherwise} \end{cases}\end{split}\]

Important

Modeling the runtime of recursive algorithms often requires using a recurrence relationship.

2.3.3. Memory Efficiency#

What about memory consumption? As we saw in the previous lecture, procedure calls consume memory via the call stack. Since recursion relies on procedure calls, recursive algorithms consume more memory.

Consider again the first version of the sum of the \(n\) first integers. How much memory does that consume? As we did for the runtime efficiency, we have to model this using a recurrence relationship. Let’s look at the base cases and the recursive cases separately:

  • The base case, there is no variable besides the input n. The memory consume is 0.

  • In the recursive case, there is no additional variable either, but there is a procedure call. This procedure call requires storing the arguments onto the call stack. Here we have one argument n, so that is a cost of 1.

That gives us the following recurrence relationships:

\[\begin{split}m(n) = \begin{cases} 0 & \textrm{if } n = 1 \\ 1 + m(n-1) & \textrm{otherwise} \end{cases}\end{split}\]

Which reduces to \(m(n) = n-1\). Interestingly, this shows that such a simple recursive algorithm requires memory in quantity that is proportional to the size of its input!

Important

Recursive algorithms consume (in general) significantly more memory than their iterative equivalent, because of the underlying call stack, which grows as the recursion deepens.