Exit slides Esc

Unit 2 — Recursive Functions

Slide 1 of 61

  1. The problem: The Shrinking Function

    Here is a question in the style of the ACSL Recursive Functions category. Read it now; by the end of Lesson 1 you will answer it with pencil and paper, and by the end of Lesson 3 you will write a program that answers it for any x.

    Find f(20), where

    f(x) = { f(x - 3) + x      if x > 10
           { 2 * x - 5         if x <= 10
  2. The definition of f uses f itself. To know f(20) you need f(17); to know f(17) you need f(14); and so on. Does it ever stop? What number comes out?

  3. The programming part

    Write a program that reads one whole number x and prints f(x), for the same f.

  4. Sample Input

    20
  5. Sample Output

    73
  6. Constraints

    • -1000 <= x <= 2000

    To solve it you need three skills, one per lesson: how to read an ACSL recursive definition and work it out with a call table (Lesson 1), the other kinds of recursive definitions ACSL asks about (Lesson 2), and how to write a recursive function in Python (Lesson 3).

    • Read a recursive definition in ACSL's cases notation, and tell the base case from the recursive case.

    • Evaluate it by hand with a call table: down to the base case, then back up.

    • Handle conditions with mod, floor and abs, functions of two variables, and multiple and indirect recursion.

    • Write recursive functions in Python, and follow the call stack.

  7. Lesson 1 — Reading a recursive definition

    A definition is recursive when it uses itself. Think of a set of nesting dolls: to open the big doll you open it and find a smaller doll, which you open in the same way, until you reach the tiny doll that does not open.

    A recursive function has two kinds of rules:

    • a recursive case, which gives the answer in terms of the same function on a different, usually smaller input, such as f(x - 3) + x;
    • a base case, which gives the answer directly, with no call to the function, such as 2 * x - 5.

    The base case is the tiny doll. Without it the calls would never stop.

  8. The cases notation

    ACSL writes a recursive function as a list of rows inside one big bracket. Each row is a value, then the condition under which that row is used.

    f(x) = { f(x - 2) + 1      if x > 3
           { x                 if x <= 3
  9. Read it like this: "to find f(x), look down the rows for the one whose condition is true for your x, and work out that row's value." The conditions never overlap, so exactly one row fits each x. A last row may say otherwise, which means "when no row above fits".

    A few notes on how the definitions in this book are written:

    We write It means ACSL may also write
    2 * x 2 times x 2x or 2·x
    x mod 3 the remainder when x is divided by 3 x mod 3
    abs(x) x without its minus sign abs(x) or │x│
    floor(x / 2) x divided by 2, rounded down to a whole number ⌊x/2⌋ or int(x/2)
    <=, >= at most, at least ≤, ≥
    != is not equal to ≠

    For example, floor(7 / 2) is 3, 17 mod 5 is 2, and abs(-4) is 4.

  10. The call-table method

    To find f(8) for the definition above, work in two passes.

    Pass 1, going down. Start with f(8). Find the row that fits, and write what it turns into. Keep going until you reach a base case, which is a plain number.

    call which row fits turns into
    f(8) 8 > 3 f(6) + 1
    f(6) 6 > 3 f(4) + 1
    f(4) 4 > 3 f(2) + 1
    f(2) 2 <= 3 2 (base case)

    Pass 2, coming back up. Now fill in the values from the bottom of the table to the top.

    • f(2) = 2

    • f(4) = f(2) + 1 = 3

    • f(6) = f(4) + 1 = 4

    • f(8) = f(6) + 1 = 5

  11. So f(8) = 5.

  12. During pass 1 you cannot work out any value yet, because each row is waiting for the row below it. Only the base case gives a number, and every other value is built from it on the way back up.

  13. Solving the opening problem

    Now try f(20) from the top of the unit yourself, before you read on. Make the table, then fill in the values.

    call which row fits turns into
    f(20) 20 > 10 f(17) + 20
    f(17) 17 > 10 f(14) + 17
    f(14) 14 > 10 f(11) + 14
    f(11) 11 > 10 f(8) + 11
    f(8) 8 <= 10 2 * 8 - 5 = 11 (base case)

    Coming back up: f(8) = 11, f(11) = 11 + 11 = 22, f(14) = 22 + 14 = 36, f(17) = 36 + 17 = 53, and f(20) = 53 + 20 = 73.

  14. The + x in each row uses the x of that row. In the row for f(14) it adds 14, not 20. Writing each row's x in the table stops you from mixing them up.

  15. Checking pass 1 with a program

    Pass 1 only has to follow the arguments 20, 17, 14, 11, 8 until the base case. A short loop can list them for you. It subtracts 3 while x > 10, just like the first row of the definition, and keeps the numbers in a string.

  16. x = 20
    chain = str(x)
    while x > 10:
        x = x - 3
        chain = chain + " " + str(x)
    print(chain)

    Output

    20 17 14 11 8
    

    The loop lists the calls but does not add anything up; pass 2 is still yours to do. The version that reads x with input() is saved as assets/l1.py.

  17. Try it
    x = int(input())
    chain = str(x)
    while x > 10:
        x = x - 3
        chain = chain + " " + str(x)
    print(chain)
  18. Run it from this unit's folder:

    python assets/l1.py < assets/l1/1.in

    It prints 20 17 14 11 8, the first column of your call table.

  19. Conditions that use mod, floor and abs

    ACSL conditions are often more than x > 10. Here the row depends on whether x is odd or even:

    g(x) = { g(x - 1) + x          if x > 0 and x mod 2 = 1
           { g(floor(x / 2)) + 1   if x > 0 and x mod 2 = 0
           { 0                     if x = 0

    Find g(13).

    call which row fits turns into
    g(13) 13 is odd g(12) + 13
    g(12) 12 is even g(6) + 1
    g(6) 6 is even g(3) + 1
    g(3) 3 is odd g(2) + 3
    g(2) 2 is even g(1) + 1
    g(1) 1 is odd g(0) + 1
    g(0) x = 0 0 (base case)

    Coming back up: g(0) = 0, g(1) = 1, g(2) = 2, g(3) = 5, g(6) = 6, g(12) = 7, and g(13) = 20.

  20. The argument does not always shrink by the same amount. Sometimes it drops by 1, sometimes it halves. Check the condition fresh for every row.

  21. With abs, the argument can even turn negative on the way down:

    h(x) = { h(abs(x) - 5) + 1     if abs(x) > 3
           { x                     if abs(x) <= 3

    For h(9): abs(9) = 9 > 3, so h(9) = h(4) + 1. Then abs(4) = 4 > 3, so h(4) = h(-1) + 1. Now abs(-1) = 1 <= 3, so h(-1) = -1, a base case. Coming back up: h(4) = 0 and h(9) = 1.

  22. The base case returns x itself, and here x is -1, so keep the minus sign. abs is applied only where the definition writes it.

  23. Lesson 2 — More kinds of recursive functions

    ACSL asks about a few different shapes of recursive function. The call-table method works for all of them; only the bookkeeping changes.

  24. Two variables

    A function can take two inputs. Each row changes one or both of them.

    f(x, y) = { f(x - 2, y + 1) + y     if x > y
              { y - x                   if x <= y

    Find f(9, 1).

    call which row fits turns into
    f(9, 1) 9 > 1 f(7, 2) + 1
    f(7, 2) 7 > 2 f(5, 3) + 2
    f(5, 3) 5 > 3 f(3, 4) + 3
    f(3, 4) 3 <= 4 4 - 3 = 1 (base case)

    Coming back up: f(5, 3) = 1 + 3 = 4, f(7, 2) = 4 + 2 = 6, and f(9, 1) = 6 + 1 = 7.

  25. x goes down while y goes up, so they meet. With two variables, keep both in every row of the table; the + y uses that row's y.

  26. Multiple recursion

    In multiple recursion, one row calls the function more than once.

    f(x) = { f(x - 1) + 2 * f(x - 2)    if x >= 2
           { x                          if x <= 1

    To find f(4) you need f(3) and f(2). Each of those needs two more calls, so the calls branch out like a tree:

                     f(4)
                  /        \
              f(3)          f(2)
             /    \        /    \
          f(2)    f(1)   f(1)   f(0)
         /    \
      f(1)    f(0)
  27. The tree for f(4) already has 9 calls, and f(2) is worked out twice. For a bigger x the tree grows very fast.

    The table trick. Instead of drawing the tree, work upward from the base cases, one value at a time. Each new value needs only the two values just before it.

    x 0 1 2 3 4 5 6
    f(x) 0 1 1 + 2·0 = 1 1 + 2·1 = 3 3 + 2·1 = 5 5 + 2·3 = 11 11 + 2·5 = 21

    So f(4) = 5 and f(6) = 21.

  28. The table gives the same answers as the tree, because f(2) is always 1, however many times it is asked for. Working bottom-up just avoids doing the same work twice. Be careful which value gets the 2 *: here it is always the one two places back.

  29. A program can build the same table with a list. Position x of the list holds f(x), so table[x - 1] and table[x - 2] are the two values just before.

  30. n = 6
    table = []
    for x in range(n + 1):
        if x <= 1:
            table.append(x)
        else:
            table.append(table[x - 1] + 2 * table[x - 2])
    print(table)

    Output

    [0, 1, 1, 3, 5, 11, 21]
    

    The if inside the loop plays the part of the base-case row, and the else plays the recursive row. The version that reads n and prints the row of the table with spaces is saved as assets/l2.py.

  31. Try it
    n = int(input())
    table = []
    for x in range(n + 1):
        if x <= 1:
            table.append(x)
        else:
            table.append(table[x - 1] + 2 * table[x - 2])
    row = str(table[0])
    for x in range(1, n + 1):
        row = row + " " + str(table[x])
    print(row)
  32. Run it from this unit's folder:

    python assets/l2.py < assets/l2/1.in

    It prints 0 1 1 3 5 11 21, the bottom row of the table above.

  33. Indirect recursion

    Intermediate and above.

    In indirect recursion, one function calls a second function, which calls the first one back. Neither one calls itself directly, but together they go round in a loop until a base case stops them.

    f(x) = { g(x - 1) + 1              if x > 0
           { 0                         if x <= 0
    
    g(x) = { 2 * f(floor(x / 2))       if x > 1
           { x + 1                     if x <= 1

    Find f(10). Add a column for which function is being called.

    call which row fits turns into
    f(10) 10 > 0 g(9) + 1
    g(9) 9 > 1 2 * f(4)
    f(4) 4 > 0 g(3) + 1
    g(3) 3 > 1 2 * f(1)
    f(1) 1 > 0 g(0) + 1
    g(0) 0 <= 1 0 + 1 = 1 (base case)
  34. Coming back up: f(1) = 1 + 1 = 2, g(3) = 2 * 2 = 4, f(4) = 4 + 1 = 5, g(9) = 2 * 5 = 10, and f(10) = 10 + 1 = 11.

  35. Each row uses the rules of the function named in its call. g(0) uses g's rows, even though it was called from f.

  36. Does it ever stop?

    Before you start a table, check that the calls head toward a base case.

    f(x) = { f(x + 2) - 1     if x > 0
           { 5                if x <= 0

    For f(3): f(3) needs f(5), which needs f(7), and so on forever. x moves away from the base case, so this f(3) has no value. This is called infinite recursion. ACSL definitions always reach a base case, but when you invent your own, make sure every recursive row moves toward one.

  37. ACSL never asks about a function applied to its own result, such as f(f(x)), so this book does not either.

  38. Lesson 3 — Recursion in Python

    A Python function can call itself. Here is the opening problem's f, written as a Python function. Compare it with the definition row by row.

  39. def f(x):
        if x > 10:
            return f(x - 3) + x
        return 2 * x - 5
    
    
    print(f(20))
    print(f(8))

    Output

    73
    11
    

    Each row of the cases notation becomes a branch of the function. The if holds the recursive case, which calls f again on x - 3. The last line is the base case: it returns a number without calling f. f(20) gives 73, the same answer as your call table.

  40. Watching the calls

    Add two print lines to see the calls happen: one when a call starts, and one just before it returns.

  41. def f(x):
        print("start f(" + str(x) + ")")
        if x > 10:
            answer = f(x - 3) + x
        else:
            answer = 2 * x - 5
        print("f(" + str(x) + ") = " + str(answer))
        return answer
    
    
    print(f(17))

    Output

    start f(17)
    start f(14)
    start f(11)
    start f(8)
    f(8) = 11
    f(11) = 22
    f(14) = 36
    f(17) = 53
    53
    
  42. All the start lines come first, going down: 17, 14, 11, 8. Then the answers come back up in the opposite order: f(8) first, f(17) last. That is exactly the two passes of the call table: Python does pass 1 and pass 2 for you.

  43. The call stack

    When f(17) calls f(14), the call f(17) is not finished: it still has to add 17. Python remembers every unfinished call, each with its own x, in a pile called the call stack.

    f(8)     <- running now; it is a base case, so it returns 11
    f(11)    waiting to add 11
    f(14)    waiting to add 14
    f(17)    waiting to add 17
  44. A new call goes on top of the pile. When the top call returns, it is removed, and the call below it carries on with the returned value. Because each call has its own x, the x in f(11) stays 11 while f(8) runs.

    The pile cannot grow forever. Python stops a program with a RecursionError when calls go about 1000 deep. That happens when a function has no base case, or when a recursive case moves away from it:

  45. Error demo: this code stops with an error
    def broken(x):
        return broken(x - 1) + 1
    
    
    print(broken(5))

    broken has no base case, so the calls go broken(4), broken(3), ..., broken(-990), ... until Python gives up. This program is not run here: it could only end in an error. Two rules keep a recursive function safe: check the base case before you recurse, and make sure every recursive call moves toward a base case.

  46. mod, floor and abs in Python

    The ACSL operations have Python partners you already know:

    Definition Python
    x mod 2 x % 2
    floor(x / 2) x // 2
    abs(x) abs(x)

    Here is g from Lesson 1.

  47. def g(x):
        if x > 0 and x % 2 == 1:
            return g(x - 1) + x
        if x > 0 and x % 2 == 0:
            return g(x // 2) + 1
        return 0
    
    
    print(g(13))
    print(-7 // 2)

    Output

    20
    -4
    

    // always rounds down, so -7 // 2 is -4, not -3. That matches floor, even for negative numbers. The definitions in this book use mod only with numbers that are 0 or more, where % matches it exactly.

  48. Two variables, multiple and indirect recursion

    Every kind of definition from Lesson 2 goes into Python the same way: one branch per row.

  49. def two(x, y):
        if x > y:
            return two(x - 2, y + 1) + y
        return y - x
    
    
    def many(x):
        if x >= 2:
            return many(x - 1) + 2 * many(x - 2)
        return x
    
    
    print(two(9, 1))
    print(many(6))

    Output

    7
    21
    

    many calls itself twice in one line, just like the tree in Lesson 2. It gives 21, the same as the table. For large x it gets slow, because it repeats work: the table method is faster by hand and in code.

  50. For indirect recursion, write both functions. Python looks up g only when f actually runs, so it does not matter that f is written first.

  51. def f(x):
        if x > 0:
            return g(x - 1) + 1
        return 0
    
    
    def g(x):
        if x > 1:
            return 2 * f(x // 2)
        return x + 1
    
    
    print(f(10))

    Output

    11
    
  52. Returning two things at once

    Sometimes you want a function to report two results, for example its value and how many calls it took. Return a tuple, and unpack it where you call the function.

  53. def value_and_calls(x):
        if x > 10:
            value, calls = value_and_calls(x - 3)
            return (value + x, calls + 1)
        return (2 * x - 5, 1)
    
    
    value, calls = value_and_calls(20)
    print(value, calls)

    Output

    73 5
    

    The base case counts itself as 1 call, and every recursive case adds 1 for itself to the count its call gave back. f(20) takes 5 calls: f(20), f(17), f(14), f(11) and f(8).

  54. Put it together: here is a complete solution to the programming part of the opening problem. It reads x and prints f(x). It is saved as assets/l3.py.

  55. Try it
    def f(x):
        if x > 10:
            return f(x - 3) + x
        return 2 * x - 5
    
    
    x = int(input())
    print(f(x))
  56. Run it from this unit's folder:

    python assets/l3.py < assets/l3/1.in
  57. The definition becomes the function, and the rest of the program is just "read, call, print". Test the edges the constraints allow: a base case straight away (assets/l3/2.in), a value just above the boundary (assets/l3/3.in), a negative x (assets/l3/4.in), and the largest x (assets/l3/5.in), which makes about 660 calls, safely under Python's limit.

    • A recursive definition needs a base case that stops the calls.

    • A call table works in two passes: pass 1 goes down to the base case, and pass 2 puts the values back up. For the opening problem, f(20) = 73.

    • For multiple recursion, fill a table upward from the base cases instead of drawing the whole tree.

    • A Python function can call itself; the call stack remembers every unfinished call, and a missing base case ends in a RecursionError.

  58. You can now evaluate an ACSL recursive function by hand and check it with a program.