Recursion
Recursion
5 Recursion
Contents
www.it-ebooks.info
190 Chapter 5. Recursion
One way to describe repetition within a computer program is the use of loops,
such as Java’s while-loop and for-loop constructs described in Section 1.5.2. An
entirely different way to achieve repetition is through a process known as recursion.
Recursion is a technique by which a method makes one or more calls to itself
during execution, or by which a data structure relies upon smaller instances of
the very same type of structure in its representation. There are many examples of
recursion in art and nature. For example, fractal patterns are naturally recursive. A
physical example of recursion used in art is in the Russian Matryoshka dolls. Each
doll is either made of solid wood, or is hollow and contains another Matryoshka
doll inside it.
In computing, recursion provides an elegant and powerful alternative for per-
forming repetitive tasks. In fact, a few programming languages (e.g., Scheme,
Smalltalk) do not explicitly support looping constructs and instead rely directly
on recursion to express repetition. Most modern programming languages support
functional recursion using the identical mechanism that is used to support tradi-
tional forms of method calls. When one invocation of the method makes a recursive
call, that invocation is suspended until the recursive call completes.
Recursion is an important technique in the study of data structures and algo-
rithms. We will use it prominently in several later chapters of this book (most
notably, Chapters 8 and 12). In this chapter, we begin with the following four illus-
trative examples of the use of recursion, providing a Java implementation for each.
• The file system for a computer has a recursive structure in which directories
can be nested arbitrarily deeply within other directories. Recursive algo-
rithms are widely used to explore and manage these file systems.
www.it-ebooks.info
5.1. Illustrative Examples 191
www.it-ebooks.info
192 Chapter 5. Recursion
This method does not use any explicit loops. Repetition is achieved through
repeated recursive invocations of the method. The process is finite because each
time the method is invoked, its argument is smaller by one, and when a base case
is reached, no further recursive calls are made.
We illustrate the execution of a recursive method using a recursion trace. Each
entry of the trace corresponds to a recursive call. Each new recursive method call
is indicated by a downward arrow to a new invocation. When the method returns,
an arrow showing this return is drawn and the return value may be indicated along-
side this arrow. An example of such a trace for the factorial function is shown in
Figure 5.1.
return 5 ∗ 24 = 120
factorial(5)
return 4 ∗ 6 = 24
factorial(4)
return 3 ∗ 2 = 6
factorial(3)
return 2 ∗ 1 = 2
factorial(2)
return 1 ∗ 1 = 1
factorial(1)
return 1
factorial(0)
www.it-ebooks.info
5.1. Illustrative Examples 193
Figure 5.2: Three sample outputs of an English ruler drawing: (a) a 2-inch ruler
with major tick length 4; (b) a 1-inch ruler with major tick length 5; (c) a 3-inch
ruler with major tick length 3.
The English ruler pattern is a simple example of a fractal, that is, a shape that has
a self-recursive structure at various levels of magnification. Consider the rule with
major tick length 5 shown in Figure 5.2(b). Ignoring the lines containing 0 and 1,
let us consider how to draw the sequence of ticks lying between these lines. The
central tick (at 1/2 inch) has length 4. Observe that the two patterns of ticks above
and below this central tick are identical, and each has a central tick of length 3.
www.it-ebooks.info
194 Chapter 5. Recursion
In general, an interval with a central tick length L ≥ 1 is composed of:
• An interval with a central tick length L − 1
• A single tick of length L
• An interval with a central tick length L − 1
Although it is possible to draw such a ruler using an iterative process (see Ex-
ercise P-5.29), the task is considerably easier to accomplish with recursion. Our
implementation consists of three methods, as shown in Code Fragment 5.2.
The main method, drawRuler, manages the construction of the entire ruler. Its
arguments specify the total number of inches in the ruler and the major tick length.
The utility method, drawLine, draws a single tick with a specified number of dashes
(and an optional integer label that is printed to the right of the tick).
The interesting work is done by the recursive drawInterval method. This method
draws the sequence of minor ticks within some interval, based upon the length of
the interval’s central tick. We rely on the intuition shown at the top of this page,
and with a base case when L = 0 that draws nothing. For L ≥ 1, the first and last
steps are performed by recursively calling drawInterval(L − 1). The middle step is
performed by calling method drawLine(L).
1 /∗∗ Draws an English ruler for the given number of inches and major tick length. ∗/
2 public static void drawRuler(int nInches, int majorLength) {
3 drawLine(majorLength, 0); // draw inch 0 line and label
4 for (int j = 1; j <= nInches; j++) {
5 drawInterval(majorLength − 1); // draw interior ticks for inch
6 drawLine(majorLength, j); // draw inch j line and label
7 }
8 }
9 private static void drawInterval(int centralLength) {
10 if (centralLength >= 1) { // otherwise, do nothing
11 drawInterval(centralLength − 1); // recursively draw top interval
12 drawLine(centralLength); // draw center tick line (without label)
13 drawInterval(centralLength − 1); // recursively draw bottom interval
14 }
15 }
16 private static void drawLine(int tickLength, int tickLabel) {
17 for (int j = 0; j < tickLength; j++)
18 System.out.print("-");
19 if (tickLabel >= 0)
20 System.out.print(" " + tickLabel);
21 System.out.print("\n");
22 }
23 /∗∗ Draws a line with the given tick length (but no label). ∗/
24 private static void drawLine(int tickLength) {
25 drawLine(tickLength, −1);
26 }
Code Fragment 5.2: A recursive implementation of a method that draws a ruler.
www.it-ebooks.info
5.1. Illustrative Examples 195
The execution of the recursive drawInterval method can be visualized using a re-
cursion trace. The trace for drawInterval is more complicated than in the factorial
example, however, because each instance makes two recursive calls. To illustrate
this, we will show the recursion trace in a form that is reminiscent of an outline for
a document. See Figure 5.3.
Output
drawInterval(3)
drawInterval(2)
drawInterval(1)
drawInterval(0)
drawLine(1)
drawInterval(0)
drawLine(2)
drawInterval(1)
drawInterval(0)
drawLine(1)
drawInterval(0)
drawLine(3)
drawInterval(2)
(previous pattern repeats)
Figure 5.3: A partial recursion trace for the call drawInterval(3). The second pattern
of calls for drawInterval(2) is not shown, but it is identical to the first.
www.it-ebooks.info
196 Chapter 5. Recursion
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
Figure 5.4: Values stored in sorted order within an array. The numbers at top are
the indices.
When the sequence is unsorted, the standard approach to search for a target
value is to use a loop to examine every element, until either finding the target or
exhausting the data set. This algorithm is known as linear search, or sequential
search, and it runs in O(n) time (i.e., linear time) since every element is inspected
in the worst case.
When the sequence is sorted and indexable, there is a more efficient algorithm.
(For intuition, think about how you would accomplish this task by hand!) If we
consider an arbitrary element of the sequence with value v, we can be sure that all
elements prior to that in the sequence have values less than or equal to v, and that all
elements after that element in the sequence have values greater than or equal to v.
This observation allows us to quickly “home in” on a search target using a variant
of the children’s game “high-low.” We call an element of the sequence a candidate
if, at the current stage of the search, we cannot rule out that this item matches the
target. The algorithm maintains two parameters, low and high, such that all the
candidate elements have index at least low and at most high. Initially, low = 0 and
high = n − 1. We then compare the target value to the median candidate, that is,
the element with index
mid = (low + high)/2 .
We consider three cases:
• If the target equals the median candidate, then we have found the item we are
looking for, and the search terminates successfully.
• If the target is less than the median candidate, then we recur on the first half
of the sequence, that is, on the interval of indices from low to mid − 1.
• If the target is greater than the median candidate, then we recur on the second
half of the sequence, that is, on the interval of indices from mid + 1 to high.
An unsuccessful search occurs if low > high, as the interval [low, high] is empty.
www.it-ebooks.info
5.1. Illustrative Examples 197
This algorithm is known as binary search. We give a Java implementation in
Code Fragment 5.3, and an illustration of the execution of the algorithm in Fig-
ure 5.5. Whereas sequential search runs in O(n) time, the more efficient binary
search runs in O(log n) time. This is a significant improvement, given that if n is
1 billion, log n is only 30. (We defer our formal analysis of binary search’s running
time to Proposition 5.2 in Section 5.2.)
1 /∗∗
2 ∗ Returns true if the target value is found in the indicated portion of the data array.
3 ∗ This search only considers the array portion from data[low] to data[high] inclusive.
4 ∗/
5 public static boolean binarySearch(int[ ] data, int target, int low, int high) {
6 if (low > high)
7 return false; // interval empty; no match
8 else {
9 int mid = (low + high) / 2;
10 if (target == data[mid])
11 return true; // found a match
12 else if (target < data[mid])
13 return binarySearch(data, target, low, mid − 1); // recur left of the middle
14 else
15 return binarySearch(data, target, mid + 1, high); // recur right of the middle
16 }
17 }
Code Fragment 5.3: An implementation of the binary search algorithm on a sorted
array.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37
low=mid=high
Figure 5.5: Example of a binary search for target value 22 on a sorted array with 16
elements.
www.it-ebooks.info
198 Chapter 5. Recursion
/user/rt/courses/
cs016/ cs252/
grades grades
homeworks/ programs/ projects/
Given the recursive nature of the file-system representation, it should not come
as a surprise that many common behaviors of an operating system, such as copying
a directory or deleting a directory, are implemented with recursive algorithms. In
this section, we consider one such algorithm: computing the total disk usage for all
files and directories nested within a particular directory.
For illustration, Figure 5.7 portrays the disk space being used by all entries in
our sample file system. We differentiate between the immediate disk space used by
each entry and the cumulative disk space used by that entry and all nested features.
For example, the cs016 directory uses only 2K of immediate space, but a total of
249K of cumulative space.
www.it-ebooks.info
5.1. Illustrative Examples 199
5124K
/user/rt/courses/
1K
249K 4874K
cs016/ cs252/
2K 1K
82K 4787K
hw1 hw2 hw3 pr1 pr2 pr3 papers/ demos/
3K 2K 4K 57K 97K 74K 1K 1K
Figure 5.7: The same portion of a file system given in Figure 5.6, but with additional
annotations to describe the amount of disk space that is used. Within the icon for
each file or directory is the amount of space directly used by that artifact. Above
the icon for each directory is an indication of the cumulative disk space used by
that directory and all its (recursive) contents.
The cumulative disk space for an entry can be computed with a simple recursive
algorithm. It is equal to the immediate disk space used by the entry plus the sum
of the cumulative disk space usage of any entries that are stored directly within
the entry. For example, the cumulative disk space for cs016 is 249K because it
uses 2K itself, 8K cumulatively in grades, 10K cumulatively in homeworks, and
229K cumulatively in programs. Pseudocode for this algorithm is given in Code
Fragment 5.4.
Code Fragment 5.4: An algorithm for computing the cumulative disk space usage
nested at a file-system entry. We presume that method size returns the immediate
disk space of an entry.
www.it-ebooks.info
200 Chapter 5. Recursion
The java.io.File Class
To implement a recursive algorithm for computing disk usage in Java, we rely on
the java.io.File class. An instance of this class represents an abstract pathname in
the operating system and allows for properties of that operating system entry to be
queried. We will rely on the following methods of the class:
• new File(pathString) or new File(parentFile, childString)
A new File instance can be constructed either by providing the full path as
a string, or by providing an existing File instance that represents a directory
and a string that designates the name of a child entry within that directory.
• file.length( )
Returns the immediate disk usage (measured in bytes) for the operating sys-
tem entry represented by the File instance (e.g., /user/rt/courses).
• file.isDirectory( )
Returns true if the File instance represents a directory; false otherwise.
• file.list( )
Returns an array of strings designating the names of all entries within the
given directory. In our sample file system, if we call this method on the
File associated with path /user/rt/courses/cs016, it returns an array with
contents: {"grades", "homeworks", "programs"}.
Java Implementation
With use of the File class, we now convert the algorithm from Code Fragment 5.4
into the Java implementation of Code Fragment 5.5.
1 /∗∗
2 ∗ Calculates the total disk usage (in bytes) of the portion of the file system rooted
3 ∗ at the given path, while printing a summary akin to the standard 'du' Unix tool.
4 ∗/
5 public static long diskUsage(File root) {
6 long total = root.length( ); // start with direct disk usage
7 if (root.isDirectory( )) { // and if this is a directory,
8 for (String childname : root.list( )) { // then for each child
9 File child = new File(root, childname); // compose full path to child
10 total += diskUsage(child); // add child’s usage to total
11 }
12 }
13 System.out.println(total + "\t" + root); // descriptive output
14 return total; // return the grand total
15 }
Code Fragment 5.5: A recursive method for reporting disk usage of a file system.
www.it-ebooks.info
5.1. Illustrative Examples 201
Recursion Trace
To produce a different form of a recursion trace, we have included an extraneous
print statement within our Java implementation (line 13 of Code Fragment 5.5).
The precise format of that output intentionally mirrors the output that is produced
by a classic Unix/Linux utility named du (for “disk usage”). It reports the amount
of disk space used by a directory and all contents nested within, and can produce a
verbose report, as given in Figure 5.8.
When executed on the sample file system portrayed in Figure 5.7, our imple-
mentation of the diskUsage method produces the result given in Figure 5.8. During
the execution of the algorithm, exactly one recursive call is made for each entry in
the portion of the file system that is considered. Because each line is printed just
before returning from a recursive call, the lines of output reflect the order in which
the recursive calls are completed. Notice that it computes and reports the cumula-
tive disk space for a nested entry before computing and reporting the cumulative
disk space for the directory that contains it. For example, the recursive calls regard-
ing entries grades, homeworks, and programs are computed before the cumulative
total for the directory /user/rt/courses/cs016 that contains them.
8 /user/rt/courses/cs016/grades
3 /user/rt/courses/cs016/homeworks/hw1
2 /user/rt/courses/cs016/homeworks/hw2
4 /user/rt/courses/cs016/homeworks/hw3
10 /user/rt/courses/cs016/homeworks
57 /user/rt/courses/cs016/programs/pr1
97 /user/rt/courses/cs016/programs/pr2
74 /user/rt/courses/cs016/programs/pr3
229 /user/rt/courses/cs016/programs
249 /user/rt/courses/cs016
26 /user/rt/courses/cs252/projects/papers/buylow
55 /user/rt/courses/cs252/projects/papers/sellhigh
82 /user/rt/courses/cs252/projects/papers
4786 /user/rt/courses/cs252/projects/demos/market
4787 /user/rt/courses/cs252/projects/demos
4870 /user/rt/courses/cs252/projects
3 /user/rt/courses/cs252/grades
4874 /user/rt/courses/cs252
5124 /user/rt/courses/
Figure 5.8: A report of the disk usage for the file system shown in Figure 5.7, as
generated by our diskUsage method from Code Fragment 5.5, or equivalently by
the Unix/Linux command du with option -a (which lists both directories and files).
www.it-ebooks.info
202 Chapter 5. Recursion
Computing Factorials
It is relatively easy to analyze the efficiency of our method for computing factorials,
as described in Section 5.1.1. A sample recursion trace for our factorial method was
given in Figure 5.1. To compute factorial(n), we see that there are a total of n + 1
activations, as the parameter decreases from n in the first call, to n − 1 in the second
call, and so on, until reaching the base case with parameter 0.
It is also clear, given an examination of the method body in Code Fragment 5.1,
that each individual activation of factorial executes a constant number of opera-
tions. Therefore, we conclude that the overall number of operations for computing
factorial(n) is O(n), as there are n + 1 activations, each of which accounts for O(1)
operations.
www.it-ebooks.info
5.2. Analyzing Recursive Algorithms 203
Drawing an English Ruler
In analyzing the English ruler application from Section 5.1.2, we consider the fun-
damental question of how many total lines of output are generated by an initial call
to drawInterval(c), where c denotes the center length. This is a reasonable bench-
mark for the overall efficiency of the algorithm as each line of output is based upon
a call to the drawLine utility, and each recursive call to drawInterval with nonzero
parameter makes exactly one direct call to drawLine.
Some intuition may be gained by examining the source code and the recur-
sion trace. We know that a call to drawInterval(c) for c > 0 spawns two calls to
drawInterval(c − 1) and a single call to drawLine. We will rely on this intuition to
prove the following claim.
www.it-ebooks.info
204 Chapter 5. Recursion
Justification: To prove this claim, a crucial fact is that with each recursive call
the number of candidate elements still to be searched is given by the value
high − low + 1.
Moreover, the number of remaining candidates is reduced by at least one-half with
each recursive call. Specifically, from the definition of mid, the number of remain-
ing candidates is either
low + high high − low + 1
(mid − 1) − low + 1 = − low ≤
2 2
or
low + high high − low + 1
high − (mid + 1) + 1 = high − ≤ .
2 2
Initially, the number of candidates is n; after the first call in a binary search, it is
at most n/2; after the second call, it is at most n/4; and so on. In general, after
the j th call in a binary search, the number of candidate elements remaining is at
most n/2 j . In the worst case (an unsuccessful search), the recursive calls stop when
there are no more candidate elements. Hence, the maximum number of recursive
calls performed, is the smallest integer r such that
n
< 1.
2r
In other words (recalling that we omit a logarithm’s base when it is 2), r is the
smallest integer such that r > log n. Thus, we have
r = log n + 1,
www.it-ebooks.info
5.2. Analyzing Recursive Algorithms 205
To formalize this argument, we can define the nesting level of each entry such
that the entry on which we begin has nesting level 0, entries stored directly within
it have nesting level 1, entries stored within those entries have nesting level 2, and
so on. We can prove by induction that there is exactly one recursive invocation of
diskUsage upon each entry at nesting level k. As a base case, when k = 0, the only
recursive invocation made is the initial one. As the inductive step, once we know
there is exactly one recursive invocation for each entry at nesting level k, we can
claim that there is exactly one invocation for each entry e at nesting level k + 1,
made within the for loop for the entry at level k that contains e.
Having established that there is one recursive call for each entry of the file
system, we return to the question of the overall computation time for the algorithm.
It would be great if we could argue that we spend O(1) time in any single invocation
of the method, but that is not the case. While there is a constant number of steps
reflected in the call to root.length( ) to compute the disk usage directly at that entry,
when the entry is a directory, the body of the diskUsage method includes a for loop
that iterates over all entries that are contained within that directory. In the worst
case, it is possible that one entry includes n − 1 others.
Based on this reasoning, we could conclude that there are O(n) recursive calls,
each of which runs in O(n) time, leading to an overall running time that is O(n2 ).
While this upper bound is technically true, it is not a tight upper bound. Remark-
ably, we can prove the stronger bound that the recursive algorithm for diskUsage
completes in O(n) time! The weaker bound was pessimistic because it assumed
a worst-case number of entries for each directory. While it is possible that some
directories contain a number of entries proportional to n, they cannot all contain
that many. To prove the stronger claim, we choose to consider the overall number
of iterations of the for loop across all recursive calls. We claim there are precisely
n − 1 such iterations of that loop overall. We base this claim on the fact that each
iteration of that loop makes a recursive call to diskUsage, and yet we have already
concluded that there are a total of n calls to diskUsage (including the original call).
We therefore conclude that there are O(n) recursive calls, each of which uses O(1)
time outside the loop, and that the overall number of operations due to the loop
is O(n). Summing all of these bounds, the overall number of operations is O(n).
The argument we have made is more advanced than with the earlier examples
of recursion. The idea that we can sometimes get a tighter bound on a series of
operations by considering the cumulative effect, rather than assuming that each
achieves a worst case is a technique called amortization; we will see another ex-
ample of such analysis in Section 7.2.3. Furthermore, a file system is an implicit
example of a data structure known as a tree, and our disk usage algorithm is really
a manifestation of a more general algorithm known as a tree traversal. Trees will
be the focus of Chapter 8, and our argument about the O(n) running time of the
disk usage algorithm will be generalized for tree traversals in Section 8.4.
www.it-ebooks.info
206 Chapter 5. Recursion
Figure 5.9: Computing the sum of a sequence recursively, by adding the last number
to the sum of the first n − 1.
www.it-ebooks.info
5.3. Further Examples of Recursion 207
A recursive algorithm for computing the sum of an array of integers based on
this intuition is implemented in Code Fragment 5.6.
1 /∗∗ Returns the sum of the first n integers of the given array. ∗/
2 public static int linearSum(int[ ] data, int n) {
3 if (n == 0)
4 return 0;
5 else
6 return linearSum(data, n−1) + data[n−1];
7 }
return 15 + data[4] = 15 + 8 = 23
linearSum(data, 5)
return 13 + data[3] = 13 + 2 = 15
linearSum(data, 4)
return 7 + data[2] = 7 + 6 = 13
linearSum(data, 3)
return 4 + data[1] = 4 + 3 = 7
linearSum(data, 2)
return 0 + data[0] = 0 + 4 = 4
linearSum(data, 1)
return 0
linearSum(data, 0)
www.it-ebooks.info
208 Chapter 5. Recursion
Reversing a Sequence with Recursion
Next, let us consider the problem of reversing the n elements of an array, so that
the first element becomes the last, the second element becomes second to the last,
and so on. We can solve this problem using linear recursion, by observing that the
reversal of a sequence can be achieved by swapping the first and last elements and
then recursively reversing the remaining elements. We present an implementation
of this algorithm in Code Fragment 5.7, using the convention that the first time we
call this algorithm we do so as reverseArray(data, 0, n−1).
We note that whenever a recursive call is made, there will be two fewer elements
in the relevant portion of the array. (See Figure 5.11.) Eventually a base case is
reached when the condition low < high fails, either because low == high in the
case that n is odd, or because low == high + 1 in the case that n is even.
The above argument implies that the recursive
algorithm of Code Fragment 5.7
is guaranteed to terminate after a total of 1 + 2n recursive calls. Because each call
involves a constant amount of work, the entire process runs in O(n) time.
0 1 2 3 4 5 6 7
4 3 6 2 7 8 9 5
5 3 6 2 7 8 9 4
5 9 6 2 7 8 3 4
5 9 8 2 7 6 3 4
5 9 8 7 2 6 3 4
Figure 5.11: A trace of the recursion for reversing a sequence. The highlighted
portion has yet to be reversed.
www.it-ebooks.info
5.3. Further Examples of Recursion 209
Recursive Algorithms for Computing Powers
As another interesting example of the use of linear recursion, we consider the prob-
lem of raising a number x to an arbitrary nonnegative integer n. That is, we wish
to compute the power function, defined as power(x, n) = xn . (We use the name
“power” for this discussion, to differentiate from the pow method of the Math class,
which provides such functionality.) We will consider two different recursive for-
mulations for the problem that lead to algorithms with very different performance.
A trivial recursive definition follows from the fact that xn = x · xn−1 for n > 0.
1 if n = 0
power(x, n) =
x · power(x, n − 1) otherwise.
This definition leads to a recursive algorithm shown in Code Fragment 5.8.
1 /∗∗ Computes the value of x raised to the nth power, for nonnegative integer n. ∗/
2 public static double power(double x, int n) {
3 if (n == 0)
4 return 1;
5 else
6 return x ∗ power(x, n−1);
7 }
Code Fragment 5.8: Computing the power function using trivial recursion.
A recursive call to this version of power(x, n) runs in O(n) time. Its recursion
trace has structure very similar to that of the factorial function from Figure 5.1,
with the parameter decreasing by one with each call, and constant work performed
at each of n + 1 levels.
However, there is a much faster way to compute the power function using an
alternative definition that employs a squaring technique. Let k = n2 denote the
floor of the integer division (equivalent to n/2 in Java when n is an int). We consider
2 2 n 2
the expression xk . When n is even, n2 = n2 and therefore xk = x 2 = xn .
n n−1 k 2 2
When n is odd,
2 = 2 and x = xn−1 , and therefore xn = xk · x, just as
213 = 26 · 26 · 2. This analysis leads to the following recursive definition:
⎧
⎪
⎨ 1 if n = 0
n 2
power(x, n) = power x, 2 · x if n > 0 is odd
⎩ power x, n 2
⎪
if n > 0 is even
2
If we were
to implement this recursion making two recursive calls to compute
power(x, n2 ) · power(x, n2 ), a trace of the recursion would demonstrate O(n)
calls. We can perform significantly fewer operations by computing power(x, n2 )
and storing it in a variable as a partial result, and then multiplying it by itself. An
implementation based on this recursive definition is given in Code Fragment 5.9.
www.it-ebooks.info
210 Chapter 5. Recursion
1 /∗∗ Computes the value of x raised to the nth power, for nonnegative integer n. ∗/
2 public static double power(double x, int n) {
3 if (n == 0)
4 return 1;
5 else {
6 double partial = power(x, n/2); // rely on truncated division of n
7 double result = partial ∗ partial;
8 if (n % 2 == 1) // if n odd, include extra factor of x
9 result ∗= x;
10 return result;
11 }
12 }
Code Fragment 5.9: Computing the power function using repeated squaring.
return 64 ∗ 64 ∗ 2 = 8192
power(2, 13)
return 8 ∗ 8 = 64
power(2, 6)
return 2 ∗ 2 ∗ 2 = 8
power(2, 3)
return 1 ∗ 1 ∗ 2 = 2
power(2, 1)
return 1
power(2, 0)
To analyze the running time of the revised algorithm, we observe that the ex-
ponent in each recursive call of method power(x,n) is at most half of the preceding
exponent. As we saw with the analysis of binary search, the number of times that
we can divide n by two before getting to one or less is O(log n). Therefore, our new
formulation of power results in O(log n) recursive calls. Each individual activation
of the method uses O(1) operations (excluding the recursive call), and so the total
number of operations for computing power(x,n) is O(log n). This is a significant
improvement over the original O(n)-time algorithm.
The improved version also provides significant saving in reducing the memory
usage. The first version has a recursive depth of O(n), and therefore, O(n) frames
are simultaneously stored in memory. Because the recursive depth of the improved
version is O(log n), its memory usage is O(log n) as well.
www.it-ebooks.info
5.3. Further Examples of Recursion 211
When a method makes two recursive calls, we say that it uses binary recursion.
We have already seen an example of binary recursion when drawing the English
ruler (Section 5.1.2). As another application of binary recursion, let us revisit the
problem of summing the n integers of an array. Computing the sum of one or zero
values is trivial. With two or more values, we can recursively compute the sum of
the first half, and the sum of the second half, and add those sums together. Our
implementation of such an algorithm, in Code Fragment 5.10, is initially invoked
as binarySum(data, 0, n−1).
0,7
0,3 4,7
www.it-ebooks.info
212 Chapter 5. Recursion
To solve such a puzzle, we need to assign a unique digit (that is, 0, 1, . . . , 9) to each
letter in the equation, in order to make the equation true. Typically, we solve such
a puzzle by using our human observations of the particular puzzle we are trying to
solve to eliminate configurations (that is, possible partial assignments of digits to
letters) until we can work through the feasible configurations that remain, testing
for the correctness of each one.
If the number of possible configurations is not too large, however, we can use
a computer to simply enumerate all the possibilities and test each one, without em-
ploying any human observations. Such an algorithm can use multiple recursion
to work through the configurations in a systematic way. To keep the description
general enough to be used with other puzzles, we consider an algorithm that enu-
merates and tests all k-length sequences, without repetitions, chosen from a given
universe U . We show pseudocode for such an algorithm in Code Fragment 5.11,
building the sequence of k elements with the following steps:
1. Recursively generating the sequences of k − 1 elements
2. Appending to each such sequence an element not already contained in it.
Throughout the execution of the algorithm, we use a set U to keep track of the
elements not contained in the current sequence, so that an element e has not been
used yet if and only if e is in U .
Another way to look at the algorithm of Code Fragment 5.11 is that it enumer-
ates every possible size-k ordered subset of U , and tests each subset for being a
possible solution to our puzzle.
For summation puzzles, U = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} and each position in the
sequence corresponds to a given letter. For example, the first position could stand
for b, the second for o, the third for y, and so on.
www.it-ebooks.info
5.3. Further Examples of Recursion 213
Algorithm PuzzleSolve(k, S, U ):
Input: An integer k, sequence S, and set U
Output: An enumeration of all k-length extensions to S using elements in U
without repetitions
for each e in U do
Add e to the end of S
Remove e from U {e is now being used}
if k == 1 then
Test whether S is a configuration that solves the puzzle
if S solves the puzzle then
add S to output {a solution}
else
PuzzleSolve(k − 1, S, U ) {a recursive call}
Remove e from the end of S
Add e back to U {e is now considered as unused}
initial call
PuzzleSolve(3, ( ), {a,b,c})
www.it-ebooks.info
214 Chapter 5. Recursion
• Test for base cases. We begin by testing for a set of base cases (there should
be at least one). These base cases should be defined so that every possible
chain of recursive calls will eventually reach a base case, and the handling of
each base case should not use recursion.
• Recur. If not a base case, we perform one or more recursive calls. This recur-
sive step may involve a test that decides which of several possible recursive
calls to make. We should define each possible recursive call so that it makes
progress towards a base case.
Parameterizing a Recursion
www.it-ebooks.info
5.5. Recursion Run Amok 215
www.it-ebooks.info
216 Chapter 5. Recursion
An Inefficient Recursion for Computing Fibonacci Numbers
In Section 2.2.3, we introduced a process for generating the progression of Fi-
bonacci numbers, which can be defined recursively as follows:
F0 = 0
F1 = 1
Fn = Fn−2 + Fn−1 for n > 1.
Ironically, a recursive implementation based directly on this definition results in the
method fibonacciBad shown in Code Fragment 5.13, which computes a Fibonacci
number by making two recursive calls in each non-base case.
c0 = 1
c1 = 1
c2 = 1 + c0 + c1 = 1 + 1 + 1 = 3
c3 = 1 + c1 + c2 = 1 + 1 + 3 = 5
c4 = 1 + c2 + c3 = 1 + 3 + 5 = 9
c5 = 1 + c3 + c4 = 1 + 5 + 9 = 15
c6 = 1 + c4 + c5 = 1 + 9 + 15 = 25
c7 = 1 + c5 + c6 = 1 + 15 + 25 = 41
c8 = 1 + c6 + c7 = 1 + 25 + 41 = 67
If we follow the pattern forward, we see that the number of calls more than doubles
for each two consecutive indices. That is, c4 is more than twice c2 , c5 is more than
twice c3 , c6 is more than twice c4 , and so on. Thus, cn > 2n/2 , which means that
fibonacciBad(n) makes a number of calls that is exponential in n.
www.it-ebooks.info
5.5. Recursion Run Amok 217
An Efficient Recursion for Computing Fibonacci Numbers
We were tempted into using the bad recursive formulation because of the way the
n th Fibonacci number, Fn , depends on the two previous values, Fn−2 and Fn−1 . But
notice that after computing Fn−2 , the call to compute Fn−1 requires its own recursive
call to compute Fn−2 , as it does not have knowledge of the value of Fn−2 that was
computed at the earlier level of recursion. That is duplicative work. Worse yet, both
of those calls will need to (re)compute the value of Fn−3 , as will the computation
of Fn−1 . This snowballing effect is what leads to the exponential running time of
fibonacciBad.
1 /∗∗ Returns array containing the pair of Fibonacci numbers, F(n) and F(n−1). ∗/
2 public static long[ ] fibonacciGood(int n) {
3 if (n <= 1) {
4 long[ ] answer = {n, 0};
5 return answer;
6 } else {
7 long[ ] temp = fibonacciGood(n − 1); // returns {Fn−1, Fn−2 }
8 long[ ] answer = {temp[0] + temp[1], temp[0]}; // we want {Fn , Fn−1 }
9 return answer;
10 }
11 }
Code Fragment 5.14: Computing the n th Fibonacci number using linear recursion.
In terms of efficiency, the difference between the bad and good recursions for
this problem is like night and day. The fibonacciBad method uses exponential
time. We claim that the execution of method fibonacciGood(n) runs in O(n) time.
Each recursive call to fibonacciGood decreases the argument n by 1; therefore, a
recursion trace includes a series of n method calls. Because the nonrecursive work
for each call uses constant time, the overall computation executes in O(n) time.
www.it-ebooks.info
218 Chapter 5. Recursion
www.it-ebooks.info
5.6. Eliminating Tail Recursion 219
www.it-ebooks.info
220 Chapter 5. Recursion
1 /∗∗ Returns true if the target value is found in the data array. ∗/
2 public static boolean binarySearchIterative(int[ ] data, int target) {
3 int low = 0;
4 int high = data.length − 1;
5 while (low <= high) {
6 int mid = (low + high) / 2;
7 if (target == data[mid]) // found a match
8 return true;
9 else if (target < data[mid])
10 high = mid − 1; // only consider values left of mid
11 else
12 low = mid + 1; // only consider values right of mid
13 }
14 return false; // loop ended without success
15 }
Code Fragment 5.15: A nonrecursive implementation of binary search.
www.it-ebooks.info
5.7. Exercises 221
5.7 Exercises
Reinforcement
R-5.1 Describe a recursive algorithm for finding the maximum element in an array, A,
of n elements. What is your running time and space usage?
R-5.2 Explain how to modify the recursive binary search algorithm so that it returns the
index of the target in the sequence or −1 (if the target is not found).
R-5.3 Draw the recursion trace for the computation of power(2, 5), using the traditional
algorithm implemented in Code Fragment 5.8.
R-5.4 Draw the recursion trace for the computation of power(2, 18), using the repeated
squaring algorithm, as implemented in Code Fragment 5.9.
R-5.5 Draw the recursion trace for the execution of reverseArray(data, 0, 4), from
Code Fragment 5.7, on array data = 4, 3, 6, 2, 6.
R-5.6 Draw the recursion trace for the execution of method PuzzleSolve(3, S,U), from
Code Fragment 5.11, where S is empty and U = {a, b, c, d}.
R-5.7 Describe a recursive algorithm for computing the n th Harmonic number, defined
as Hn = ∑nk=1 1/k.
R-5.8 Describe a recursive algorithm for converting a string of digits into the integer it
represents. For example, '13531' represents the integer 13, 531.
R-5.9 Develop a nonrecursive implementation of the version of the power method from
Code Fragment 5.9 that uses repeated squaring.
R-5.10 Describe a way to use recursion to compute the sum of all the elements in an
n × n (two-dimensional) array of integers.
Creativity
C-5.11 Describe a recursive algorithm to compute the integer part of the base-two loga-
rithm of n using only addition and integer division.
C-5.12 Describe an efficient recursive algorithm for solving the element uniqueness
problem, which runs in time that is at most O(n2 ) in the worst case without using
sorting.
C-5.13 Give a recursive algorithm to compute the product of two positive integers, m and
n, using only addition and subtraction.
C-5.14 In Section 5.2 we prove by induction that the number of lines printed by a call to
drawInterval(c) is 2c − 1. Another interesting question is how many dashes are
printed during that process. Prove by induction that the number of dashes printed
by drawInterval(c) is 2c+1 − c − 2.
www.it-ebooks.info
222 Chapter 5. Recursion
C-5.15 Write a recursive method that will output all the subsets of a set of n elements
(without repeating any subsets).
C-5.16 In the Towers of Hanoi puzzle, we are given a platform with three pegs, a, b, and
c, sticking out of it. On peg a is a stack of n disks, each larger than the next, so
that the smallest is on the top and the largest is on the bottom. The puzzle is to
move all the disks from peg a to peg c, moving one disk at a time, so that we
never place a larger disk on top of a smaller one. See Figure 5.15 for an example
of the case n = 4. Describe a recursive algorithm for solving the Towers of Hanoi
puzzle for arbitrary n. (Hint: Consider first the subproblem of moving all but
the n th disk from peg a to another peg using the third as “temporary storage.”)
C-5.17 Write a short recursive Java method that takes a character string s and outputs its
reverse. For example, the reverse of 'pots&pans' would be 'snap&stop'.
C-5.18 Write a short recursive Java method that determines if a string s is a palindrome,
that is, it is equal to its reverse. Examples of palindromes include 'racecar'
and 'gohangasalamiimalasagnahog'.
C-5.19 Use recursion to write a Java method for determining if a string s has more vowels
than consonants.
C-5.20 Write a short recursive Java method that rearranges an array of integer values so
that all the even values appear before all the odd values.
C-5.21 Given an unsorted array, A, of integers and an integer k, describe a recursive
algorithm for rearranging the elements in A so that all elements less than or equal
to k come before any elements larger than k. What is the running time of your
algorithm on an array of n values?
C-5.22 Suppose you are given an array, A, containing n distinct integers that are listed
in increasing order. Given a number k, describe a recursive algorithm to find two
integers in A that sum to k, if such a pair exists. What is the running time of your
algorithm?
C-5.23 Describe a recursive algorithm that will check if an array A of integers contains
an integer A[i] that is the sum of two integers that appear earlier in A, that is, such
that A[i] = A[ j] + A[k] for j, k < i.
www.it-ebooks.info
Chapter Notes 223
C-5.24 Isabel has an interesting way of summing up the values in an array A of n integers,
where n is a power of two. She creates an array B of half the size of A and sets
B[i] = A[2i] + A[2i + 1], for i = 0, 1, . . . , (n/2) − 1. If B has size 1, then she
outputs B[0]. Otherwise, she replaces A with B, and repeats the process. What is
the running time of her algorithm?
C-5.25 Describe a fast recursive algorithm for reversing a singly linked list L, so that the
ordering of the nodes becomes opposite of what it was before.
C-5.26 Give a recursive definition of a singly linked list class that does not use any Node
class.
Projects
P-5.27 Implement a recursive method with calling signature find(path, filename) that
reports all entries of the file system rooted at the given path having the given file
name.
P-5.28 Write a program for solving summation puzzles by enumerating and testing all
possible configurations. Using your program, solve the three puzzles given in
Section 5.3.3.
P-5.29 Provide a nonrecursive implementation of the drawInterval method for the En-
glish ruler project of Section 5.1.2. There should be precisely 2c − 1 lines of
output if c represents the length of the center tick. If incrementing a counter from
0 to 2c − 2, the number of dashes for each tick line should be exactly one more
than the number of consecutive 1’s at the end of the binary representation of the
counter.
P-5.30 Write a program that can solve instances of the Tower of Hanoi problem (from
Exercise C-5.16).
Chapter Notes
The use of recursion in programs belongs to the folklore of computer science (for example,
see the article of Dijkstra [31]). It is also at the heart of functional programming languages
(for example, see the book by Abelson, Sussman, and Sussman [1]). Interestingly, binary
search was first published in 1946, but was not published in a fully correct form until 1962.
For further discussions on lessons learned, see papers by Bentley [13] and Lesuisse [64].
www.it-ebooks.info