In nature and mathematics, you can find the topic self-similarity or recurring structures, such as snowflakes, fractals, and Julia sets, which are interesting graphical formations. In this context, one speaks of recursion, meaning that things repeat or resemble each other. Related to methods, this means that they call themselves. Important therefore is a termination condition in the form of special input values, which leads to the end of the self calls.
3.1 Introduction
Various computations can be described as recursive functions. The goal is to break down a more complex task into several simpler subtasks.
3.1.1 Mathematical Examples
Below you will take a look at the computation of the factorial, summation, and Fibonacci numbers, three introductory examples for recursive definitions.
Example 1: Factorial
Mathematically, the factorial for a positive number n is defined as the product (i. e., the multiplication) of all natural numbers from 1 to n, inclusive. For notation, the exclamation mark is placed after the corresponding number. For example, 5! stands for the factorial of the number 5:
5! = 5 ∗ 4 ∗ 3 ∗ 2 ∗ 1 = 120
This can be generalized as follows:
n! = n ∗ (n − 1) ∗ (n − 2) ∗ ... ∗ 2 ∗ 1
Here, the inverted »A« (∀) denotes for all.
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
n! | 1 | 2 | 6 | 24 | 120 | 720 | 5040 | 40320 |
Please note that lambdas usually encapsulate only a tiny piece of functionality and thus you should not name them and assign them to a variable. In this book, I sometimes break this PEP-8 rule for a better insight into how things work or to express more clearly what was intended.
Example 2: Calculation of the Sum of Numbers Up to n
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
sum_of(n) | 1 | 3 | 6 | 10 | 15 | 21 | 28 | 36 |
Example 3: Fibonacci Numbers
n | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
fib(n) | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
It should furthermore be noted that there is a variation that starts at the value of 0. Then fib(0) = 0 and fib (1) = 1 are the base values and afterwards you get fib (n) = fib(n − 1) + fib(n − 2) according to the recursive definition. This produces the same sequence of numbers as the definition above, only with the value for 0 added.
Keep in mind that self calls happen again and again for summing up and computing the Fibonacci numbers. That’s why you can only pass inputs around 990 here. Larger values will result in a RecursionError: maximum recursion depth exceeded. For other recursive functions, there are similar restrictions on the number of self calls. Other programming languages like Java allow significantly more self calls. In Java, over 10,000 self calls are easily possible.
There are several variants in recursion. An advantageous one is called tail-recursive. This is characterized by the fact that the recursive call is the last action in the calculation. Such functions can be processed without the otherwise usual storing of intermediate results on a stack.
3.1.2 Algorithmic Examples
In the introduction, you looked at mathematical examples. But recursion is also very well suited for algorithmic tasks. For example, it is possible to check for an array or list whether the values stored form a palindrome. A palindrome is a word that reads the same from the front and the back, such as OTTO or ABBA. Here it is meant that the elements match pairwise from the front and the back. This applies, for example, to a list with the following values: { 1, 2, 3, 2, 1 }.
Example 1: Palindrome—Recursive Variant
You can easily test for a palindrome property recursively. Let’s look at this as a program after I have briefly described the algorithm.
However, the described and implemented approach leads to many copies and extractions of subarrays or sublists. It is affordable to avoid this effort by keeping the idea but modifying the algorithm minimally by using a trick.
Optimized algorithm Rather than using a copy, you still use the original data structure. You include two position markers left and right, which initially span the entire array or list. Now you check if the left and right values referenced by these positions match. If this is the case, the position markers are moved inward by one position on both sides, and the whole procedure is called recursively. This is repeated until the left position pointer reaches or skips the right one.
Perhaps you wonder why I don’t write the process more compactly and even use less return statements. My main concern in presenting algorithms is comprehensibility. Multiple returns are really only a problem if a function is very long and confusing.
The idea of position pointers in arrays, lists, or strings is a common tool used in solutions to recursion for optimization and avoidance of, say, array copying. To prevent the whole thing becoming inconvenient for callers, it is a good idea to have a high-level function calling a helper function that has additional parameters. This allows you to include certain information in the recursive descent. In this example, these are the left and right limits, so that potentially costly copying can be eliminated. Many subsequent examples will take advantage of the general idea.
Example 1: Palindrome—Iterative Variant
The return value is determined by the condition implied by the comment, if left >= right holds, then values is not a palindrome. With this variant, however, you have to think much more about the return. Again, I prefer understandability and maintainability over brevity or performance.
Also, consider for this variant that in the presumably rare case of enormous amounts of data, an inverse variant of the original list is generated here. Thus, the memory is required twice.
Example 2: Fractal Generation
3.1.3 Steps When Multiplying the Digits of a Number
It is clearly visible how the recursive calls happen with a continuously shorter sequence of numbers. Finally, the result is constructed or calculated based on the last digit in the other direction.
3.1.4 Typical Problems: Endless Calls and RecursionError
Recursion often allows problems to be expressed and implemented in an understandable way. A detail worth knowing is that the self calls lead to them being stored temporarily on the stack. For each function call, a so-called stack frame containing information about the called function and its parameters is stored on the stack. The stack is, however, limited in its size. Thus only a finite number of nested function calls can take place—usually around 990. This was already discussed briefly in a practical tip.
You may still recognize a direct endless self call fairly well. But this becomes more difficult with an increasing number of lines. With some experience and practice in recursion, even the missing termination condition in the function factorial_no_abortion() may still be quite recognizable. But, in the function factorial_wrong_call() this is not that easy to determine. Here you must know more accurately what was intended.
- 1.
Termination condition: A recursive function must always include at least one termination condition. But even if defined correctly, it is possible that, for example, the disallowed negative value range is not checked. For factorial(n) a call with a negative value would then lead to a RecursionError.
- 2.
Complexity reduction : A recursive function must always subdivide the original problem into one or more smaller subproblems. Sometimes, this is already accomplished by reducing the value of a parameter by 1.
3.2 Exercises
3.2.1 Exercise 1: Fibonacci (★★✩✩✩)
Exercise 1a: Fibonacci Recursive (★✩✩✩✩)
Example
Input | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
fib(n) | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
Exercise 1b: Fibonacci Iterative (★★✩✩✩)
The recursive calculation of Fibonacci numbers is not efficient, and the running time increases enormously from about the fortieth or fiftieth Fibonacci number. Write an iterative version for the calculation.
3.2.2 Exercise 2: Process Digits (★★✩✩✩)
Exercise 2a: Count Digits (★★✩✩✩)
Write recursive function count_digits(value) that finds the number of digits in a positive natural number. We already discussed how to extract digits in the previous chapter in section 2.1.
Exercise 2b: Cross Sum (★★✩✩✩)
Calculate the sum of the digits of a number recursively. Write recursive function calc_sum_of_digits(value) for this purpose.
Examples
Input | Number of digits | Cross sum |
---|---|---|
1234 | 4 | 1 + 2 + 3 + 4 = 10 |
1234567 | 7 | 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28 |
3.2.3 Exercise 3: GCD (★★✩✩✩)
Exercise 3a: GCD Recursive (★✩✩✩✩)
Examples
Input 1 | Input 2 | Result |
---|---|---|
42 | 7 | 7 |
42 | 28 | 14 |
42 | 14 | 14 |
Exercise 3b: GCD Iterative (★★✩✩✩)
Create an iterative version for the GCD calculation.
Exercise 3c: LCM (★✩✩✩✩)
Examples
Input 1 | Input 2 | Result |
---|---|---|
2 | 7 | 14 |
7 | 14 | 14 |
42 | 14 | 42 |
3.2.4 Exercise 4: Reverse String (★★✩✩✩)
Write recursive function reverse_string (text) that flips the letters of the text passed in.
Examples
Input | Result |
---|---|
“A” | “A” |
“ABC” | “CBA” |
“abcdefghi” | “ihgfedcba” |
3.2.5 Exercise 5: List Sum (★★✩✩✩)
Write function sum_rec(values) that recursively computes the sum of the values of the given list. No call to the built-in functionality sum() is allowed.
Examples
Input | Result |
---|---|
[1, 2, 3] | 6 |
[1, 2, 3, -7] | -1 |
3.2.6 Exercise 6: List Min (★★✩✩✩)
Write function min_rec(values) that uses recursion to find the minimum value of the passed list. For an empty list, the value sys.maxsize should be returned. In the implementation, no call to the built-in functionality min() is allowed.
Examples
Input | Result |
---|---|
[7, 2, 1, 9, 7, 1] | 1 |
[11, 2, 33, 44, 55, 6, 7] | 2 |
[1, 2, 3, -7] | -7 |
3.2.7 Exercise 7: Conversions (★★✩✩✩)
Exercise 7a: Binary (★★✩✩✩)
Write function to_binary(n) that recursively converts the given positive integer into a textual binary representation. No call to int(x, base) may be used.
Examples
Input | Result |
---|---|
5 | “101” |
7 | “111” |
22 | “10110” |
42 | “101010” |
256 | “100000000” |
Exercise 7b: Octal and Hexadecimal Numbers (★★✩✩✩)
Write conversions to octal and hexadecimal numbers by implementing the corresponding functions to_octal(n) and to_hex(n). Again, no call to int(x, base) may be used.
Examples
Input | Method | Result |
---|---|---|
7 | octal | “7” |
8 | octal | “10” |
42 | octal | “52” |
15 | hexadecimal | “F” |
77 | hexadecimal | “4D” |
3.2.8 Exercise 8: Exponential Function (★★✩✩✩)
Exercise 8a: Power of Two (★★✩✩✩)
Write recursive function is_power_of_2(n) that evaluates the given positive integer to see if it is a power of two.
Examples
Input | Result |
---|---|
2 | True |
10 | False |
16 | True |
Exercise 8b: Exponentiation Recursive (★★✩✩✩)
Write recursive function power_of(value, exponent) that exponentiates the given positive integer with the positive number specified as second parameter. For example, the call power_of(4, 2) should return the square of 4, so compute 42 = 16. You may not use the built-in functionality pow() or the operator **.
Exercise 8c: Exponentiation Iterative (★★✩✩✩)
Write an iterative version of this exponentiation functionality.
Examples
Input base | Input exponent | Result |
---|---|---|
2 | 2 | 4 |
2 | 8 | 256 |
4 | 4 | 256 |
3.2.9 Exercise 9: Pascal’s Triangle (★★✩✩✩)
Starting with the third line, each subsequent line is calculated based on the previous one with the help of an addition, as shown in the last line of the following definition. For each line, these values are flanked by a 1 at the front and at the back. Since this is a two-dimensional structure, the recursive definition is a little more complex.
For background information and an in-depth explanation, please consult https://en.wikipedia.org/wiki/Pascal's_triangle.
3.2.10 Exercise 10: Number Palindromes (★★★★✩)
A palindrome is a word that reads the same from the front and the back. You can extend this definition to the digits of a number. Write recursive function is_number_palindrome(number) but without converting the number into a string and then using string functionalities like [::-1].
Examples
Input | Result |
---|---|
7 | True |
13 | False |
171 | True |
47742 | False |
3.2.11 Exercise 11: Permutations (★★★✩✩)
Calculate all permutations of a sequence of letters given as a string; this means all possible combinations of these letters. Implement this calculation in function calc_permutations(text). Consider also the case of duplicate letters, but do not use the standard Python functionality from the itertools module.
Examples
Input | Result |
---|---|
“A” | “A” |
“AA” | “AA” |
“AB” | “AB”, “BA” |
“ABC” | “ABC, “BAC”, “ACB”, “CAB”, “CBA”, “BCA” |
“AAC” | “AAC”, “ACA”, “CAA” |
3.2.12 Exercise 12: Count Substrings (★★✩✩✩)
Write function count_substrings (text, value_to_find) that counts all occurrences of the given substring. Thereby, when a pattern is found, it should be consumed; in other words, it should not be available for hits again. This is shown n the following table as the last case. Implement the whole thing yourself without resorting to the standard count().
Examples
Input | Search term | Result |
---|---|---|
“xhixhix” | “x” | 3 |
“xhixhix” | “hi” | 2 |
“mic” | “mic” | 1 |
“haha” | “ho” | 0 |
“xxxxyz” | “xx” | 2 |
3.2.13 Exercise 13: Ruler (★★✩✩✩)
In the introduction, I showed how to draw a simple shape of a ruler as well as a stylized snowflake (see Figure 3-3) using recursion. In this exercise, you want to imitate an English-style ruler. This involves dividing an area of one inch into 1/2 and 1/4 and 1/8. In doing so, the length of the strokes decreases by one each time.
Example
3.3 Solutions
3.3.1 Solution 1: Fibonacci (★★✩✩✩)
Solution 1a: Fibonacci Recursive (★✩✩✩✩)
Example
Input | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
fib(n) | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |
Keep in mind that self calls happen again and again when calculating Fibonacci numbers. Even worse, that is the case for values that have already been calculated before. This is suboptimal. In addition to the iterative variant shown in the following, the technique memoization discussed in section 7.1 can be used for optimization. In Python, decorators are suitable for this purpose, which I briefly introduce in Appendix B.
Solution 1b: Fibonacci Iterative (★★✩✩✩)
The recursive calculation of Fibonacci numbers is not efficient, and the running time increases enormously from about the fortieth to fiftieth Fibonacci number. Write an iterative version for the calculation.
Verification
3.3.2 Solution 2: Process Digits (★★✩✩✩)
Solution 2a: Count Digits (★★✩✩✩)
Write recursive function count_digits(value) that finds the number of digits in a positive natural number. You explored how to extract digits in the previous chapter in section 2.1.
Examples
Input | Number of digits | Cross sum |
---|---|---|
1234 | 4 | 1 + 2 + 3 + 4 = 10 |
1234567 | 7 | 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28 |
To ensure stable programs, it is often a good idea to check the parameters for validity. This can be accomplished in the form of simple if statements, as you have done several times before. In Python, however, this can be achieved more elegantly with the help of decorators, which I briefly introduce in Appendix B.
Solution 2b: Cross Sum (★★✩✩✩)
Calculate the sum of the digits of a number recursively. Write recursive function calc_sum_of_digits(value) for this purpose.
However, this assignment is not about brevity, but about getting to know the recursive description of the calculation of the sum of the digits.
Verification
3.3.3 Solution 3: GCD (★★✩✩✩)
Solution 3a: GCD Recursive (★✩✩✩✩)
Examples
Input 1 | Input 2 | Result |
---|---|---|
42 | 7 | 7 |
42 | 28 | 14 |
42 | 14 | 14 |
Solution 3b: GCD Iterative (★★✩✩✩)
Create an iterative version for the GCD calculation.
Verification
Solution 3c: LCM (★✩✩✩✩)
Write function lcm(a, b) that computes the least common multiplier (LCM) . For two natural numbers a and b, you can calculate this based on the GCD using the following formula:
lcm(a, b) = a ∗ b / gcd(a, b);
Examples
Input 1 | Input 2 | Result |
---|---|---|
2 | 7 | 14 |
7 | 14 | 14 |
42 | 14 | 42 |
Verification
3.3.4 Solution 4: Reverse String (★★✩✩✩)
Write recursive function reverse_string(text) that flips the letters of the text passed in.
Examples
Input | Result |
---|---|
“A” | “A” |
“ABC” | “CBA” |
“abcdefghi” | “ihgfedcba” |
However, this task is about getting to know recursion.
Verification
3.3.5 Solution 5: List Sum (★★✩✩✩)
Write function sum_rec(values) that recursively computes the sum of the values of the given list. No call to the built-in functionality sum() is allowed.
Examples
Input | Result |
---|---|
[1, 2, 3] | 6 |
[1, 2, 3, -7] | -1 |
Algorithm Compute the partial sum with the recursive definition as long as
sum(values(0)) = values[0]
sum(values(0 ... n)) = values[0] + sum(values(1 ... n))
Alternative algorithm Alternatively, it is also possible to let the pos counter run from length - 1 to 0, so the recursion reverses to the following:
sum(values(0 ... n)) = sum(values(0 ... n − 1)) + values[n]
Verification
3.3.6 Solution 6: List Min (★★✩✩✩)
Write function min_rec(values) that uses recursion to find the minimum value of the passed list. For an empty list, the value sys.maxsize should be returned. In the implementation, no call to the built-in functionality min() is allowed.
Examples
Input | Result |
---|---|
[7, 2, 1, 9, 7, 1] | 1 |
[11, 2, 33, 44, 55, 6, 7] | 2 |
[1, 2, 3, -7] | -7 |
Verification
3.3.7 Solution 7: Conversions (★★✩✩✩)
Solution 7a: Binary (★★✩✩✩)
Write function to_binary(n) that recursively converts the given positive integer into a textual binary representation. No call to int(x, base) may be used.
Examples
Input | Result |
---|---|
5 | “101” |
7 | “111” |
22 | “10110” |
42 | “101010” |
256 | “100000000” |
Invocation | Process | Result |
---|---|---|
to_binary(22) | to_binary(22/2) + str(22%2) => to_binary(11) + “0” | “10110” |
to_binary(11) | to_binary(11/2) + str(11%2) => to_binary(5) + “1” | “1011” |
to_binary(5) | to_binary(5/2) + str(5%2) => to_binary(2) + “1” | “101” |
to_binary(2) | to_binary(2/2) + str(2%2) => to_binary(1) + “0” | “10” |
to_binary(1) | str(1) => “1” | “1” |
Solution 7b: Octal and Hexadecimal Numbers (★★✩✩✩)
Write conversions to octal and hexadecimal numbers by implementing the corresponding functions to_octal(n) and to_hex(n). Again, no call to int(x, base) may be used.
Examples
Input | Method | Result |
---|---|---|
7 | Octal | “7” |
8 | Octal | “10” |
42 | Octal | “52” |
15 | Hexadecimal | “F” |
77 | Hexadecimal | “4D” |
Verification
3.3.8 Solution 8: Exponential Function (★★✩✩✩)
Solution 8a: Power of Two (★★✩✩✩)
Write recursive function is_power_of_2(n) that evaluates the given positive integer to see if it is a power of two.
Examples
Input | Result |
---|---|
2 | True |
10 | False |
16 | True |
For the initial check, use a little trick with return n==1, which has the following effect:
n < 0 : False (negative number, so never the value 1)
n = 0 : False (0 ≠1)
n = 1 : True (1 = 1)
Solution 8b: Exponentiation Recursive (★★✩✩✩)
Write recursive function power_of(value, exponent) that exponentiates the given positive integer with the positive number specified as second parameter. For example, the call power_of(4, 2) should return the square of 4, so compute 42 = 16. You may not use the built-in functionality pow() or the operator **.
This alternative has a cost of O(n). But it is quite easy to optimize this and reduce it to O(log(n)).
Solution 8c: Exponentiation Iterative (★★✩✩✩)
Write an iterative version of this exponentiation functionality.
Examples
Input base | Input exponent | Result |
---|---|---|
2 | 2 | 4 |
2 | 8 | 256 |
4 | 4 | 256 |
Verification
3.3.9 Solution 9: Pascal’s Triangle (★★✩✩✩)
Starting with the third line, each subsequent line is calculated based on the previous one with the help of an addition, as shown in the last line of the following definition. For each line, these values are flanked by a 1 at the front and at the back. Since this is a two-dimensional structure, the recursive definition is a little more complex.
For background information and an in-depth explanation, please consult https://en.wikipedia.org/wiki/Pascal's_triangle.
Actually, there is no need for a separate termination condition for the top. Nevertheless, this is shown here for the sake of better comprehension—but of course, that is a matter of taste.
Optimized algorithm The pure recursive definition results in quite a lot of computations. It becomes more understandable, comprehensible, and performant if you work line by line.
Verification
3.3.10 Solution 10: Number Palindromes (★★★★✩)
A palindrome is a word that reads the same from the front and the back. You can extend this definition to the digits of a number. Write recursive function is_number_palindrome(number) but without converting the number into a string and then using string functionalities like [::-1].
Examples
Input | Result |
---|---|
7 | True |
13 | False |
171 | True |
47742 | False |
Algorithm Because of the restriction demanded in the exercise, it is not possible to compare character by character. However, the operations modulo and division are suitable, which you have already used for similar tasks. You use both to separate and compare the left and right digits.
The solution shown is by no means optimal since the factors have to be determined constantly. Furthermore, the entire procedure is still quite difficult to understand from the source code, even though helper functions have already been extracted.
Verification
3.3.11 Solution 11: Permutations (★★★✩✩)
Calculate all permutations of a sequence of letters given as a string; this means all possible combinations of these letters. Implement this calculation in function calc_permutations(text). Consider also the case of duplicate letters, but do not use the standard Python functionality from the itertools module.
Examples
Input | Result |
---|---|
“A | “A” |
“AA” | “AA” |
“AB” | “AB”, “BA” |
“ABC” | “ABC, “BAC”, “ACB”, “CAB”, “CBA”, “BCA” |
“AAC” | “AAC”, “ACA”, “CAA” |
Algorithm The best way to compute all permutations for a given string is to take a look at the recursive definition and then implement it:
Select and extract the ith character.
Build the remaining string and calculate the permutations for it.
Put the whole thing together again.
This implementation leads to the creation of quite a lot of instances of strings and sets as intermediate buffers. How can this be improved?
Optimized algorithm The drawbacks mentioned above are negligible for a short string. However, the longer the string gets, creating all the temporary objects and performing the string actions become more noticeable. How can this be avoided?
Let me comment a bit on the optimization. While calling the method calc_permutations("abcdefghij") takes about 7 to 8 seconds with my iMac (i7 4Ghz), calc_permutations_mini_opt("abcdefghij") finishes after only about 4 to 5 seconds—this is due to the very large number of calls, for which smaller optimizations may be worthwhile.
However, if you add one additional character to the input, the overhead grows enormously to around 111 seconds and for the optimized version to around 85 seconds. Such increases in running time are, of course, absolutely undesirable. After reading Chapter 7 covering more advanced recursion techniques, you may want to look again at the computation of the permutations to attempt an improvement with the help of memoization. However, this will be at the expense of the required memory.
Verification
3.3.12 Solution 12: Count Substrings (★★✩✩✩)
Write function count_substrings (text, value_to_find) that counts all occurrences of the given substring. Thereby, when a pattern is found, it should be consumed, so it should not be available for hits again. This is shown in the following table as the last case. Implement the whole thing yourself without resorting to the standard count().
Examples
Input | Search term | Result |
---|---|---|
“xhixhix” | “x” | 3 |
“xhixhix” | “hi” | 2 |
“mic” | “mic” | 1 |
“haha” | “ho” | 0 |
“xxxxyz” | “xx” | 2 |
Optimized algorithm Calls to text[len(value_to_find):] and text[1:] keep generating new strings in the original algorithm. For short input values, this is not so dramatic. But for a very long text, this can be unfavorable.
- 1.
Since the text does not get shorter, you must now subtract the value of left from the original length.
- 2.
You used startswith() to compare for a match. Conveniently there is a variant that allows for providing an offset.
- 3.
If there is a match, you must move the position pointer by the number of characters in the search pattern, otherwise by one position.
Python shortcut Conveniently, this functionality is already built into Python. Therefore, a call to the built-in functionality count() for strings would be much simpler. However, the point here is to look at variants and see how to avoid too many temporary strings.
Verification
3.3.13 Solution 13: Ruler (★★✩✩✩)
In the introduction, I showed how to draw a simple shape of a ruler as well as a stylized snowflake (see Figure 3-3) using recursion. In this exercise, you want to imitate an English-style ruler. This involves dividing an area of one inch into 1/2 and 1/4 and 1/8. In doing so, the length of the strokes decreases by one each time.
Example
Verification
3.4 Summary: What You Learned
This introductory chapter laid the foundation for a good understanding of recursion. The exercises expanded your knowledge on how to use recursion to solve problems. This is crucial to be able to implement recursive solutions in the following chapters in an efficient way and with a solid basis.
Now let’s move on to sequences of characters, also known as strings. Very few program can live without them—time to get into it.