CS51A - Fall 2019 - Class 11

Example code in this lecture

   recursion.py
   mystery_recursion.py
   turtle_recursion.py

Lecture notes

  • administrative

  • 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

       - 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

  • 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
          - sum_{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 code
          - 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 sum(some_list)

       2. define the recursive case
          - pretend like we have a function called sum that we can use but only on smaller strings
          - what would we get back if we called sum 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 code
          - 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

       - 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 in recursion.py code):
          - 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 code
          - 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 code 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:])
          3. The list will get smaller and smaller. max([]) doesn't really make sense, so our base case will be when there's a single element

          
       - 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
       

  • look at the spiral function in turtle_recursion.py code
       - 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 code
       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 broccoli function in turtle_recursion.py code
             - 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

       - 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.


























  • 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

       1. define what the function header is
          def power(base, exponent)

       2. 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

       3. define the base case
          - each time the exponent is getting smaller
          - eventually, the exponent will be 0
             - b^0 = 1

       4. 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)