Lecture 10: The Call Stack and Recursion

Topics

The Call Stack

  • what is displayed if we call mystery(2, 3) in call_stack.py?
    • The mystery number is: 15
  • why?
  • We can visualize each of these function calls:

    mystery(2, 3)
      |
      V
    "The mystery number is: " + c(2, 3)
            |
            V
            b(6) - 1
            |
            V
            6 + a()
              |
              V
              10
    
  • Finally, when a returns its value then we can work our way back and get the final answer
    • a() returns 10
    • which allows b to now return 16
    • once c knows its call to b was 16, it returns 15
    • and finally, we can generate the string that the mystery function prints out
  • the way that the computer keeps track of all of this is called the "stack"
    • as functions are called, the stack grows
      • new function calls are added onto the stack
    • when functions finished, the stack shrinks
      • that function call is removed
      • its result is given to the next function on the stack (the function that called it)
  • you can actually see this stack (called the "call stack"), when you get an error, e.g. we change a to return 10 + '':
    • when we run it we see:

      >>> mystery(2,3)
      Traceback (most recent call last):
       Python Shell, prompt 2, line 1
       # Used internally for debug sandbox under external interpreter
       File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 11, in <module>
       print("The mystery number is: " + str(c(num1, num2)))
       File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 8, in <module>
       return b(num1 * num2) - 1
       File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 5, in <module>
       return num + a()
       File "/Users/drk04747/classes/cs51a/examples/call_stack.py", line 2, in <module>
       if __name__ == '__main__':
      builtins.TypeError: unsupported operand type(s) for +: 'int' and 'str'
      
    • it's a little bit cryptic, but if you look on the right, you see the call from mystery, to c, to b and finally to a
  • write a function called factorial that takes a single parameter and returns the factorial of that number

    >>> factorial(1)
    1
    >>> factorial(2)
    2
    >>> factorial(3)
    6
    >>> factorial(8)
    40320
    
  • look at factorial_iterative in recursion.py
    • I'm guessing most people wrote it this way
    • does a loop from 2 up to n and multiplies the numbers
  • Another option is factorial_iterative2
    • Here we did a range through n, so i goes from 0, 1, …, n-1
    • In the body of the loop be multiply by i+1, i.e., by 1, 2, …, n
  • could we have done it any other way?

Recursion

  • a recursive function is defined with respect to itself
    • what does this mean?
      • somewhere inside the function, the function calls itself
      • just like any other function call
      • the recursive call should be on a "smaller" version of the problem
  • can we write factorial recursively?
    • key idea: try and break down the problem into some computation, plus a smaller subproblem that looks similar

      5! = 5 * 4 * 3 * 2 * 1
      5! = 5 * 4!
      
    • a first try:

      def factorial(n):
        return n * factorial(n-1)
      
    • what happens if we run this with say 5?

      5 * factorial(4)
        |
        V
        4 * factorial(3)
          |
          V
          3 * factorial(2)
            |
            V
            2 * factorial(1)
              |
              V
              1 * factorial(0)
                |
                V
                0 * factorial(-1)
                  ...
      
      • at some point we need to stop. this is called the "base case" for recursion
      • when is that?
        • when n = 1 (or if you want to account for 0!, at 0)
    • how can we change our function to do this?
  • look at the factorial function
    • first thing, check to see if we're at the base case (if n == 0)
      • if so, just return 1 (this lets 0! be 1)
    • otherwise, we fall into our recursive case:
      • n * factorial(n-1)

Writing recursive functions

  1. define what the function header is
    • what is the name of the function?
    • what parameters does the function take?
  2. define the recursive case
    • pretend you had a working version of your function, but it only works on smaller versions of your current problem, how could you write your function?
      • the recursive problem should be getting "smaller", by some definition of smaller, e.g
        • for smaller numbers (like in factorial)
        • for lists that are smaller/shorter
        • for strings that are shorter
    • other ideas:
      • sometimes define it in English first and then translate that into code
      • often nice to think about it mathematically, using equals
  3. define the base case
    • recursive calls should be making the problem "smaller"
    • what is the smallest (or simplest) problem? This is often the base case
  4. put it all together
    • first, check the base case
      • return something (or do something) for the base case
    • if the base case isn't true
      • calculate the problem using the recursive definition
      • return the answer
  5. recursion has a similar feel to "induction" in mathematics
    • proof by induction in mathematics:
      1. show something works the first time (base case)
      2. assume that it works for some time
      3. show it will work for the next time (i.e. time after "some time")
      4. therefore, it must work for all the times
    • You will become extremely familiar with induction in CS054!

Exercises

write a recursive function called rec_sum that takes a positive number as a parameter and calculates the sum of the numbers from 1 up to and including that number

  1. define what the function header is: def rec_sum(n)
  2. define the recursive case
    • ∑_{i=1}^n = 1+2+3+…+(n-1)+n = ???
      • can you rewrite this expression so that there's a sum on the right hand side (that's smaller?)
    • Another way to think about it: pretend like we have a function called rec_sum that we can use but only on smaller numbers: rec_sum(n) = ?????? rec_sum(?)
    • rec_sum(n) = n + rec_sum(n-1)
      • i.e. the sum of the numbers 1 through n, is n plus the sum of the numbers 1 through n-1
  3. define the base case
    • in each case the number is getting smaller
    • what's the smallest number we would ever want to have the sum of?
      • 0
    • what's the answer when it's 0?
      • 0
  4. put it all together! - look at the rec_sum function in recursion.py
    • check the base case first
      • if n == 0
    • otherwise
      • do exactly our recursive relationship

write a recursive function called rec_sum_list that takes a list of numbers as a parameter and calculates their sum

  1. define what the function header is def rec_sum_list(some_list)
  2. define the recursive case
    • pretend like we have a function called rec_sum_list that we can use but only on smaller strings
    • what would we get back if we called rec_sum_list on everything except the first element?
      • the sum of all of those elements
    • how would we get the sum to the entire list?
      • just add that element to the sum of the rest of the elements
    • the recursive relationship is: rec_sum_list(some_list) = some_list[0] + rec_sum_list(some_list[1:])
  3. define the base case
    • in each case the list is going to get shorter
    • eventually, it will be an empty list. what is the sum of an empty list?
      • 0
  4. put it all together! - look at the rec_sum_list function in recursion.py
    • check the base case first
      • if the list is empty
      • we could have also done if len(some_list) == 0
    • otherwise
      • do exactly our recursive relationship
  5. Why does this work?!?
    • Let's look at an example:

      rec_sum_list([1, 2, 3, 4])
        |
        V
        1 + rec_sum_list([2,3,4])
          |
          V
          2 + rec_sum_list([3, 4])
            |
            V
            3 + rec_sum_list([4])
              |
              V
              rec_sum_list([])
      
    • finally we hit the base case and we get our answer to rec_sum([]), which is 0, and then rec_sum([4]) can return its answer, etc.
    • We can actually see this happening by adding some print statements to our function (see rec_sum_list_print):
    • If we run this with [1, 2, 3, 4] we get:

      rec_sum_list([1, 2, 3, 4])
      Calling rec_sum_list: [1, 2, 3, 4]
      Calling rec_sum_list: [2, 3, 4]
      Calling rec_sum_list: [3, 4]
      Calling rec_sum_list: [4]
      Calling rec_sum_list: []
      base case returning 0
      [4] returning 4
      [3, 4] returning 7
      [2, 3, 4] returning 9
      [1, 2, 3, 4] returning 10
      10
      
      • like the diagram above, we see all the calls down to the recursive calls
      • then, the base case returns 0
      • then each of the successive calls slowly returns their answer

write a recursive function called reverse that takes a string as a parameter and reverses the string

  1. define what the function header is: def reverse(some_string)
  2. define the recursive case
    • pretend like we have a function called reverse that we can use but only on smaller strings
    • to reverse a string
      • remove the first character
      • reverse the remaining characters
      • put that first character at the end

        reverse(some_string) = reverse(some_string[1:]) + some_string[0]
        
  3. define the base case
    • in each case the string is going to get shorter
    • eventually, it will be an empty string. what is the reverse of the empty string?
      • ""
  4. look at reverse function in recursion.py
    • check the base case first: if the length of the string is 0
    • otherwise
      • call reverse again on the shorter version of the string
      • append on what is returned by some_string
  5. if we added a print statement to reverse to print out each call to reverse what would we see?
    • e.g. print("Reverse: " + some_string)

      >>> reverse("abcd")
      Reverse: abcd
      Reverse: bcd
      Reverse: cd
      Reverse: d
      Reverse:
      
    • we can also change the function to see what is being returned each time:

      >>> reverse("abcd")
      Reverse: abcd
      Reverse: bcd
      Reverse: cd
      Reverse: d
      Reverse:
      Returning:
      Returning: d
      Returning: dc
      Returning: dcb
      Returning: dcba
      
  6. to reverse the string "abcd", reverse is called four times recursively

What does mystery_recursion function in mystery_recursion.py do?

  • Recursive function
  • Work through a small example:
    • rec_mystery([2, 4, 3, 1])

      rec_mystery([2, 4, 3, 1]) # when recursive call finishes: compares m = 4 and l[0] = 2 and returns 4
      |
      |
      ---> rec_myster([4, 3, 1]) # when recursive call finishes: compares m = 3 and l[0] = 4 and returns 4
        |
        |
        -->rec_mystery([3, 1]) # when recursive call finishes: compares m = 1 and l[0] = 3 and returns 4
          |
          |
          --> rec_mystery([1]) # returns 1
      
  • what does this function do?
    • calculates the max!
  • how?
    1. rec_max(list)
    2. rec_max(list) = ??? rec_max(list[1:])
      • assume/trust that the recursive call works
      • if it does, then it will return the largest value in list[1:]
      • the largest value of the whole list is then either the first element (l[0]) or the largest value in the rest of the list (rec_max(list[1:])
      • recursive case:
        • make a recursive call on the rest of the list
        • store that value in m
        • compare m to the first element and return whichever it larger
    3. The list will get smaller and smaller. rec_max([]) doesn't really make sense, so our base case will be when there's a single element

write a function called power that takes a base and an exponent and returns base^exponent (i.e. the same thing as '' without using '')

  • you can assume that exponent >= 0
  • define what the function header is def power(base, exponent)
  • define the recursive case b^e = b * b^(e-1)
    • we can define the power function as the power function of the exponent - 1 times the base
  • define the base case
    • each time the exponent is getting smaller
    • eventually, the exponent will be 0
      • b^0 = 1
  • look at the power function in recursion.py code
    • check the base case when the exponent == 0
      • in this case just return 1
    • otherwise, do the recursive case
      • base * power(base, exponent-1)

look at the spiral function in turtle_recursion.py

  • for example, what would the picture look like if I called:

    >>> spiral(80, 50)
    
  • what does this function do?
    • recursive function
    • draws a spiral on the screen
      • forward 80
      • left 30
      • spiral( 76, 49 )
        • forward 76
        • left 30
        • spiral(72.2, 48)
          • forward 72.2
          • left 30
    • when does it stop?
      • when levels = 0=
        • I put a dot here just do make it explicit
        • we could have also just done nothing if we wanted
    • repeat 50 times:
      • forward length
      • left 30
      • reduce length by 5%
  • what if we wanted to end up back at the starting point, but we couldn't pick the pen up?
    • one way would be to trace our steps backward
    • Assume that the recursive call returns back to its starting point. What would we need to do to make sure that our call returned back to the starting point?
      • Add the following after the recursive call:

        right(30)
        backward(length)
        
    • if we run it now, we draw the spiral all the way down, and then we retrace backwards
  • why does this work?
    • each call to spiral retraces its own part after the recursive call
    • the stack keeps track of each of the recursive calls

run broccoli_demo function in turtle_recursion.py

  1. define what the function header is
    • broccoli(x, y, length, angle)
  2. define the recursive case
    • broccoli is a line with three other broccolis at the end
      • one directly straight out
      • one 20 degrees to the left
      • one 20 degrees to the right
    • the three other broccolis should be smaller/shorter than the current
  3. define the base case
    • eventually the length of the broccoli to be drawn gets too short
    • at that point instead of recursing, we draw a yellow dot at the end and stop recursing
  4. put it all together
    • look at the broccoli function
      • draw a line in the direction specified
      • check the base case
        • if the line length is less than 10, just put a yellow dot at the end
      • otherwise, it's the recursive case
        • draw three smaller broccolis at different angles
      • what are new_x and new_y?
        • the ending coordinates of the line being drawn
      • why do we need to save them?
        • after the first recursive call to broccoli the turtle won't be in the same place
  5. if we turn tracing back on, what will we see, that is, what order will it draw in?
    • going to go right (angle -20) over and over again until it gets too short
      • it will end the recursion then draw a short center
      • this also will be a base case and draw the left
    • then it will start to work it's way back up
    • eventually, it will make it back up to the top-level and start drawing the center stalk
    • after drawing the center stalk, it will draw the left stalk

Why does recursion work?

  • Specifically, why can we trust that the recursive call is going to work?
    • The base case works (assuming we coded it up correctly)
    • If the base case works, the the call before the base case works
    • If that works, then the call before that works.
    • Etc, etc.

Examples

  • Binary search: given a list of numbers and a target number, find the target number.
    • Start in the middle, see if smaller than target, if so check right side else check left
  • Binary space partitioning: want to find an item in a cluttered room. we have a special detector that tells us if it's to the left of us or the right of us.
  • Path planning: the best route from a to z includes the best route from (b1, b2, b3) to z. The best route from b1 to z includes the best route from (b1c1, b1c2, b1c3) to z. Etc.
  • Edit distance: how many changes do I need to make to turn string A into string B? Well, just look at the first pair of characters and then worry about the rest of the string.

Author: Joseph C. Osborn

Created: 2020-04-21 Tue 10:44

Validate