Chapter 6: Believe the Leaves
Before we begin this chapter, let's make sure that we're all on the same page. You should be familiar with base cases, recursive calls, the splitting strategy, and the counting strategy.
Last updated
Before we begin this chapter, let's make sure that we're all on the same page. You should be familiar with base cases, recursive calls, the splitting strategy, and the counting strategy.
Last updated
Yum, a chocolate bar! Who doesn't love chocolate! Well, unfortunately, you were hit in the jaw and you can only eat small, bite-sized pieces of chocolate. Sadly, our chocolate bar is a bit larger than "bite-sized", so we're going to have to break it up somehow.
Of course, we're going to solve this problem recursively. You might be thinking that we could solve this problem the same way we did with the gold bar. We could separate it into (bite-sized + rest of the bar), and eventually eat the entire bar. You'd be correct! However, we are already familiar with this method, so we'll try to solve it a different way.
Construct the function devour, which takes in a choco_bar as a parameter and aims to eat the entire bar, returning how many bite-sized pieces were consumed.
I know I want to make the problem smaller, but I don't want to use the method we've used before. It's kind of hard to break off exactly a bite-sized piece from the bar, so why don't we do something easier: break the chocolate bar in half.
Now, we have two smaller pieces of chocolate, but neither one is bite-sized yet. However, we've still managed to make our problem smaller, which is always good when solving things with recursion. Let's translate this into code talk:
What does this do? It takes our chocolate bar, divides into half, and then attempts to devour the left half, then the right half.
We've managed to divide our problem into two problems that both cannot be readily solved, but this is what is important:
both problems are smaller
both problems can be split further in a similar way.
combined, both problems are equivalent to the original problem.
We put our faith into the fact that eventually, the problem will be reduced until it can be solved easily.
Let's consider what happens next:
First, the left half is devoured, and split in half. Then, the right half is devoured, and split in half. This leaves us with 4 bite-sized pieces of chocolate! This is our base case: the problem can be immediately solved!
We are now dividing our problems into two problems that we only know are smaller than the original problem, but together, are equivalent to the original problem. We take a leap of faith and assume that eventually, the problem will be small enough to solve.
As usual, here's a visual recap:
If you're thinking to yourself, "OK, that problem kind of made sense. The leap of faith is kind of scary, but I can kind of see how it works...", then that's great! This was an example of tree recursion, which is pretty infamous for being difficult. Now that we've covered a theoretical example, it's about to get a bit more difficult with a concrete example: the Fibonacci numbers.
Before I introduce the problem, I'll explain what the Fibonacci numbers are.
The Fibonacci numbers are a sequence of integers that are produced in the following way:
We start with the 0-th and 1st numbers in the sequence: 0 and 1, respectively.
We produce the 2nd number by adding these numbers together:
Leaving us with this set of Fibonacci numbers:
We produce the 3rd number by adding the 2 most recent numbers together.
This leaves us with another set of Fibonacci numbers.
We do the same thing to get the 4th number: add the 2 most recent numbers together.
Leaving us with:
We keep doing this over and over and over again...there are infinite Fibonacci numbers!
Now that we understand what Fibonacci numbers are, I'll introduce the problem.
Given the Fibonacci numbers, write a function to find the n-th Fibonacci number.
We will define a function fib to do exactly this. fib accepts a parameter, n, and returns the n-th Fibonacci number.
For example, when n = 5, we want to return the fifth Fibonacci number, which is 5.
Let's define fib:
As a reminder, with our new strategy, we want to divide our problem into two problems with the following properties:
both problems are smaller
both problems can be split further in a similar way
combined, both problems are equivalent to the original problem.
How can we make our problem smaller? Well, let's consider how each fibonacci number is generated:
Consider the case where n = 5:
The n-th Fibonacci number is 5. The (n-1)th Fibonacci number is 3. The (n-2)th Fibonacci is 2.
fib(5) = fib(5-1) + fib(5-2)
fib(5) = fib(4) + fib(3)
5 = 3 + 2
In general, the n-th Fibonacci number is equal to the sum of:
The (n-1)th Fibonacci number
The (n-2)th Fibonacci number
Does this satisfy the properties of our new strategy? Yes! To verify:
both problems are smaller: yes, we are decreasing n in both problems
both problems can be split further in a similar way: yes, both are trying to find fib as well, just at a smaller n
combined, both problems are equivalent to the original problem: yes, by definition, they are equivalent.
This seems like a decent try, so let's take our leap of faith and put it into code:
This looks pretty good! Of course, we only have our recursive calls, so we need base cases.
When is our problem instantly solvable?
As a reminder, we start our Fibonacci numbers with our 0th and 1st number: 0 and 1.
Without any calculation, we know that fib(0) = 0, and fib(1) = 1. These seem like good base cases.
That looks pretty good! Of course, it's difficult to visualize this function working, so I'll provide a visual recap so you can see what's going on:
You can count up the 1's to see that, in fact, fib(5) = 5.
(Note that this is a terrible way to compute fibonacci numbers: notice that we re-calculate fib(3) twice, and we re-calculate fib(2) 3 times. That's a waste of time! You'll learn a method called memoization that removes this re-calculation later.)
It's not the most difficult task in the world to look at fib, plug in a few values of n, and see how it works. However, if you understand how we came up with the recursive calls, as well as why it works, then you're well prepared to start applying tree recursion to other problems!
If you're taking CS61A, then at this point, you should now know enough recursion to tackle midterm 1.There is no substitute for practice, so please do lots of old midterm problems! (I won't provide any practice problems or solutions because a lot of the recursion in CS61A is tied to higher-order functions, which is out of scope of this gitbook.)