CS100 Review Session
30 July 1999
I went through many examples on recursion. Hopefully this will help you understand recursion better. A recursive function is a function that calls itself (either directly or indirectly). You have learned about QuickSort, which is a recursive function.
There are three rules to follow when you write a recursive function.
Rule 1: Always include a terminal case.
The terminal case makes sure that the function eventually terminates. A recursive function without a terminal case will call itself forever and ever, and will never stop.
The terminal case is usually a case where the input is the "simplest". For example, in the case of QuickSort, the terminal case occurs when the input is an array of size 0, 1 (do nothing) or 2 (swap if necessary).
Rule 2: Each call to the recursive method should progress towards the terminal case.
This ensures the terminal case will eventually be reached and terminates the function. For example, in the case of QuickSort, every call to QuickSort reduces the size of the input.
Rule 3: Assume that your function works, when you write your function.
Since a recursive function calls itself, you have to assume that you function is already working perfectly when you are still writing your function.
Write a function sum that takes in a positive integer n, and return the sum of 1 + 2 + … + (n – 1) + n.
During the review session, I only wrote in pseudocode (half English, half Java).
Sum (n) = 1 + 2 + … + n.
First we look for a pattern.
Sum (n) = 1 + 2 + … + n-1 + n = Sum(n-1) + n
We just expressed the function Sum in terms of itself. This is our recursive function (assuming that SUM(n-1) return the correct thing).
SUM(n)
return SUM(n – 1) + n
But this is not complete yet, because we do not have any terminal case. So let’s think about the terminal case. Terminal cases always consider the simplest input. In this case the simplest input is 1. SUM(1) = 1. Therefore, our SUM function should look like :
SUM(n)
if n is 1
return 1
else
return SUM(n – 1) + n
Now that we have a terminal case, we should check if we satisfied rule 2. The answer is yes, because each call to SUM reduces the input by 1 (progress towards the terminal value).
Let’s trace through the code. Let say we call SUM(3). The argument n will be 3. Since n is not equals to 1, we return SUM(2) + 3. SUM(2) in turns call SUM(1) + 2. SUM(1) returns 1 (terminal case). So SUM(2) returns (1 + 2), and SUM(3) returns (1 + 2 + 3).
This is very similar to the summation example. Here we want to calculate the factorial of a function. Given a non-negative integer n, returns 1*2*3 .. * n. The only tricky part is the terminal case. Since 0! is defined as 1, the terminal case should be when n is 0.
FAC(n)
if n is 0
return 1
else
return FAC(n – 1) * n
The Fibonacci series is the series 0 1 1 2 3 5 8 …. Where the first two numbers are fixed as 0 and 1, and the subsequent numbers are the sum of two previous numbers. (8 = 5 + 3, 5 = 3 + 2 …). Mathematically, the nth Fibonacci number is defined as FIB(n) = FIB(n-1) + FIB(n-2). Now, let’s write the recursive function for finding the nth Fibonacci number.
FIB(n)
return FIB(n-1) + FIB(n-2)
This is the easy part since the this is just the formula of the Fibonacci number. We must think about the terminal case. What is the simplest input to our function ? 0 ? 1 ? 2 ? . We now the first two number are fixed. So FIB(0) is 0 and FIB(1) is 1.
FIB(n)
if n is 0
return 0
else if n is 1
return 1
else
return FIB(n-1) + FIB(n-2)
Note on Efficiency : Recursive functions are short and elegant. But they may not be efficient. The Fibonacci function, for example, is not efficient because it creates an exponential number of call frames in the memory. Furthermore, a lot of work done by Fibonacci is repeated work.
Suppose we want to write a function REVERSE that takes in a string and return the reverse of the string. So REVERSE("CS100") would returns "001SC". First, we assume that we already wrote such a function. Then we think about how we can use the function, to write the function. (this sounds like magic!).
So, let’s assume we already have a REVERSE function. To reverse a string, we just need to remove the first character of the string, REVERSE the rest of the string, and append the first character at the end of the reverse string.
REVERSE("CS100") = REVERSE("S100") + "C" = "001S" + "C" = "001SC"
Now, let’s think about the terminal case. What is the simplest form of input ? An empty string ! "". REVERSE("") is "". Here is the code for REVERSE :
1 REVERSE(s)
2 if s is empty
3 return empty string
4 else
5 c = first character of s
6 s’ = rest of s
7 return REVERSE(s’) + c;
Now, let’s think a little bit harder. How can we set c and s’ to the first character of s and the rest of s ? To do that, we need an index into the string. Let’s call that index i. Index i points to the beginning of the substring we are interested in reversing. So a substring of s can be represented by the pair (s, i). For example, the pair ("Cornell", 2) represents the substring "rnell". Since this index needs to be shared between different calls to REVERSE, we need to pass it as a parameter.
In this situation, instead of modifying the original interface, it is a good idea to create a second method, called REVERSE2, which takes in a string s and an index i. REVERSE2 should return the reverse of the substring starting from index I till the end of the string. For example, REVERSE2("CS100", 2) should return "001".
Let’s rewrite the function REVERSE above. To check if a string is empty, we just check if i has reached the end of the string (line 2). In Line 5, the "first" character of the string should now be the i-th character of the string s. And Line 6, s’ is equivalent to the pair (s, i+1).
REVERSE2(s, i)
if i is length of s
return empty string
else
c = i-th character of s
return REVERSE(s’, i+1) + c;
Now, let go back to our original function REVERSE, which should just return the value returned by REVERSE2.
REVERSE(s)
return REVERSE2(s, 0)
Check for violation of Rule 2 : Terminal case happens when i is the length of s. We start with i equals 0. Every call of i, we increment i by 1. So eventually i will equal to length of s. We are OK!
Here, we defined a tiny simple language called "expression" and write a small recursive verifier for it.
An expression consists of only 4 symbols "a", "(", ")" and "!". The definition of an expression is as follows : "a" is an expression. An expression, surrounded by a pair of bracket, is still an expression. An expression, prefixed by "!" is still an expression. In mathematical notation :
EXPR = a
EXPR = (EXPR)
EXPR = !EXPR
We want to write a method that takes in a string and return true if the string is an expression. What is the terminal case ? The simplest string is an empty string, which is not an expression. The next simplest thing is "a" (which is an expression) and other single-character strings (which is not an expression). So the function looks something like :
ISEXPR(s)
if s is empty
return false
else if s is "a"
return true
else if s has a length of 1
return false
:
:
We also know that an expression may begins with a ! or surrounded by ( ). If we recognize these condition, we should recursively check if the rest of the string is an expression. Otherwise, we can be sure that the input is not an expression.
1 ISEXPR(s)
2 if s is empty
3 return false
4 else if s is "a"
5 return true
6 else if s has a length of 1
7 return false
8 else if first character of s is !
9 return ISEXPR(rest of s)
10 else if s is surrounded by ( and )
11 return ISEXPR(middle of s)
12 else
13 return false
Just like QuickSort and REVERSE, here we need to recursively process on a sub-array/substring of the original input. So we need to represents a substring using the three things, the original string, the left boundary and the right boundary (s, i, j). So lets modify the method to takes in three parameters and give it a different name. I will represent a substring s with (s’, i, j) where s’ is the original string, i is the left boundary and j is the right boundary.
ISEXPR2(s’, i, j)
if j < i
return false
else if i equals j and i-th char of s’ is ‘a"
return true
else if i equals j
return false
else if i-th character of s’ is !
return ISEXPR(s’, i+1, j)
else if i-th char of s’ is ( and j-th char is )
return ISEXPR(s’, i+1, j-1)
else
return false
And we are done ! You should verify yourself that this function progresses towards the terminal case.
ISEXPR(s)
return ISEXPR2(s, 0, s.length() – 1)