Recursive Algorithms ==================== Recursive Functions ------------------- Defining mathematical functions recursively is similar to to rule based programming. A program is a sequence of rules. Each rule has certains conditions that must be met in order for the rule to apply. Consider the recursive definition of the factorial function. Let :math:`n` be a natural number. By :math:`n!` we denote *the factorial* of :math:`n`. Its recursive definition is given by two rules: 1. For :math:`n \leq 1`: :math:`n! = 1`. 2. If we know the value for :math:`(n-1)!`, then :math:`n! = n \times (n-1)!`. Recursion is similar to mathematical proof by induction: 1. First we verify the trivial or base case. 2. Assuming the statement holds for all values smaller than :math:`n` -- the induction hypothesis -- we extend the proof to :math:`n`. In Python, we can define the factorial recursively as in the function below. :: def factorial(nbr): """ Computes the factorial of nbr recursively. """ if nbr <= 1: return 1 else: return nbr*factorial(nbr-1) def main(): """ Prompts the user for a number and returns the factorial of it. """ nbr = int(input('give a number n : ')) fac = factorial(nbr) print('n! = ', fac) print('len(n!) = ', len(str(fac))) To understand how recursive functions work, let us trace the execution of the recursive function ``factorial``, with ``5`` as argument. :: factorial(5) call #0: call for n-1 = 4 factorial(4) call #1: call for n-1 = 3 factorial(3) call #2: call for n-1 = 2 factorial(2) call #3: call for n-1 = 1 factorial(1) call #4: base case, return 1 factorial(2) call #3: returning 2 factorial(3) call #2: returning 6 factorial(4) call #1: returning 24 factorial(5) call #0: returning 120 Observe that the computations happen in the return statements: ``return 1``, ``return 1*2``, ``return 1*2*3``, ``return 1*2*3*4``, and ``return 1*2*3*4*5``. When running the recursive factorial, we may run into the limit of the stack size. :: $ python factorial.py give a number : 79 n! = 894618213078297528685144171539831652 069808216779571907213868063227837990693501 860533361810841010176000000000000000000 len(n!) = 117 Exploiting Python long integers, a bit more: :: $ python factorial.py give a number : 1234 ... RuntimeError: maximum recursion depth exceeded What just happened? Why did we encounter this exception? The execution of recursive functions requires a stack of function calls. For example, for ``n = 5``, the stack grows like :: factorial(1) call #4: base case, return 1 factorial(2) call #3: returning 2 factorial(3) call #2: returning 6 factorial(4) call #1: returning 24 factorial(5) call #0: returning 120 New function calls are pushed on the stack. Upon return, a function call is popped off the stack. As an exception handler, we will compute the factorial iteratively. :: def factexcept(nbr): """ When the recursion depth is exceeded the factorial of nbr is computed iteratively. """ if nbr <= 1: return 1 else: try: return nbr*factexcept(nbr-1) except RuntimeError: print('run time error raised') fac = 1 for i in range(2, nbr+1): fac = fac*i return fac Accumulating Parameters ----------------------- An *accumulating parameter* accumulates the result of a computation. With accumulating parameters, we can trace recursive functions automatically. Tracing the execution of a recursive function means: displaying for each function call: 1. the value(s) for the input parameter(s); 2. what is computed inside the function; and 3. the return value of the function. As an example of an accumulating parameter, let us keep track of the number of each function call. Use an accumulating parameter ``k``: :: def factotrace(nbr, k): The we increment ``k`` with each recursive call: :: return factotrace(nbr-1, k+1) To illustrate the running of the function ``factotrace``, we call ``factotrace`` with ``nbr = 5`` and ``k = 0``, which then prints :: factotrace(5,0): call for nbr-1 = 4 factotrace(4,1): call for nbr-1 = 3 factotrace(3,2): call for nbr-1 = 2 factotrace(2,3): call for nbr-1 = 1 factotrace(1,4): base case, return 1 factotrace(2,3): returning 2 factotrace(3,2): returning 6 factotrace(4,1): returning 24 factotrace(5,0): returning 120 At call ``k``, we indent with ``k`` spaces. The code for the function comes next. :: def factotrace(nbr, k): """ Prints out trace information in call k the initial value for k should be zero. """ prt = k*' ' prt = prt + 'factotrace(%d,%d):' % (nbr, k) if nbr <= 1: print(prt + ' base case, return 1') return 1 else: print(prt + ' call for nbr-1 = ' + str(nbr-1)) fac = nbr*factotrace(nbr-1, k+1) print(prt + ' returning %d' % fac) return fac Instead of performing the multiplications in the return of our first recursive function for the factorial, we can execute the multiplications with an accumulating parameter. Consider the following function. :: def factaccu(nbr, fac): """ Accumulates the factorial of nbr in fac call factaccu initially with fac = 1. """ if nbr <= 1: return fac else: return factaccu(nbr-1, fac*nbr) Let us trace the execution of ``factaccu``, for ``nbr = 5`` and ``fac = 1`` on input. :: factaccu(5,1) call #0: call for nbr-1 = 4 factaccu(4,5) call #1: call for nbr-1 = 3 factaccu(3,20) call #2: call for nbr-1 = 2 factaccu(2,60) call #3: call for nbr-1 = 1 factaccu(1,120) call #4: returning 120 factaccu(2,60) call #3: returning 120 factaccu(3,20) call #2: returning 120 factaccu(4,5) call #1: returning 120 factaccu(5,1) call #0: returning 120 We observe that ``factaccu`` ``1*5``, ``1*5*4``, ``1*5*4*3``, ``1*5*4*3*2``, and then returns 120. If we extend ``factaccu`` with another accumulating parameter to count the number of function calls, we can have the function print all tracing statements. Consider the function below. :: def factatrace(nbr, fac, k): """ Accumulates the factorial of nbr in fac, k is used to trace the calls initialize fac to 1 and k to 0. """ prt = k*' ' prt = prt + 'factatrace(%d,%d,%d)' % (nbr, fac, k) if nbr <= 1: print(prt + ' returning ' + str(fac)) return fac else: print(prt + ' call for nbr-1 = ' + str(nbr-1)) result = factatrace(nbr-1, fac*nbr, k+1) print(prt + ' returning %d' % result) return result The output of ``factatrace``, called with ``nbr = 5``, ``fac = 1``, and ``k = 0`` is below :: factatrace(5,1,0) call for nbr-1 = 4 factatrace(4,5,1) call for nbr-1 = 3 factatrace(3,20,2) call for nbr-1 = 2 factatrace(2,60,3) call for nbr-1 = 1 factatrace(1,120,4) returning 120 factatrace(2,60,3) returning 120 factatrace(3,20,2) returning 120 factatrace(4,5,1) returning 120 factatrace(5,1,0) returning 120 Recursive Problem Solving ------------------------- If reading a word forwards and backwards is the same, then the word is a *palindrome*. Examples of palindromes are mom, dad, rotor. The problem is to write a function which, given a word, returns ``True`` if the word is palindrom, and return ``False`` otherwise. Using the builtin methods of Python, we can solve this problem quickly. Consider the session below. :: >>> s = 'motor' >>> L = [c for c in s] >>> L ['m', 'o', 't', 'o', 'r'] >>> L.reverse() >>> L ['r', 'o', 't', 'o', 'm'] >>> t = ''.join(L) >>> t 'rotom' >>> s == t False Next we derive a recursive solution. :: def is_palindrome(word): """ Returns True if word is a palindrome, and returns False otherwise. """ We have three base cases: 1. The word is empty or only one character long. 2. The first and last character are different. 3. The word consists of two equal characters. These three base cases are implemented in the first three if statements of the function below. :: def is_palindrome(word): """ Returns True if word is a palindrome, and returns False otherwise. """ if len(word) <= 1: return True elif word[0] != word[len(word)-1]: return False elif len(word) == 2: return True else: short = word[1:len(word)-1] return is_palindrome(short) The main function used to test ``is_palindrome`` follows. :: def main(): """ Prompts the user for a word and checks if it is a palindrome. """ word = input('give a word : ') prt = 'the word \"' + word + '\" is ' if not is_palindrome(word): prt = prt + 'not ' prt = prt + 'a palindrome' print(prt) if __name__ == "__main__": main() We end with some examples of running the script ``palindromes.py`` at the command line. :: $ python3 palindromes.py give a word : palindromes the word "palindromes" is not a palindrome Because of the ``input()`` returns a string, we can also test numbers: :: $ python3 palindromes.py give a word : 1234321 the word "1234321" is a palindrome The palindrome tester works just as well on numbers. Exercises --------- 1. The *n*-th Fibonacci number :math:`F_n` is defined for :math:`n \geq 2` as :math:`F_n = F_{n-1} + F_{n-2}` and :math:`F_0 = 0`, :math:`F_1 = 1`. Give a Python function ``Fibonacci`` to compute :math:`F_n`. 2. Use an accumulating parameter ``k`` to ``Fibonacci`` to count the function calls. When tracing the execution, print with ``k`` spaces as indentations. 3. Write an equivalent C function to compute factorials recursively. Use a main interactive program to test it. 4. Extend ``is_palindrome`` with an extra accumulating parameter ``k`` to keep track of the function calls. Trace the execution with this extended function, using ``k`` spaces as indentations. 5. Write a recursive function to sum a list of numbers.