Recursion and Induction
(Some content adapted from Critchlow & Eck)
A common programming technique that you should have encountered before is recursion. One way to think of recursion is as an application of the basic approach of breaking a programming task into smaller tasks, often called stepwise refinement or top-down design. If each smaller task is split off into its own function (called functional decomposition by the same people who use fancy terms like stepwise refinement), then recursion occurs naturally when one of the smaller tasks turns out to be a simpler version of the original task.
For example, consider the problem of finding the smallest element in a list of numbers. If the list only has a single element, then the answer is obvious: that one element must be the smallest. However, if the list is longer, then we can break the job into several steps. One possible candidate for the smallest element is the first number in the list; call it . After separating from the rest of the list, we still have a list of numbers; another possible candidate is whatever the smallest element is among those remaining numbers. By naming and defining a function for the job "find the smallest element in a list", we can make use of it to find the smallest of the numbers; call it . Finally, we may compare our two candidates, and , and return the smaller as the smallest of the entire list.
Here is a pseudocode rendition of the above solution:
to findSmallest(myList):
if size(myList) == 1 then:
return first(myList)
else:
let x = first(myList)
let restOfMyList = allButFirst(myList)
let y = findSmallest(restOfMyList)
if x <= y then:
return x
else:
return y
This assumes that we already have functions size, first, and allButFirst to handle those simple tasks on lists, presumably as part of our list data structure.
The manner in which we decompose a problem into smaller problems may depend on the tools we have.
For example, if instead of the function allButFirst we had a function split that would divide a list into two smaller lists (perhaps, but not necessarily, of roughly equal size), then we might prefer the following solution (called findSmallest2 to distinguish it from the original):
to findSmallest2(myList):
if size(myList) == 1 then:
return first(myList)
else:
let front, back = split(myList)
let x = findSmallest2(front)
let y = findSmallest2(back)
if x <= y then:
return x
else:
return y
In general, compound data structures will come with one or more ways to decompose them into smaller structures, and those can guide us when looking for ways to decompose problems that work with such data.
When the data is small enough to be handled directly without further composition, we will refer to it as a base case.
In the example above, the base case occurs when the list has only one element; for other problems, an appropriate base case might be an empty list, or a list with exactly two elements (for example, it doesn't make sense to call the split function with fewer than two elements, so that would be a reasonable choice when defining split itself).
The general strategy for recursion then is to first check whether the problem is simple enough to be solved directly, in a base case. If not, then decompose the problem into smaller subproblems; some of them might be handled by existing functions, while others might require defining new functions. If any subproblem looks like a smaller version of the original problem, then handle it by making a recursive call to the function itself. As long as the subproblems are truly smaller, this process must eventually terminate with calls to solve one or more base cases.
Recursion Examples
Factorial
A standard example of a recursive subroutine is a function that computes for a non-negative integer . , which is read " factorial," is defined as follows:
For example, . Note that for ,
It is also true that when . This observation makes it possible to write a recursive function to compute .
Here is how we might write it in Java:
/**
* Compute n!.
* Precondition: n >= 0
*/
int factorial(int n) {
int answer;
if (n == 0) {
answer = 1;
} else {
answer = factorial(n - 1) * n;
}
return answer;
}
Here is an equivalent program in Scala:
def factorial(n: Int): BigInt = {
if n == 0 then 1
else factorial(n - 1) * n
}
factorial(5)
// res0: BigInt = 120
In order to compute factorial() for , this function first computes factorial() by calling itself recursively. The answer from that computation is then multiplied by to give the value of . The recursion has a base case, namely the case when . For the base case, the answer is computed directly rather than by using recursion. The base case prevents the recursion from continuing forever, in an infinite chain of recursive calls.
Now, as it happens, recursion is not the best way to compute .
It can be computed more efficiently using a loop. Furthermore,
except for small values of , the value of is outside the
range of numbers that can be represented as 32-bit ints (this is why
the Scala version above declares that it returns a BigInt).
Towers of Hanoi
Another standard example of recursion is the Towers of Hanoi problem. Let be a positive integer. Imagine a set of disks of decreasing size, piled up in order of size, with the largest disk on the bottom and the smallest disk on top. The problem is to move this tower of disks to a second pile, following certain rules: Only one disk can be moved at a time, and a disk can only be placed on top of another disk if the disk on top is smaller. While the disks are being moved from the first pile to the second pile, disks can be kept in a third, spare pile. All the disks must at all times be in one of the three piles, except for a disk being moved. For example, if there are two disks, the problem can be solved by the following sequence of moves:
- Move disk 1 from pile 1 to pile 3
- Move disk 2 from pile 1 to pile 2
- Move disk 1 from pile 3 to pile 2
A simple recursive subroutine can be used to write out the list of moves to solve the problem for any value of . The recursion is based on the observation that for , the problem can be solved as follows: Move disks from pile number 1 to pile number 3 (using pile number 2 as a spare). Then move the largest disk, disk number , from pile number 1 to pile number 2. Finally, move the disks from pile number 3 to pile number 2, putting them on top of the disk (using pile number 1 as a spare). In both cases, the problem of moving disks is a smaller version of the original problem and so can be done by recursion. Here is the subroutine, written in Java:
/**
* List the moves for moving n disks from pile number A
* to pile number B, using pile number C as a spare.
* Precondition: n > 0
*/
void Hanoi(int n, int A, int B, int C) {
if (n == 1) {
System.out.println("Move disk 1 from pile " + A + " to pile " + B);
} else {
Hanoi(n - 1, A, C, B);
System.out.println("Move disk " + n + " from pile " + A + " to pile " + B);
Hanoi(n - 1, C, B, A);
}
}
Again, here is equivalent code in Scala:
def hanoi(n: Int, a: Int, b: Int, c: Int): Unit = {
if n == 1 then
println(s"Move disk $n from pile $a to pile $b")
else
hanoi(n - 1, a, c, b)
println(s"Move disk $n from pile $a to pile $b")
hanoi(n - 1, c, b, a)
}
hanoi(2, 1, 2, 3)
// Move disk 1 from pile 1 to pile 3
// Move disk 2 from pile 1 to pile 2
// Move disk 1 from pile 3 to pile 2
Binary Trees
Recursion is often used with linked data structures, which are data structures that are constructed by linking several objects of the same type together with pointers. For an example, we'll look at the data structure known as a binary tree. A binary tree consists of nodes linked together in a tree-like structure. The nodes can contain any type of data, but we will consider binary trees in which each node contains an integer. A binary tree can be empty, or it can consist of a node (called the root of the tree) and two smaller binary trees (called the left subtree and the right subtree of the tree). You can already see the recursive structure: A tree can contain smaller trees. In Java, the nodes of a tree can be represented by objects belonging to the class
class BinaryTreeNode {
int item; // An integer value stored in the node.
BinaryTreeNode left; // Pointer to left subtree.
BinaryTreeNode right; // Pointer to right subtree.
}
An empty tree is represented by a pointer that has the special value null. If root is a pointer to the root node of a tree, then root.left is a pointer to the left subtree and root.right is a pointer to the right subtree. Of course, both root.left and root.right can be null if the corresponding subtree is empty. Similarly, root.item is a name for the integer in the root node.
Here is the corresponding definition of a binary tree in Scala:
enum Tree:
case Empty
case Node(left: Tree, item: Int, right: Tree)
import Tree.*
Instead of null, we use an explicit constructor value for empty
trees, Empty. To construct a tree node from subtrees left and
right, with integer value item, we use the constructor
Node(left, item, right). We will see below how to extract these
fields from the node.
Let's say that we want a function that will find the sum of all the integers in all the nodes of a binary tree. We can do this with a simple recursive function. The base case of the recursion is an empty tree. Since there are no integers in an empty tree, the sum of the integers in an empty tree is zero. For a non-empty tree, we can use recursion to find the sums of the integers in the left and right subtrees, and then add those sums to the integer in the root node of the tree. In Java, this can be expressed as follows:
/**
* Find the sum of all the integers in the tree that has
* the given root.
*/
int TreeSum(BinaryTreeNode root) {
int answer;
if (root == null) { // The tree is empty.
answer = 0;
} else {
answer = TreeSum(root.left);
answer = answer + TreeSum(root.right);
answer = answer + root.item;
}
return answer;
}
Here is the corresponding function in Scala. We are using the pattern matching match expression to decide whether we have an empty tree or not, and to extract the left, item, and right fields if the tree is not empty:
def treeSum(root: Tree): Int = {
root match
case Empty => 0
case Node(left, item, right) =>
treeSum(left) + item + treeSum(right)
}
val example = Node(Node(Empty, 1, Empty), 2, Node(Empty, 3, Empty))
// example: Tree = Node(
// left = Node(left = Empty, item = 1, right = Empty),
// item = 2,
// right = Node(left = Empty, item = 3, right = Empty)
// )
treeSum(example)
// res2: Int = 6
Induction
Now that you are comfortable with recursion as a technique to write programs, you are ready for a very closely related technique for reasoning about programs (or more generally about anything that has a recursive description—that is, where compound things can be decomposed into similar smaller things). This is the technique of proof by induction.
For example, suppose that we want to argue that our findSmallest function truly does find the smallest value in any non-empty list.
There are too many possible lists to show this by exhaustively checking all possible inputs.1
Instead, we can use something like stepwise refinement in making our argument:
Claim:
findSmallest(myList)returns the smallest element inmyListfor all possible non-empty lists.
Base Case: If
myListonly has one element, then that must be the smallest.findSmallest(myList)will return that element, so it is correct in this case.Inductive Case: If
myListhas more than one element, then suppose thatfindSmallestworks correctly on lists shorter thanmyList; we will refer to this as the induction hypothesis. Now, the definition offindSmallest(myList)proceeds by letting be the first element and be the result offindSmallest(restOfMyList), whererestOfMyListis the list with removed from the front. By the induction hypothesis, will be the smallest value inrestOfMyList, because that list is smaller thanmyList. Since the smallest value in the entire list must either be the first element () or the smallest value in the rest of the list (), the program is correct because we compare and and return the smaller of the two.
Since we have covered all of the cases (a non-empty list must either have one element or more than one element), we are done. The essential point here is that we are using the induction hypothesis as a shortcut for an unknown amount of additional work: just as a recursive call to solve a smaller problem will eventually reach a base case, the claim in our induction hypothesis can be traced back to facts about ever smaller cases until we reach something proved in a base case.
Ordinary Mathematical Induction
In a math class you may have encountered the concept of (ordinary) mathematical induction. This is the special case where we are proving that some claim is true of all natural numbers, or all positive integers. In this situation, the base case will generally show that the claim is true for zero (or one, if that is our starting point). The induction hypothesis will make the specific assumption that the claim is true for some particular , and then the induction case will use that assumption to show that it must also be true for .
If we abbreviate our claim about a number by writing , then the structure of a proof by mathematical induction is to show , and, for arbitrary , if then also . This gives us a chain of facts , (because ), (because ), , etc. This chain will eventually pass through every natural number, so we could expand out the proof for any particular into an appropriate series of implications that trace back to the base case. We will see several examples of this below.
Strong Mathematical Induction
If you have learned about ordinary mathematical induction on the natural numbers before, then you might also have learned about strong mathematical induction. For this technique, we use a stronger form of induction hypothesis: instead of just assuming to prove , we will assume that all of , , …, are true, so that any of them may be used to establish . It is more typical to express this slightly differently: in our inductive case, we will show that is true given the assumption that is true for all . By using this stronger set of assumptions, we can prove claims where the chain of facts needed doesn't go directly from to ; this is a better fit for the natural way to break up some problems into subproblems. Again, see the examples below.
Structural Induction
In computer science, both of the above principles are special cases of the technique of structural induction. This is just our original statement above, where we look at the structure of the problem to divide it up into base cases (solvable/provable directly) and inductive cases (which may be decomposed into smaller problems which we assume we already know how to solve/prove).
Structural induction is particularly useful when proving claims about data types other than the natural numbers! As long as we have a way to decompose compound data into smaller but similar structures, which themselves may be decomposed further until we reach one or more base cases, we may prove claims about such data using essentially the same technique as we use to write our recursive programs. In a very real sense, induction is just recursion applied to proofs instead of programs!2
Induction Examples
Ordinary Mathematical Induction
Theorem: The number is divisible by 3 for all natural numbers .
Proof: Here, is the statement that is divisible by 3.
Base case: When , and is divisible by 3 (since .) Therefore the statement holds when .
Inductive case: We want to show that if the statement is true for (where is an arbitrary natural number), then it is true for also. That is, we must prove the implication . So we assume , that is, we assume that is divisible by 3. This means that for some integer . We want to prove , that is, that is also divisible by 3:
and from the last line we see that is in fact divisible by 3. (The third step—subtracting and adding 4—was done to enable us to use our inductive hypothesis.)
Altogether, we have proved that holds and that, for all , is true. Therefore, by the principle of induction, is true for all in , i.e. is divisible by 3 for all in .
Summations
The sum of an arbitrary number of terms is written using the symbol . (This symbol is the Greek letter sigma, which is equivalent to the Latin letter S and stands for "sum.") Thus, we have
This notation for a sum, using the operator, is called summation notation. A similar notation for products uses the symbol . (This is the Greek letter pi, which is equivalent to the Latin letter P and stands for "product.") For example,
Induction can be used to prove many formulas that use these notations. Here are two examples:
Theorem: for any integer greater than zero.
Proof: Let be the statement . We use induction to show that is true for all .
Base case: Consider the case . is the statement that . Since and , is true.
Inductive case: Let be arbitrary, and assume that is true. We want to show that is true. is the statement . But
which is what we wanted to show. This computation completes the induction.
Theorem: for any natural number .
Proof: Let be the statement . We use induction to show that is true for all
Base case: Consider the case . is the statement that . Since each side of this equation is equal to one, this is true.
Inductive case: Let be arbitrary, and assume that is true. We want to show that is true. is the statement . But, we can compute that
which is what we wanted to show. This completes the induction.
For example, these theorems show that and that , as well as infinitely many other such sums.
Factorial
Theorem: The factorial function defined above correctly computes for any natural number . (If we are using the Java version, we need to add the assumption that fits within a 32-bit integer.)
Proof: Let be the statement "factorial() correctly computes ." We use induction to prove that is true for all natural numbers .
Base case: In the case , the if statement in the function assigns the value 1 to the answer. Since 1 is the correct value of , factorial(0) correctly computes .
Inductive case: Let be an arbitrary natural number, and assume that is true. From this assumption, we must show that is true. The assumption is that factorial() correctly computes , and we want to show that factorial() correctly computes .
When the function computes factorial(), the value of the parameter is . Since , the if statement in the function computes the value of factorial() by applying the computation factorial We know, by the induction hypothesis, that the value computed by factorial() is . It follows that the value computed by factorial() is . As we observed above, for any , . We see that factorial() correctly computes . This completes the induction.
Towers of Hanoi
Theorem: The sequence of moves printed by the Hanoi subroutine as given above correctly solves the Towers of Hanoi problem for any integer .
Proof: We prove by induction that whenever is a positive integer and , , and are the numbers 1, 2, and 3 in some order, the subroutine call Hanoi() prints a sequence of moves that will move disks from pile to pile , following all the rules of the Towers of Hanoi problem.
Base case: In the base case, , the subroutine call Hanoi() prints out the single step "Move disk 1 from pile A to pile B," and this move does solve the problem for 1 disk.
Inductive case: Let be an arbitrary positive integer, and suppose that Hanoi() correctly solves the problem of moving the disks from pile to pile using pile as the spare, whenever , , and are the numbers 1, 2, and 3 in some order. We need to show that Hanoi() correctly solves the problem for disks. Since , Hanoi() begins by calling Hanoi(). By the induction hypothesis, this correctly moves disks from pile to pile . Disk number is not moved during this process. At that point, pile contains the smallest disks and pile still contains the disk, which has not yet been moved. So the next move printed by the subroutine, "Move disk from pile A to pile B," is legal because pile is empty. Finally, the subroutine calls Hanoi(), which, by the induction hypothesis, correctly moves the smallest disks from pile to pile , putting them on top of the disk, which does not move during this process. At that point, all disks are on pile , so the problem for disks has been correctly solved.
Strong Mathematical Induction
Theorem: Every natural number greater than one can be written as a product of prime numbers.
Proof: Let be the statement "if , then can be written as a product of prime numbers." We will prove that is true for all by applying the strong principle of induction.
Note that and are both automatically true, since and do not satisfy the condition that , and is true since 2 is the product of the single prime number 2. Suppose that is an arbitrary natural number with , and suppose that , , …, are already known to be true; we want to show that is true. In the case where is a prime number, then is a product of one prime number, so is true.
Consider the case where is not prime. Then, according to the definition of prime number, it is possible to write where and are numbers in the range from 2 to inclusive. Since through are known to be true, and can each be written as a product of prime numbers. Since , can also be written as a product of prime numbers. We have shown that follows from , and this completes the induction.
Binary Trees
Theorem: The function TreeSum, defined above, correctly computes the sum of all the integers in a binary tree.
Proof: We use induction on the number of nodes in the tree. Let be the statement "TreeSum correctly computes the sum of the nodes in any binary tree that contains exactly nodes." We show that is true for every natural number .
Base case: Consider the case . A tree with zero nodes is empty, and an empty tree is represented by a null pointer. In this case, the if statement in the definition of TreeSum assigns the value 0 to the answer, and this is the correct sum for an empty tree. So, is true.
Induction case: Let be an arbitrary natural number, with . Suppose we already know for each natural number with . That is, TreeSum correctly computes the sum of all the integers in any tree that has fewer than nodes. We must show that it follows that is true, that is, that TreeSum works for a tree with nodes. Suppose that root is a pointer to the root node of a tree that has a total of nodes. Since the root node counts as a node, that leaves a total of nodes for the left and right subtrees, so each subtree must contain fewer than nodes. By the induction hypothesis, we know that TreeSum(root.left) correctly computes the sum of all the integers in the left subtree, and TreeSum(root.right) correctly computes the sum of all the integers in the right subtree. The sum of all the integers in the tree is root.item plus the sums of the integers in the subtrees, and this is the value computed by TreeSum. So, TreeSum does work for a tree with nodes. This completes the induction.
Note how closely the structure of the inductive proof follows the structure of the recursive function. In particular, the strong principle of mathematical induction is very natural here, since the size of subtree could be anything up to one less than the size of the complete tree. It would be very difficult to use the ordinary principle of mathematical induction in a proof about binary trees.
Structural Induction
Instead of using strong mathematical induction, it is often more straightforward to use structural induction. Here is the proof about the TreeSum function again, expressed as a structural induction over trees:
Theorem: The function TreeSum, defined above, correctly computes the sum of all the integers in a binary tree.
Proof: We use structural induction on the construction of a tree. Let be the statement "TreeSum correctly computes the sum of the nodes in the binary tree ." We show that is true for every binary tree .
Base case: If is an empty tree, then the definition of TreeSum returns the value 0, which is the correct sum for an empty tree. So, is true.
Induction case: Suppose we already know that and hold for some trees and . That is, TreeSum correctly computes the sum of all the integers in and . We must show that it follows that is true, where is the tree constructed from subtrees (left) and (right), plus an integer item. The value computed by TreeSum() will be TreeSum() + item + TreeSum(). By the induction hypothesis, we know that TreeSum() correctly computes the sum of all the integers in the left subtree, and TreeSum() correctly computes the sum of all the integers in the right subtree. The sum of all the integers in the tree is item plus the sums of the integers in the subtrees, so, TreeSum also works for the tree .
Recursive Definitions
Recursion occurs in programming when a subroutine is defined—partially, at least—in terms of itself. But recursion also occurs outside of programming. A recursive definition is a definition that includes a reference to the term that is being defined. A recursive definition defines something at least partially in terms of itself. As in the case of recursive subroutines, mathematical induction can often be used to prove facts about things that are defined recursively.
As already noted, there is a recursive definition for , for in . We can define and for . Other sequences of numbers can also be defined recursively. For example, the famous Fibonacci sequence is the sequence of numbers , , , …, defined recursively by
Using this definition, we compute that
and so on. Based on this definition, we can use induction to prove facts about the Fibonacci sequence. We can prove, for example, that grows exponentially with , even without finding an exact formula for :
Theorem: The Fibonacci sequence, , , , …, satisfies , for .
Proof: We prove this by induction on . For , we have that while , which is about . So for . Similarly, for , we have and , which is about 11.4. So for .
Now suppose that is an arbitrary integer with . Suppose that we already know that for and for . We want to show that the inequality then holds for as well. But
This string of equalities and inequalities shows that . This completes the induction and proves the theorem.
Exercises
- Use induction to prove that is divisible by 3 for all natural numbers .
Answer
The base case, when , is the claim that is divisible by 3. Evaluating the expression gives 0, which is , so the claim is true.
For the inductive case, suppose that is divisible by 3 for some ; that is, there is some such that . Using that induction hypothesis, we need to show that is also divisible by 3. Expanding this expression gives . With the induction hypothesis, this can be rewritten as , or . This may be factored as , showing that it too is divisible by 3.
Thus we have shown that it is true for , and if it holds for any then it also holds for ; by mathematical induction, therefore, it holds for all natural numbers.
- Use induction to prove that
for any natural number and for any real number such that .
- Use induction to prove that for any natural number ,
In addition to proving this by induction, show that it follows as a corollary of Exercise 2.
- Use induction to prove that for any natural number ,
In addition to proving this by induction, show that it follows as a corollary of Exercise 2.
Answer
Base case (): .
Inductive case: suppose true for some . Then , by the induction hypothesis. Now, , so our summation equals , showing that the formula also holds for . Therefore it holds for all natural numbers .
We may also use Exercise 2, taking . The formula for the sum is then
- Use induction to prove that for any positive integer ,
- Use induction to prove that for any positive integer ,
-
Evaluate the following sums, using results proved in this section and in the previous exercises:
-
Write each of the sums in the preceding problem using summation notation.
-
The Hanoi subroutine given in this section does not just find some solution to the Towers of Hanoi problem. It solves the problem using the minimum possible number of moves. Use induction to prove this fact.
Answer
The base case is when , where it moves a single disk in one move.
In the inductive case, suppose that Hanoi(n, A, B, C) moves disks
from A to B in the minimum possible number of moves for some . Then
to move disks from A to B, we must at least move the top disks
to another pile before we can move the largest disk from A to B; the call
Hanoi(n, A, C, B) will get this done in the minimum number of moves by
the induction hypothesis. After moving the largest disk, we then need to
move the other disks from C to B, which can be done in the minimum
number of moves by the call Hanoi(n, C, B, A). This description is
precisely what happens when we call Hanoi(n+1, A, B, C), so it
moves disks in the shortest way possible.
By induction, then, Hanoi(n, A, B, C) uses the minimum number of moves
for every .
- Use induction to prove that the Hanoi subroutine uses moves to solve the Towers of Hanoi problem for disks. (There is a story that goes along with the Towers of Hanoi problem. It is said that on the day the world was created, a group of monks in Hanoi were set the task of solving the problem for 64 disks. They can move just one disk each day. On the day the problem is solved, the world will end. However, we shouldn't worry too much, since days is a very long time—about 50 million billion years.)
Answer
For the base case, we need to check that Hanoi(1, A, B, C) takes move.
For the inductive case, suppose that Hanoi(n, A, B, C) takes moves for
some . Then Hanoi(n+1, A, B, C) takes
moves, which completes the induction.
- Consider the following recursive function:
/** Compute x raised to the power n.
* Precondition: n >= 0.
*/
int power(int x, int n) {
int answer;
if (n == 0) {
answer = 1;
} else if (n % 2 == 0) {
answer = power(x * x, n / 2);
} else {
answer = x * power(x, n - 1);
}
return answer;
}
Show that for any integer and any non-negative integer ,
the function power(,) correctly computes the value
of . (Assume that the int data type can represent
arbitrarily large integers.) Note that the test
"if (n % 2 == 0)" tests whether is evenly divisible by 2.
That is, the test is true if is an even number. (This function is
actually a very efficient way to compute .)
Answer
We will prove by strong induction on that power(x, n) correctly computes
, for every .
Base Case: When , we can check that power(x, 0) returns 1, which
is for every .3
Inductive Case: Suppose that the claim is true for every , for some .
Then we need to show that it is also true for . If is even, then power(x, n)
is power(x*x, n/2); since , we know by the induction hypothesis that
this computes . If is odd, then power(x, n) is
x * power(x, n-1), which similarly we know to compute .
Therefore, we have shown that power(x, n) correctly computes for all .
- Write the power function from the previous problem in Scala, and check that it works on several examples. Hint: The code will be almost the same as the Java, except for the different function syntax and not using the temporary variable answer (see examples above).
def power(x: BigInt, n: Int): BigInt = {
/* TODO */
}
Answer
def power(x: BigInt, n: Int): BigInt = {
if n == 0 then
1
else if n % 2 == 0 then
power(x * x, n / 2)
else
x * power(x, n - 1)
}
- A leaf node in a binary tree is a node in which both the left and the right subtrees are empty. Prove that the following recursive function correctly counts the number of leaves in a binary tree:
/**
* Counts the number of leaf nodes in the tree with the
* specified root.
*/
int LeafCount( BinaryTreeNode root ) {
int count;
if (root == null) {
count = 0;
} else if (root.left == null && root.right == null) {
count = 1;
} else {
count = LeafCount(root.left);
count = count + LeafCount(root.right);
}
return count;
}
Answer
We will prove this by structural induction on the formation of a
tree . For the base cases, if is empty (null), then
LeafCount(t) returns 0, while if is a single node (with both
children empty), then LeafCount(t) returns 1; these are both correct.
For the inductive case, we know that is a node with at least one
non-empty child (or else we would be in one of the base cases). We will
assume that LeafCount(t.left) and LeafCount(t.right) are both correct,
and we just have to show that LeafCount(t) also correctly counts the
number of leaves. But in this case the root node of cannot be a leaf,
so the number of leaves equals the sum of the number of leaves in the
children. This is exactly what LeafCount(t) computes, so we are done.
- Complete this Scala version of the LeafCount function
from the previous problem. Note that we may use patterns such
as
Node(Empty, _, Empty)in the switch statement to match nodes where the subtrees are bothEmpty(and the_indicates that we don't care what the value of the item field is).
def leafCount(t: Tree): Int = {
t match
case Empty => /* TODO */
case Node(Empty, _, Empty) => /* TODO */
case Node(left, _, right) => /* TODO */
}
}
Answer
def leafCount(t: Tree): Int = {
t match
case Empty => 0
case Node(Empty, _, Empty) => 1
case Node(left, _, right) => leafCount(left) + leafCount(right)
}
}
- A binary search tree satisfies the following property: If node is a pointer to any node in the tree, then all the integers in the left subtree of node are less than node.item and all the integers in the right subtree of node are greater than or equal to node.item. Prove that the following recursive subroutine prints all the integers in a binary search tree in non-decreasing order:
/**
* Prints the integers in the tree with the given root node
* in non-decreasing order.
* Precondition: root is a pointer to the root node of a
* binary search tree.
*/
void SortPrint(BinaryTreeNode root) {
if (root == null) {
// There is nothing to print.
} else {
SortPrint(root.left);
System.out.println(root.item);
SortPrint(root.right);
}
}
Answer
Here is a sketch of a proof by structural induction. The base case,
when the tree is empty, is easy. If we suppose that SortPrint(root.left)
prints all of the integers in the left subtree (which are less than root.item)
in order, and that SortPrint(root.right) prints all of the integers in the
right subtree (which are greater than or equal to root.item) in order, then
SortPrint(root) must print all of the numbers in order.
- Complete this Scala version of the SortPrint function from the previous problem.
def sortPrint(root: Tree): Unit = {
root match
case Empty => () /* There is nothing to print */
case Node(left, item, right) =>
/* TODO */
}
}
Answer
def sortPrint(root: Tree): Unit = {
root match
case Empty => () /* There is nothing to print */
case Node(left, item, right) =>
sortPrint(left)
println(item)
sortPrint(right)
}
- Prove that the Fibonacci sequence, , , , …, satisfies for all natural numbers .
Answer
Because the recursive case of the Fibonacci depends on the two previous values, we will use two base cases: and . For the inductive case, suppose that and for some ; then , which is less than by the induction hypothesis. That sum is in turn less than , so we may conclude that the claim is true for all natural numbers .
- Suppose that , , , …, is a sequence of numbers which is defined recursively by and for . Prove that for every positive integer .
Answer
The base case is when : . For the induction case, suppose that for some . Then , which shows that it also holds for . Therefore it holds for all .