CS51A - Spring 2025 - Class 12

Example code in this lecture

   call_stack.py
   recursion.py

Lecture notes

  • administrative
       - midterm next Thursday (10/10)
          - in class
          - paper-based
          - can bring in two pages of notes (either two pieces of paper, single-side or one piece, double-sided)
          - problems like practice problems
             - coding
             - what's wrong with this function
             - what would this function do
             - is this valid?
             - what would the output be
             - ...
          - practice writing code on paper (it's different than on the computer)
          - Practice problems posted
          - Midterm will cover everything through dictionaries (not recursion)
       - ChatGPT (and similar tools): what's appropriate
          - From the syllabus:
          "ChatGPT (and other similar tools): For this class, you should be writing all of your code on your own without any additional tools beyond PyCharm. ChatGPT (and similar tools) should not be used on the assignments in this class in any way (either to generate code or to assist you in debugging code, etc.). You may use it to help you understand concepts (e.g., to help give an explanation of a topic/idea covered in class) as well as to help study for exams (e.g., by having it generate sample questions).

  • the call stack
       - what is displayed if we call mystery(2, 3) in call_stack.py code?
          - 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 function 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 in recursion.py
          - 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 factorial function in recursion.py code
          - 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

       - 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