Maggie Johnson Handout
#29
CS103A
Key topics:
* Recursive Definitions
* Recursive Subprograms
* Proving Properties of Recursive
Programs
Recursive Definitions
One of the most
important tasks in computer science is to discover and characterize regular
patterns, such as those associated with processes that are repeated. In math, as we have seen, such patterns are
called sequences. There are three ways to define a sequence:
1) enumerate the items in the
sequence and hope the pattern is obvious;
2) give a formula for the elements
in the sequence, for example, the sequence of powers
of 2 is given by a(n) = 2n;
3) give a recursive or inductive
definition with the following parts:
a) the initial condition or basis which defines
the first (or first few) elements of
the sequence;
b) an inductive step in which later terms in
the sequence are defined in terms of earlier terms. This is called a recurrence relation.
Here are lots of
examples of these types of definitions (mostly recurrence relations):
1) We’ll start with the obvious: The set of even numbers can
be defined as:
1) 2, 4, 6, 8, 10....
2) 2k for some integer k
3) basis:
2 is in EVEN
induction: if x is in
EVEN then so is x + 2
2) Compound interest can be defined recursively as:
basis: A(0) = initial amount
induction: A(k) = interest rate *
A(k-1)
For example, if the initial amount
is $1000 and the interest rate is 1.055, after 21 years we get:
A(0) = 1000
A(1) = 1.055 * A(0) =
1055.00
A(2) = 1.055 * A(1) =
1113.02
...
A(20) = 1.055 * A(19) =
2917.76
A(21) = 1.055 * A(20) =
3078.23
3) The product of the positive integers from 1 to n,
inclusive, called "n factorial"
is denoted by n!: multiply 1 * 2
* ... * n = n! (this is an iterative definition).
To devise a
recursive definition, notice:
1! = 1 2!
= 1 * 2 = 2 3! = 1 * 2 * 3 =
6 4! = 1 * 2 * 3 * 4 = 24
Observe that 4!
= 4 * 3! or in general: n! = n * (n - 1)!. Factorial is defined recursively as follows:
basis: 1! = 1
induction: n! = n * (n - 1)!
For example: 4! = 4 * 3!
3! = 3 * 2!
2!
= 2 * 1!
1!
= 1
2!
= 2 * 1
3! = 3 * 2
4! = 4 * 6
By the way,
inductive proofs come in very handy when we are working with recursive
definitions. For example, we can prove
using induction that the recursive definition of factorial is equivalent to the
iterative one:
Prove P(n): n! as defined
recursively equals 1 * 2 * ... * n
i) base case: prove that P(1) is true: 1!=1 and 1 * 1 = 1.
ii) induction hypothesis is to
assume P(k): n! as defined recursively equals 1 * 2 * ... * n
and show P(k+1): (n + 1)! as defined
recursively equals 1 * 2 * ... * n * (n + 1).
PROOF:
(n + 1)! = (n + 1) * ((n + 1) - 1)! substitution
of (n+1) in recursive definition
(n + 1) * n! subtraction
n! * (n + 1) commutative law
for *
(1 * 2 * ...
* n) * (n + 1) inductive
hypothesis
P(k+1) is true when P(k) is true,
and therefore the recursive and iterative definitions are equivalent.
4) Give a recursive definition of an where a is
a nonzero integer and n is a nonnegative integer.
basis: a0 = 1
induction: a(n+1) = an
* a
5) The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21,
34... i.e., each element is the sum of
the two preceding elements is defined recursively:
basis: F(0) = 0, F(1) = 1
induction: F(n) = F(n-1) + F(n-2)
Induction
also comes in handy when we need to prove properties of sequences and
patterns. We saw this in the Induction
handout. Here’s an example: Using
strong induction, we will prove that in the Fibonacci sequence, F(n+4) =
3F(n+2) - F(n) for all n >= 1. Not
something you may have observed in the Fibonacci series, but true
nonetheless...
Prove P(n): in the Fibonacci sequence,
F(n+4) = 3F(n+2) - F(n) for all n >= 1
i) base case: prove that P(1) and P(2) are true:
F(1+4) = 3F(1+2) - F(1)
= 3*2 - 1 = 5 F(5) = 5
F(2+4) = 3F(2+2) - F(2)
= 3*3 - 1 = 8 F(6) = 8
ii) induction hypothesis is to
assume P(k): Assume that for all positive numbers i where i <= k: F(i+4) = 3F(i+2) - F(i) and
show P(k+1): F(k+1+4) = 3F(k+1+2) - F(k+1)
or F(k+5) = 3F(k+3) - F(k+1)
PROOF:
We know from the definition of the
Fibonacci sequence that F(k+5) = F(k+3) + F(k+4). By the inductive hypothesis with i = k -1 and i = k
respectively,
F(k+3) = 3F(k+1) -
F(k-1)
and
F(k+4) = 3F(k+2) - F(k)
Therefore, F(k+5) = 3F(k+1) - F(k-1) + 3F(k+2) - F(k)
3[F(k+1)
+ F(k+2)] - [F(k-1) + F(k)]
3F(k+3)
- F(k+1) substitution
of inductive part of
recursive definition
P(k+1) is true when P(k) is true,
and therefore P(n) is true for all integers >= 1.
6) Arithmetic expressions can be defined recursively as
follows:
basis: The following entities are
arithmetic expressions:
- variables
- integers
- real numbers
induction: if E1 and E2 are arithmetic expressions, then the following are
also arithmetic expressions:
1. (E1 + E2)
2. (E1 - E2)
3. (E1 * E2)
4. (E1 / E2)
5. if E is an arithmetic expression, so is
(-E).
Show that (y * (-(x + 10))) is an arithmetic expression:
i) x basis
ii) 10 basis
iii) (x
+ 10) induction
rule 1
iv) (-(x
+ 10)) induction rule 5
v) y basis
vi) (y
* (-(x + 10))) induction rule 3
Feeling frustrated with how long it takes to get email over his local network, Fred decides to fight back. He and a group of his friends decide to bring the system to its knees on a specified date in the near future. The way they are going to do this is Fred will send an email to 10 of his friends. In this email, he will ask each of his 10 friends to pass the same email on to 10 of their friends (all subscribers to the same network), etc., etc. So how long do you suppose it will take to flood the system, assuming everyone plays their part perfectly?
Prove: Fn < 2n for all
integers n >= 1
A gambler decides to play successive
games of blackjack until he loses 3 times in a row. Thus, he could play five games by losing the first, winning the
second and losing the next three (LWLLL); or, by winning the first two and
losing the next three (LLWWW). These
are the only two ways to play five games. Let gn be the number of ways the
gambler can play n games.
Find g3, g4, and g5. Find g6.
Find a recurrence relation for gk.
A single line divides a plane into two regions. Two lines (by crossing) can divide a plane into four regions; three lines into seven regions. Let Pn be the maximum number of regions into which n lines divide a plane, where n is a positive integer. Derive a recurrence relation for Pk.
Recursively define the set of bit strings that have more 0's than 1's.
Recursive Subprograms
So much for the
mathematical side of recursion. As
computer scientists, we might be more interested in how recursion can be used
in programming. As you may know,
recursion in programming has a very basic form. As a programming technique, recursion is when a procedure or
function calls itself either directly or indirectly. To keep such a procedure or function from calling itself
indefinitely, a recursive subprogram must have two properties:
1) There must be some base condition
for which the subprogram does not call itself.
2) Each time the subprogram calls
itself, it must converge on the base condition.
Basically,
recursion is a technique for performing a task T by performing another task
T'. T' has exactly the same nature as
the original T (we are calling the same
procedure or function), but it is in some sense smaller than T. Recursion is another way of doing iteration
in a program. In fact, any iterative
code can be rewritten as recursive code.
Sometimes recursion is more elegant and efficient than using loops,
sometimes not, so most programmers use a mix of both techniques.
Consider the
following silly function called Sum which returns the sum of the first n
integers. This recursive function takes
advantage of the fact that adding 4+3+2+1 is the same as adding 4 and the sum
of 3+2+1:
int Sum(int n)
/* using
recursion */
{
if (n == 1)
return(1);
else
return(n + sum(n - 1));
}
Calling this
function with n = 4 gives the following execution:
Sum(4);
Sum(3);
Sum(2);
Sum(1);
sumN
= 1;
sumN = 2 +
1;
sumN = 3 + 3;
sumN = 4 + 6 { return 10 }
No computer scientist
in his or her right mind would ever implement sum in this manner due to the
overhead of making recursive calls. Not
to mention the fact that we have Gauss’ neat little formula that can give us
the sum directly (n(n+1)/2).
Give a recursive algorithm for
computing an:
Give a recursive algorithm for finding the minimum of a set of integers:
Give a recursive algorithm for finding the reversal of a bit string:
Give iterative and recursive algorithms for finding the nth term of the sequence defined by: a0 = 1, a1 = 3, a2 = 5, an = an-1 * a2n-2 * a3n-3. Which is more efficient?
Binary Search
Recall that a
binary search (the "telephone directory" search) consists of
comparing the item to be found with the element in the middle of the list. If the search item is less than the item in
the middle, we limit our search to the first half of the list. If the search item is greater than the item
in the middle, we limit our search to the second half of the list. Otherwise, the item is equal to the middle
element and the item is found. If the
item is not found, we do the exact same process again with a new middle
element, this one being in the middle of the first or last half of the original
list. We continue doing this until the
item is found. Notice that the size of the list that we are searching is cut in
half each time. Therefore, we are doing
exactly the same type of search but the size of the input has gotten much
smaller. This is an example of what we
in the computer science racket call “divide and conquer!”.
int
binsearch(int x, int v[], int low, int high)
/*
recursive binary search: find x in
v[low]..v[high]; return index of location
*/
{
int
mid, loc;
mid
= (low + high) / 2;
if
(x == v[mid])
return(mid);
else
if ((x < v[mid]) && (low < mid))
return(binsearch(x, v, low, mid-1));
else
if ((x > v[mid]) && (high >
mid))
return(binsearch(x, v, mid+1,
high));
else
return(-1);
}
What does the following function
output? ______________________________
int XX(int n, int a)
{
if
(n == 1)
return(a
* a);
else
return((XX(n-1,
a)) * (XX(n-1, a)));
}
Proving Properties of Recursive Programs
Proving the
correctness of recursive programs is similar to proving the correctness of a
loop. We use induction to prove the
subprogram works for the base case, i.e., if the subprogram is called just
once; then we assume it works for k
calls, and prove that it works for k+1 calls.
For example, to prove the factorial function correct:
int
Fact(int n)
{
1) if (n == 1)
2) return(1);
3) else
4) return(n * Fact(n - 1));
}
Prove: When called with argument i
for parameter n, Fact returns i!
i)
base case: if the condition "n ==
1" is true (i = 1), then the basis (line 2) is executed and Fact is
assigned the value of 1. Therefore
Fact(1) returns the value of 1.
ii)
Assume the induction hypothesis: We assume that when called with an argument i
> 1, Fact returns i!. We must show
that Fact(i+1) returns (i+1)!.
PROOF:
If
i > 1 then i+1 is at least 2, so the inductive part of the
definition (line 4) is executed. Now, n
= i+1 so the returned value is:
(i+1) * Fact(i)
(i+1) * i! inductive hypothesis
substitution
(i+1)! recursive definition
of factorial
The k+1 recursive call has been
verified when the k recursive call was correct, and therefore the recursive function is correct.
The binary
search function can also be proven correct using induction. We will do strong induction on the length of
array v. What we want to prove:
The function binsearch returns the
index location of item x in array v which has bounds of low and high; or, it returns -1 if item x is not in the
array.
int
binsearch(int x, int v[], int low, int high)
/*
recursive binary search: find x in
v[low]..v[high]; return index of location
*/
{
int mid, loc;
1) mid = (low + high) / 2;
2) if (x == v[mid])
3) return(mid);
4) else if ((x < v[mid]) &&
(low < mid))
5) return(binsearch(x, v, low,
mid-1));
6) else if ((x > v[mid]) &&
(high > mid))
7) return(binsearch(x, v,
mid+1, high));
8) else
9) return(-1);
}
i)
base case: The base case is when the
function is called only once. There are
two possibilities: either the array is empty or the array has 1 element. If we
have an empty array; mid has a value of
0 and the final else test (line 9) is executed. -1 is returned because x is not
found. The other base case is if
the array has one element. Mid has a value of 1. If x is equal to the only item
in the array, line 3 is executed & binsearch returns the index location of
x, being 1. If x is not equal to the
only element in the list, line 9 is executed and binsearch returns -1. Thus,
binsearch works properly for the two base cases.
ii)
Assume the induction hypothesis: Assume that binsearch works properly for
arrays of size 0, 1, ..., n. We must
show it works properly for an array of n+1 elements.
PROOF:
The
original call to the function is with upperbound+1, or lowerbound-1 (we have
n+1 elements). If the item x is at the
middle index location, we execute the first base case. If high > mid or low < mid, the item x
is not in the subarray, and we execute the other base case. Otherwise, the
array is split in half and we continue the search in one half or the other. The
length of this new list cannot be as large as n+1; in fact, its size is covered
by the inductive hypothesis. The
inductive hypothesis holds and the functions works for the smaller arrays.
Thus,
since binsearch works for an array of size n+1 when we assume it works for an
array of size n, binsearch works on an array of any size. Just like magic!
Bibliography
D. Barron, Recursive Techniques in Programming, New
York: American Elsevier, 1968.
R. Bird,
"Improving Programs by the Introduction of Recursion," Communications of the ACM, 20:11, (Nov., 1977), pp. 434-39.
D. Hofstadter, Godel, Escher, Bach: An Eternal Golden Braid,
New York: Basic Books, 1979.
D. Knuth, The Art of Computer Programming, 2nd Ed.,
Menlo Park, CA: Addision Wesley, 1981.
Z. Manna and R.
Waldinger, The Logical Basis for
Programming, Reading, MA: Addision Wesley, 1990.
E. Roberts, Thinking Recursively, New York: Wiley,
1986.
M. Wand, Induction, Recursion and Programming,
New York: North-Holland, 1980.
Historical Notes
Recursion was
studied as early as 1202 by Leonardo Fibonacci (c.1170-c.1240) where we find
probably the earliest example of a recursively defined sequence: the Fibonacci
sequence (see above) was defined in terms of the reproductive characteristics
of a group of rabbits. This sequence
was studied by many later mathematicians who used it to develop methods for
defining a formula for Fn involving only n. This was done by Abraham de Moivre
(1667-1754) and independently by Daniel Bernoulli (1700-1782) and Jacques Binet
(1786-1856). By the way, the formula
is:

This group of
mathematicians along with Eduard Lucas (1842-1891) created and studied many
recursively defined sequences. A formula for calculating the nth
term of such a sequence is called a generating
function, and the methods these mathematicians developed can be used to
find such a formula for nearly any sequence.
By the way, Lucas also created the Towers of Hanoi game, a very famous
example of recursion (we will study this later in the quarter).
The systematic
study of recursion began in the 1920's when mathematical logicians began to
treat questions of decidability and computability. All the common functions of number theory, it was shown, could be
defined recursively. This was the
beginning of an important area of computability: recursive function theory.
Stephen Kleene introduced this concept in "General Recursive
Functions of Natural Numbers," American
Journal of Mathematics, 57 (1935), and wrote the standard reference on this
theory in 1952 (Introduction to
Metamathematics, Princeton, NJ: Van Nostrand).
The first
language to use recursive subroutines was IPL (Newell, Shaw and Simon). The language was used for their Logic
Theorist system, a program that could prove propositional logic theorems. This is considered to be one of the very
first AI programs. The first language
to provide an automatic mechanism for recursion was Lisp. Most imperative languages after Algol-60
allowed recursion.
Binary search
was first mentioned by John Mauchly (of ENIAC and UNIVAC fame). The method became well known in the 50's,
but no one seemed to have worked out the details of what should be done when N
is not a power of 2. H. Bottenbruch was
apparently the first to publish a binary search algorithm that works for all N
in 1962.