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 answera()
returns 10- which allows
b
to now return 16 - once
c
knows its call tob
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)
- as functions are called, the stack grows
- you can actually see this stack (called the "call stack"), when you get an error, e.g. we change
a
toreturn 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
, toc
, tob
and finally toa
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
- what does this mean?
- 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)
- first thing, check to see if we're at the base case (if n == 0)
Writing recursive functions
- define what the function header is
- what is the name of the function?
- what parameters does the function take?
- 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
- the recursive problem should be getting "smaller", by some definition of smaller, e.g
- other ideas:
- sometimes define it in English first and then translate that into code
- often nice to think about it mathematically, using equals
- 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?
- 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
- 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
- first, check the base case
- recursion has a similar feel to "induction" in mathematics
- proof by induction in mathematics:
- show something works the first time (base case)
- assume that it works for some time
- show it will work for the next time (i.e. time after "some time")
- therefore, it must work for all the times
- You will become extremely familiar with induction in CS054!
- proof by induction in mathematics:
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
- define what the function header is:
def rec_sum(n)
- 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
- ∑_{i=1}^n = 1+2+3+…+(n-1)+n = ???
- 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
- 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
- check the base case first
write a recursive function called rec_sum_list
that takes a list of numbers as a parameter and calculates their sum
- define what the function header is
def rec_sum_list(some_list)
- 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:])
- pretend like we have a function called
- 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
- 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
- check the base case first
- 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 thenrec_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
- define what the function header is:
def reverse(some_string)
- 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]
- pretend like we have a function called
- 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?
- ""
- 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
- 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
- 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?
rec_max(list)
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
- 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)
- check the base case when the exponent == 0
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
- when
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
- define what the function header is
broccoli(x, y, length, angle)
- 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
- broccoli is a line with three other broccolis at the end
- 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
- 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
andnew_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
- look at the
- 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
- going to go right (angle -20) over and over again until it gets too short
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.