Chapter XXXVII
Chapter XXXVII
Recursion II
37.1 Introduction
You have also used very little recursion in the first part of the second year course
when you devoted most of the time to learn object oriented programming. So I
am going to do something unusual. The entire Chapter XXIII will be repeated
here followed by some advanced topics meant strictly for second year students.
The topic of recursion is vitally important for second year students and in a few
chapters you will be up to your ear lobes in recursive functions. I think it is wise
to simply start from scratch, assume nothing, and then move on to more advanced
topics. If you are comfortable with recursion, the majority of this chapter will go
very rapidly for you. If you never learned recursion or if you feel insecure, go
over this chapter very slowly and very methodologically.
Right now the only difference is this introduction. If the introduction was
identical to chapter XXIII, you may feel that the some mistake had been made
with file copying. Furthermore, the introduction in the first year indicates that
recursion will not be used a lot in the first year (nobrainer the year is just about
over) but the topic will be very important for the second year course. Do you still
remember the recursion definition?
Recursion Definition
This chapter will start in an unusual manner. A free response problem will be
explained shortly that is similar to a problem presented to students at one of the
earlier AP Computer science Examinations. The solution of this problem, called
Grid, is done surprisingly easy with the concepts explained in this chapter. In an
effort to prove a point you are presented with this problem before recursion is
explained.
Try to solve the Grid problem without using recursion. In other words, write a
solution to the problem using loop structures. Doing this exercise first, before we
look at recursion, will help to motivate why recursion is such a useful, and
powerful tool.
It is unlikely, but certainly possible, that you already understand recursion real
well, and that you are able to write a recursive solution to the Grid problem.
Doing so will defeat the purpose of this exercise. Play along and you will
understand soon enough why it is important to try this problem iteratively first.
Write function AlterGrid with the following header such that all cells in the same
object as Grid[Row,Col] are set to White; otherwise Grid is unchanged.
The instructions for the Grid problem are very similar to the instructions that were
given to students who took the 1987 AP Computer Science Examination. No
additional information, examples, hints, etc. will be given.
If you are a student at Berkner High School, and Mr. Schram is in a good mood,
you will receive extra credit for being able to write an iterative solution to the
Grid problem. You will receive negative credit for turning in a recursive
solution, which happens to be shown in this chapter, and proves that you do not
follow instructions.
Your solution needs to be printed, and it needs to be turned in before the first
official lecture on the recursion chapter. Give this problem some serious thought.
We will return to this problem at the end of the chapter, and then this exercise will
make more sense.
37.3 Recursion Requires an Exit
More than once you have wondered about Mr. Schram’s sanity. This sudden
switch to using BASIC is confirmation that Mr. Schram is not operating on all
thrusters. We can certainly program in C++ without using BASIC, but the
concept is nicely demonstrated in BASIC. The variable K is incremented by one,
displayed, and GOTO jumps back to line 120 to repeat the process. This process
will go on for quite some time because nothing has been done to stop the loop.
The GOTO statement specifies to go back to line 120 and repeat the process.
There is no conditional statement that stops the repetition. A similar result can be
achieved in C++ with recursion, as shown in program PROG3701.CPP below.
// PROG3701.CPP
// This program demonstrates recursion without an exit
#include <iostream.h>
#include <conio.h>
#include <iomanip.h>
int K;
void Count()
{
K++;
cout << setw(8) << K;
Count();
}
void main()
{
K = 0;
clrscr();
Count();
getch();
}
1 2 3 5 5 6 7 8 9
10
11 12 13 14 15 16 17 18 19
20
21 35 23 24 25 26 27 28 29
30
31 32 33 34 35 36 37 38 39
40
41 42 43 44 45 46 47 48 49
50
51 52 53 54 55 56 57 58 59
60
61 62 63 64 65 66 67 68 69
70
71 72 73 74 75 76 77 78 79
80
81 82 83 84 85 86 87 88 89
90
91 92 93 94 95 96 97 98 99
100
Theoretically, this output will never stop and the numbers will keep increasing.
However, after the output continues for some time, the program will
freeze.
You may or may not see an error message.
This program will count numbers, incrementing by 1, just like the BASIC
program. It may seem that this odd kind of program will continue to count until
the power is turned off or the computer is rebooted. Function Count calls itself
every time that K is incremented and displayed. There appears to be nothing that
will stop this infinite counting process. But very soon the execution is halted with
or without an error message. The problem is that you have a stack overflow.
For the moment being, do not be concerned about what exactly a stack over flow
error is. Accept the fact that your program will crash with some type of problem.
It may be sooner; it may be later; but accept the fact that any recursive process
that is allowed to continue without a planned method of stopping the recursive
calls will crash the computer. Absolutely guaranteed.
Recursion simulates iteration. With iterative control structures, whether you are
using for, while or do...while, a condition is stated that stops the looping process.
One such example is shown with program PROG3702.CPP, which resembles the
previous program with a minor modification. A small if condition has been added
to stop the counting beyond 100.
// PROG3702.CPP
// This program demonstrates recursion with an exit
#include <iostream.h>
#include <conio.h>
#include <iomanip.h>
int K;
void Count()
{
K++;
cout << setw(8) << K;
if (K < 100)
Count();
}
void main()
{
K = 0;
Count();
}
PROG3702.CPP
1 2 3 5 5 6 7 8 9
10
11 12 13 14 15 16 17 18 19
20
21 35 23 24 25 26 27 28 29
30
31 32 33 34 35 36 37 38 39
You were shown a very important recursion fundamental concept in the previous
section. Make sure that your recursive routine has an exit. There are various
other fundamental concepts about recursion that need to be understood before any
serious programming can involve recursion. These fundamentals will be explored
with the next three programs, which include short functions called Guess1,
Guess2 and Guess3.
// PROG3703.CPP
// What is the output of GUESS1?
#include <iostream.h>
#include <conio.h>
void Guess1()
{
char Letter;
Letter = getch();
cout << Letter;
if (Letter != 'O')
Guess1();
}
void main()
{
clrscr();
cout << "Enter HELLO" << endl;
Guess1();
getch();
}
PROG3703.CPP OUTPUT
HELLO
You are probably not confused about the output of the previous program. Looks
simple enough. Enter a letter, and display a letter. Keep up this process until O is
entered and you are done. Enter Hello ... and what a surprise, the program
displays Hello.
The next program with Guess2 adds a new twist. The cout statement comes at
the end of the function. This means that the recursive call prevents the function
from completing its normal execution sequence. What exactly does this mean?
// PROG3704.CPP
#include <iostream.h>
#include <conio.h>
void Guess2()
{
char Letter;
Letter = getch();
if (Letter != 'O')
Guess2();
cout << Letter;
}
void main()
{
clrscr();
cout << "Enter HELLO" << endl;
Guess2();
getch();
}
PROG3704.CPP OUTPUT
OLLEH
What kind of dumb process takes HELLO as input and then displays OLLEH?
Students often expect that the output should be a single O letter. You may use the
logic that the cout statement is not reached until the base case is reached. When
O is finally entered, the condition statement is false, and the recursive calls stop.
Finally, cout can display something, and the character just entered is the letter O.
But where do the previous letters come from, and why are they displayed in
reverse order? The answer to that question will require a little detour.
C++ has a special data structure called a stack that is hidden from the
programmer. Programmers cannot use the stack directly, but you can detect its
presence in a variety of ways. A stack is a data structure that will only access
NOTE
Do not confuse the internal stack with
the abstract stack data structure that
was introduced in Chapter XXXV.
So what is the job of this mysterious stack? Consider the following. Every
program statement is stored somewhere in computer memory, in machine code,
understandable only to the computer and people who like to read binary machine
code. Program execution sequence is handled by something called an Index
Pointer. The Index Pointer, or IP for short, contains the next memory address of
the line to be executed. In a simple program without any function calls,
consecutive program statements are usually consecutive memory addresses. The
value of the IP is increased by the memory size of the program statements.
Life is easy for the IP, but what address is used by the IP to resume execution.
That will be different each time that the same function is called. What comes to
the rescue is our friendly neighborhood stack. The stack is a temporary storage
structure that holds values in the unique LIFO manner. This system is made to
order to solve the function problem. When a function is called, the memory
location of the statement following the function call is added - called “pushed” -
to the stack. The stack now continues to do service by storing local variable
values of the function that are currently in scope. In other words, the stack takes
The stack really demonstrates its LIFO business when functions call other
functions. Imagine that you start in the main function and call function First.
Somewhere in the middle of First, function Second is called. Now when Second
is finished where does it return? Should program execution continue back in the
main function or function First? Hopefully you realize that execution returns to
the location where the function was called. This means back to function First, and
then after First is finished, execution continues in the main function. Do you see
how neat the stack handle this business. First a main function address is pushed
on the stack, and then a First function address. The LIFO (Last In, First Out)
controls the execution perfectly.
Digest this stack business, and return to functions Guess1 and Guess2. Letter is
a local variable in both functions and its value will be stored on the temporary
stack that takes care of function business. Keep in mind that all function
information is stored on the stack until the function has finished executing.
Guess1 has no problems. A letter is entered and immediately displayed. Guess2
does not have a chance to display the letter value. A recursive call interrupts the
function execution. This interruption continues until the base case is reached.
void Guess1()
{
char Letter;
Letter = getch();
cout << Letter;
if (Letter != 'O')
Guess1();
}
void Guess2()
{
char Letter;
Letter = getch();
if (Letter != 'O')
Guess2();
cout << Letter;
}
Now comes the really critical part. The second function call leaves the first
function calls interrupted and unfinished. This means that critical information,
such as the value of Letter = ’H’ is kept on the stack. When the base case is
finally reached the letter ’O’ is displayed.
You need to consider one more Guess function. The next program shows
Guess3, a function that performs the same action as the previous Guess functions,
but this time Letter is a reference parameter. Does this matter? It matters in the
sense that the value of Letter is not stored on the stack. The function execution
sequence will be controlled by the stack. The value of Letter will be controlled
by some memory location that is not part of the stack business.
// PROG3705.CPP
// What is the output of GUESS3?
#include <iostream.h>
#include <conio.h>
void main()
{
clrscr();
cout << "Enter HELLO" << endl;
char Letter;
Guess3(Letter);
getch();
}
What do you suspect will be the output of this program? Is it OLLEH like it was
before? Are there a few students left who think it may be just the single letter O?
Are you clueless and do not really know what is happening in this situation?
PROG3705.CPP OUTPUT
How about five identical letters in a row. Weird? No, perfectly logical. Consider
this. Function Guess3 is called five times, which means that there must be five
cout output statements happening somewhere. These output statements need to
wait for the stack to take care of unfinished business. Like before, the stack
finishes each function call in LIFO order.
This time there is a new twist. The value of Letter is not preserved. The second
call to Guess3 enters a new value for Letter, and replaces its previous value.
When the base case (letter O) is reached, recursion stops, the letter O is displayed
and now four unfinished output statements display the value of Letter. At this
time Letter stores the character O, and the result is five identical O letter outputs.
The Skip function has served you well in many programs. Skip was used to
introduce functions with parameters. It will now be used to introduce the first
recursive function that uses parameters. But first take a close look at the iterative
Skip function.
// PROG3706.CPP
// Iterative Skip function
void Skip(int N)
{
int K;
for (K = 1; K <= N; K++)
cout << endl;
}
The parameter N, in the Skip function, dictates how many times cout << endl;
will be used. A for loop is used with N, determining how many times the loop
will be iterated. Skip is not exactly a complicated function, and some students
reading this section may feel insulted by the explanation of such a trivial piece of
programming code.
The main point here is to learn how to simulate iteration with recursion. Iterative
Skip uses a loop that repeats the cout statement, N times. Duplicating the
// PROG3706.CPP
// Recursive Skip function
void Skip(int N)
{
if (N > 0)
{
cout << endl;
Skip(N-1);
}
}
The recursive Skip function starts with protection against any values of N that are
less than or equal to 0. This condition also provides an exit for the function to
stop the recursive calls.
If the condition (N > 0) is true, one line is skipped with cout << endl; Repetition
is simulated by Skip calling itself. The recursive call is simple enough, but the
key factor is the parameter N-1. Each recursive call is made with a lesser value
for N. Eventually N is reduced to the base case.
Euclid’s algorithm for finding the GCF pops up at many computer science
locations, or at least it pops up a lot in Exposure C++. I really like this algorithm
and it demonstrates many concepts so nicely. Right now it will do an excellent
job demonstrating how parameter manipulation makes recursion both possible
and elegant. You thought that elegant applies to evening gowns? Think again, in
computer science a solution can also be elegant. I am not sure that I can define
elegant properly, but it appears that elegant solutions are very brief, straight
forward and usually not the first thing coming to the mind of an unsuspecting
computer science student. However, an elegant solution is frequently the most
natural and “obvious” solution to many computer science vetrans.
First, let us review the iterative GCF very rapidly. Remember the algorithm?
Refresh yourself quickly with this example.
N1 becomes N2
N2 becomes the remainder
Divide N1 (74) by N2 (37) and compute the remainder, which is 0.
Since the remainder is 0, you are finished, and the GCF is 37.
The iterative GCF function, below, implements Euclid’s algorithm with these
precise same steps. Trace through the program statements, and you will see that it
matches the description above So how is this done recursively?
// PROG3707.CPP
// Iterative GCF function
PROG3707.CPP OUTPUT
If elegant means shorter than you cannot argue that the recursive solution is more
elegant than the iterative variety. There are strong similarities in both solutions.
In each case, the Remainder of N1 and N2 is computed first. Also, in both cases
a conditional statement checks if the remainder is equal to zero, and if it is ... the
value of N2 is returned.
Now the logic stays the same but the implementation is quite different. The
recursive solution calls GCF when the base case is not satisfied. Keep in mind
that it is not sufficient to repeat the process. The algorithm requires that N1
becomes N2 and Remainder becomes N2.
Look how we can cleverly manipulate the parameters. The recursive function
call, return GCF(N2,Rem), passes the value of actual parameter N2, to formal
parameter N1. Also the value of actual parameter Rem is passed to formal
parameter N2.
// PROG3708.CPP
// Iterative Factorial function
long Factorial(int N)
{
long Temp = 1;
int K;
for (K=N; K>=1; K--)
Temp *= K;
return Temp;
}
The iterative function sets a local variable, Temp, to 1. This value will be
returned if the loop structure is not entered. If the loop is entered, Temp is
multiplied by the loop control variable from N down to 1. The loop can count,
and multiply in both directions, the backward counting was selected because it
resembles the way that we compute factorial more closely.
// PROG3708.CPP
// Recursive Factorial function
long Factorial(long N)
{
if (N == 0)
return 1;
else
return N * Factorial(N - 1);
}
Does this recursive function make any sense to you? Let us trace through an
example of computing 4! and see what happens. The function checks first to see
if parameter N is equal to 0, otherwise the function returns 4 times Factorial(3).
Now back up to all the unfinished functions and complete the multiplication,
which is 1 x 2 x 3 x 4 = 24
PROG3708.CPP OUTPUT #1
6! = 720
PROG3708.CPP OUTPUT #2
10! = 3628800
PROG3709.CPP OUTPUT #1
PROG3709.CPP OUTPUT #2
The complete program is shown with both the iterative and recursive function.
Does the logic of the functions make sense? The recursive solution certainly
appears to be much shorter. Now go ahead and test this program. You will be
surprised with the outcome of the recursive function. Why is it so slow? Try the
30th or 35th number and you will observe a noticeable delay.
// PROG3709.CPP
// Recursive Fibonacci function
#include <iostream.h>
#include <conio.h>
long Fibo(int N)
{
if ((N == 1) || (N == 2))
return 1;
else
return Fibo(N-1) + Fibo(N-2);
}
/
*******************************************************
*******
long Fibo(int N)
{
long Temp1 = 1;
long Temp2 = 1;
long Temp3;
int K;
for (K=3; K<=N; K++)
{
Temp3 = Temp1 + Temp2;
Temp1 = Temp2;
Temp2 = Temp3;
}
return Temp3;
void main()
{
int Number;
long Result;
clrscr();
cout << "ENTER THE DESIRED FIBONACCI NUMBER ===>>
";
cin >> Number;
Result = Fibo(Number);
cout << endl << endl;
cout << "FIBONACCI NUMBER # " << Number
<< " IS " << Result << endl;
getch();
}
Look at the recursive statement in the Fibonacci function. Do you see anything
that you did not notice in any of the previous examples? Do not say that the
variables have different names. Look at the nature of the recursive statements. Is
there something odd, unusual, that catches your eye?
return N * Factorial(N-1);
return GCF(N2,Rem);
Yes, you do see it, and you did not act like you did. The Fibonacci function
generates two recursive calls in one statement. There is only one call for any of
the other functions that we checked. Is this then bad? For small numbers this is
not bad, but the consequences of these double recursive calls are disastrous for a
large number. Each recursive statement doubles the number of recursive calls.
On the next page, we will demonstrate just calling the 9th number. Check and see
how many calls this ends up generating.
7 8
5 6 6
7
3 4 4 5 4 5 5
6
1 2 2 3 2 3 3 4 2 3 3 4 3 4
4 5
12 12 12 23 12 12 23 12 2
3 23 3 4
12 12 1
2 12 12 2 3
12
Recursion Warning
So far this chapter has talked a lot about recursion and you have been shown a
variety of functions with iterative and recursive solutions. This information helps
to understand the nature of recursive routines.
You will also need to be able to evaluate recursive subprograms. Given some
function, can you determine what the value or values the functions return when
recursion is involved? This section gives you a variety of exercises to help you
gain practice and confidence in evaluating recursive functions.
In earlier chapters you were encouraged to use a variable trace table to keep
track of the variable values in a program. Consider the following iterative
function YoHo.
What is the value of YoHo(112,72)? Assume that you do not realize that function
YoHo computes the GCF of P and Q. A good approach is to use a variable trace
table. This table is nothing more than keeping track of variable values as the
program execution steps through the iterations.
P Q R YoHo Returns
112 72 40 N/A
72 40 32 N/A
40 32 8 N/A
32 8 0 8
Be careful that you do not stop after a few examples. This is not a group of
fourteen exercises of the same type. There is a considerable variety of problems
presented. Understanding this group of exercises is a good start in understanding
recursion.
int F1(int N)
{
if (N == 1)
return 25;
else
return F1(N-1);
}
CALL # N Function F1 returns
1 5 F1(4)
2 4 F1(3)
3 3 F1(2)
4 2 F1(1)
5 1 25
Exercise 02
int F2(int N)
{
if (N == 1)
return 25;
else
return F2(N+1);
}
CALL # N Function F2 returns
1 5 F2(6)
2 6 F2(7)
3 7 F2(8)
int F3(int N)
{
if (N == 1)
return 25;
else
return N + F3(N-1);
}
CALL # N Function F3 returns
1 5 5 + F3(4)
2 4 4 + F3(3)
3 3 3 + F3(2)
4 2 2 + F3(1)
5 1 25
F3(5) = 25 + 2 + 3 + 4 + 5 = 39
Exercise 04
int F4(int N)
{
if (N == 5)
return 0;
else
return N + F4(N+1);
}
CALL # N Function F4 returns
1 1 1 + F4(2)
2 2 2 + F4(3)
3 3 3 + F4(4)
4 4 4 + F4(5)
5 5 0
F4(1) = 0 + 4 + 3 + 2 + 1 = 10
int F5(int N)
{
if (N == 1 || N == 0)
return 0;
else
return N + F5(N-1) + F5(N-2);
}
CALL # N Function F5 returns
1 6 6 + F5(5) + F5(4)
2 5 5 + F5(4) + F5(3)
3 4 4 + F5(3) + F5(2)
4 3 3 + F5(2) + F5(1)
5 2 2 + F5(1) + F5(0)
6 1 0
F5(6) = 0 + 2 + 3 + 4 + 5 + 6 = 20
Using previous techniques, the answer appears to be 20. However, you are
expected to determine that F5(6) = 38. Something is wrong, and the problem
is that exercise 05 involves nasty double recursive calls. The tree structure below
determines the value that must be added at each function call. A call to F5(1) or
F5(0) will result in 0 being added.
6
5 4
4 3 3 2
3 2 2 1 2 1 1 0
2 1 1 0 1 0 1 0
1 0
Now we have 6 + 5 + 4 + 4 + 3 + 3 + 2 + 3 + 2 + 2 + 2 + 2 = 38
int F6(int N)
{
if (N == 1)
return 1;
else
return N * F6(N-1);
}
CALL # N Function F6 returns
1 5 5 * F6(4)
2 4 4 * F6(3)
3 3 3 * F6(2)
4 2 2 * F6(1)
5 1 1
F6(5) = 1 * 2 * 3 * 4 * 5 = 120
Exercise 07
F7(5,6) = 0 + 6 + 6 + 6 + 6 + 6 = 30
F8(4,3) = 1 * 3 * 3 * 3 * 3 = 81
Exercise 09
F9(4,3) = 1 * 4 * 4 * 4 = 64
1 7 3 3 + F10(6,4)
2 6 4 4 + F10(5,5)
3 5 5 5 + F10(4,6)
4 4 6 5
F10(7,3) = 5 + 5 + 4 + 3 = 17
Exercise 11
Determine that the output of F11 will be HOWDY when YDWOH is entered.
void F11()
{
char CH;
CH = getch();
if (CH != 'H')
F11();
cout << CH;
}
CALL # CH Function F5 outputs
1 Y Nothing, function not completed
2 D Nothing, function not completed
3 W Nothing, function not completed
4 O Nothing, function not completed
5 H H
F11 output ===>> HOWDY (LIFO function completion)
void F12()
{
char CH;
CH = getch();
cout << CH;
if (CH != ’O’)
F12();
cout << CH;
}
CALL # CH Function F5 outputs
1 H H, second cout not yet completed
2 E E, second cout not yet completed
3 L L, second cout not yet completed
4 L L, second cout not yet completed
5 O OO
F12 output ===>> HELLOOLLEH
Exercise 13
Exercise 14
We are now going to move recursion up one notch on the difficulty ladder. All
the previous examples have been fairly straight forward and the comparisons
between iterative and recursive solutions always involved the same parameters. It
did not really seem to matter how the procedure or function heading was stated.
// PROG3710.CPP
// Iterative Linear Search Function
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include "BOOL.H"
#include "APVECTOR.H"
void main()
{
clrscr();
ListType List(10);
int SearchNumber;
int Index;
for (int K = 0; K < 10; K++)
{
List[K] = random(9000) + 1000;
cout << List[K] << " ";
}
cout << endl << endl;
cout << "Enter search number ===>> ";
cin >> SearchNumber;
LinearSearch(List,SearchNumber,Index);
if (Index == -1)
cout << SearchNumber << " is not in the list" <<
endl;
else
PROG3711.CPP OUTPUT #1
PROG3711.CPP OUTPUT #2
So why is there a need to alter the function heading for a recursive function? The
logic is with the loop control variable K. The variable K is incremented
throughout the while loop. When you change the loop structure and implement
recursion, the parameters List, Key and Index are still needed. And where
exactly is our friend K located? Using the same logic as the iterative function
cannot work because K is defined locally, and initialized to 0. This works fine for
a loop structure, but a recursive solution would constantly reset K to 0. The trick
is that K needs to become a parameter. This time only the recursive function is
shown. The driver program for the LinearSearch is the same, except for the
function call.
The addition of the K parameter solves our problem very nicely. Each time that a
recursive call is made, the value of actual parameter K+1 is passed to formal
parameter K. This will achieve the desired incrementing of the array index.
// PROG3711.CPP
// Recursive Linear Search Function
LinearSearch(List,SearchNumber,Index,0);
The lessons that you learned with the recursive LinearSearch function will help
in solving the BinarySearch function. LinearSearcxh only required that one
parameter, K, is added to the function heading. You will find that BinarySearch
is going to be a little more complicated. But first review the logic of the iterative
function. The driving program is identical to the one used for the LinearSearch.
// PROG3712.CPP
// Iterative Binary Search Function
This function has many variables. The following list helps to clarify the purpose
for each one of the parameters, and local variables.
PROG3712.CPP OUTPUT
The important feature of the binary search is the search range. In a linear search
the search range is the entire array. In a binary search the initial search range is
the entire array. The index of the first element of the first search is 0, and that
value is assigned to Lo. The index of the last element of the search range is N-1,
and that value is assigned to Hi. The binary search performs a comparison with
the element in the middle of the search range. Variable Mid is computed as the
mean of Lo and Hi. After each comparison, the search range is “chopped” in
The iterative solution defines Lo, Hi and Mid locally. Recursively, there is no
problem with using Mid locally. The value of Mid needs to be computed each
time that a new location is searched. However, Lo and Hi are a different story.
These index values change with each recursive call. Check the code below. Does
it make sense to you?
// PROG3713.CPP
// Recursive Binary Search Function
An initial call to BinarySearch, made from the main function uses a function call
with similar constant-value-logic like the LinearSearch fucntion.
BinarySearch(List,Key,Index,0,N-1);
If the search item, Key is not found and Key is greater than List[Mid], the lower
half of the list can be ignored. This means that the Hi value remains unchanged,
but we have a new Lo, which is one greater than Mid. This will result in the
recursive function call below.
On the other hand, if Key is less than List[Mid], the upper half of the list can be
ignored. This means that the Lo value remains unchanged, and we have a new
Hi, which is one less than Mid. This will result in the recursivie function call
below.
BinarySearch(List,Key,Index,Lo,Mid-1);
Ponder over this question. In this section you were shown a linear search function
and a binary search function that are implemented recursively by adding
parameters to the function heading. Can you write these functions recursively
with the same exact parameter list as the iterative solutions?
So what is the problem? Look at the pictures below and on the next page. Three
towers, or pegs are shown. The towers are labeled A, B and C. Tower A has five
numbered disks. The disks are stacked with the largest disk on the bottom. Your
problem is to move all the disks from tower A to tower C. There is a catch,
though. You can only move one disk at a time and you are not allowed to stack a
larger disk on top of a smaller disk. With the three pegs, it is possible to
accomplish this mission. The second peg can be used as an auxiliary peg.
1 1
2 2
3 3
4 4
5 5
Peg A Peg B Peg C
1 1
2 2
3 3
4 4
5 5
Peg A Peg B Peg C
1 1
Peg A Peg B Peg C
How do we solve the 1-Disk problem? In exactly one step that be described with
the single statement:
1 1
Peg A Peg B Peg C
The solution to the 1-Disk problem is commonly known as the trivial case. The
example was shown here not to insult your intelligence, but to make sure that you
focus on the trivial case. We will be seeking a recursive solution to this problem
and it may just turn out that the trivial case also is very much part of a solution
with more than one disk.
Ironically, students have the biggest problem in properly setting up the base case
of a recursive solution to a problem. Basically, this means to start by considering
what is necessary to get out.
Step 1
1 1
2 2
Peg A Peg B Peg C
Step 2
2 2 1 1
Peg A Peg B Peg C
Step 3
1 1 2 2
Peg A Peg B Peg C
Step 4
The 2-Disk situation is also pretty easy to follow. It can be summed up with the
following three disk moves:
I will not bother you with all the pictures of a 3-Disk solution. (besides this really
is kind-of-a-pain to word process) Study the steps and see if they make sense to
you. If necessary, take some objects and simulate the Tower of Hanoi problem.
Here is the 3-Disk solution.
The complexity of this problem grows very rapidly as the number of disks grows.
The number of moves that are made by the solution also grows very rapidly. The
1-Disk problem takes 1 move. The 2-Disk problem takes 3 moves. The 3-Disk
problem takes 7 moves. And a 4-Disk problem takes 15 moves. Perhaps you
have recognized the pattern by now. The number of moves is 2N-1.
2N - 1
It may seem like a very difficult task to write a program that solves the Towers of
Hanoi puzzle. It would not be so tricky for a fixed number of disks, like 4, but
you may be perplexed by a general program that will handle any given number of
disks.
The Towers of Hanoi program in this chapter solves the puzzle, and furthermore it
solves the puzzle with an absolute minimum of code. The code is recursive and it
will not be easy to understand at first or second glance.
You may see statements in various computer science text books with the
statement the correctness of the TowersOfHanoi program is easily established.
Such statements are intimidating to students who are confused by some bizarre
looking programming code. Take your time to digest the logic and syntax. Listen
to your teacher’s explanation and allow this recursive business to start soaking in.
You will need to be pretty drenched before you feel comfortable. But suddenly,
one day, it all starts to click and recursion will be your friend from then on.
On the next page is a program that solves this interesting puzzle. A sample
execution follows, to demonstrate the output of the program. Some explanation
will be given later about the logic of the program solution, but right now there is
another objective. Take a look at the solution. Never mind whether it makes
sense or not. Isn’t the function that moves the disks amazingly short.
If you do not think that the solution is short, get behind your computer and write a
solution that does not use recursion. It can be done iteratively and your teacher
will offer you some major extra credit if your iterative solution is shorter than the
recursive solution.
// PROG3714.CPP
// Recursive Tower of Hanoi function
#include <iostream.h>
#include <conio.h>
void main()
{
int N;
clrscr();
cout << "Enter how many disks [1..8] ===> ";
cin >> N;
cout << endl << endl;
SolveHanoi('A','B','C',N);
getch();
}
PROG3714.CPP OUTPUT
You have noticed in the examples used in this chapter so far that recursive
solutions have been shorter than iterative solutions. For the most part, this has
fallen in the whoop..tee..doo category. The difference in code has not been
significant enough to be overly concerned. With this program you cannot
compare. Will an iterative solution be shown? No, it will not, since it is an extra
credit assignment. However, you can probably still appreciate that this
SolveHanoi function is very short, and accomplished a lot.
So how do we make sense out of this. I will assume that you understand clearly
how to move a single disk. Now this simplest problem does not require a loop or
any type of recursion. This simplest situation not only occurs if you have a
problem with one disk, it also occurs in all the problems when all the disks are on
the destination peg, except for the last disk. This one-disk move shall be the
trivial case.
Now recursion requires a general statement that solves the problem for the
situation that is not the base case. For instance, with the Factorial function the
general solution for integer N is to multiply N * Factorial(N-1), provided N is
greater than 0. Can you think about the Tower of Hanoi in a general way? Do
not think about C++ program code right now. State the general steps required to
solve this puzzle.
These three English program solution statements are very significant. The middle
statement sure sounds a bunch like the trivial case. Essentially, the statement says
that after you get rid of N-1 disks that are on top of the Nth disk, the Nth disk can
now easily be moved to its final destination. With the Nth disk in the correct
place, you can now move N-1 disks from the temporary peg to the final
destination on top of the Nth disk.. These three general steps are illustrated with
a diagram on the next page.
1 1
2 2
N-2 N-2
N-1 N-1
N N
Peg A Peg B Peg C
1 1
2 2
N-2 N-2
N N N-1 N-1
Peg A Peg B Peg C
1 1
2 2
N-2 N-2
N-1 N-1 N N
Peg A Peg B Peg C
Moving N-1 disks from one peg to another peg should sound like a similar
solution required with one less disk. It also sounds like this process continues
until only one disk is left. Those students whose recursive-understanding-light-
bulbs have acquired a considerable glow are smelling serious recursion here. But
something else is going on. Every one of the recursive examples presented in this
chapter only had a single statement to make recursive calls. The Fibonacci
function did call two functions recursively, but it was in one statement.
Take a look at the SolveHanoi function again. You will recognize the three
English statements of the general solution in the C++ code. You will also note
that there are two separate statements that make recursive calls. Do not think that
this is unusual. There are many problems in computer science that are solved
with multiple recursive calls.
But there remains a significant problem to solve. If you simulate the Tower of
Hanoi puzzle with some disks, blocks, books or any other objects, you will
rapidly notice a pattern. The source peg and destination peg are constantly
changing. With the three pegs the initial aim is to move N disks from A to C.
But first you need to move N-1 disks from A to B. This means that for N-1 disks
the destination peg is now B. And later when these same disks have to be moved,
B will be the source peg.
This constantly changing of source and destination is the secret in the solution.
The parameter list of the SolveHanoi function shows four parameters. The first
three parameters are used to store the peg designation with characters A, B or C.
The fourth parameter, N, passes the number of disks to be moved.
SolveHanoi('A','B','C',N);
Notice how the parameters are called Source, Temp(orary) and Dest(ination).
The initial call, made in the main function, passes A to Source, B to Temp and C
to Dest. This all seems to match what is desired.
Now our first job is to move N-1 disks from Source to Temp. Look at the first
recursive statement. Source is passed to Source, Dest is passed to Temp, and
Temp is now passed to Dest. Is this a confusing mess, or do you see what is
going on? The parameters of the recursive calls are constantly changing what is
Source, Temp and Dest.
Notice in the second recursive call that now Temp is passed to Source. This is
precisely what is needed, because you have N-1 disks sitting on the temporary
peg. After the Nth disk is moved to its proper location, it is time to move the N-1
disks, and they are located on the temporary peg.
Some students are not convinced. You do buy the movement of the single Nth
disk, but there seems to be some casual movement of N-1 disks going on. Is there
not a rule about moving one disk at a time? You are totally correct and totally
alert. After the Nth disk is in place we ignore that disk and ask how we can move
N-1 disks. This is done by first moving N-2 disks out of the way to a temporary
location, so that the (N-1th) disk can find its home. This process is continued
until the trivial case makes life simple by moving a single disk. What you have is
pure recursion.
Did we forget to consider how the recursion will stop? No, it has not been
forgotten. Function SolveHanoi can only generate recursive calls, and move a
disk whenever (N != 0) is true. Any time that the disk count become 0, it is time
to say good bye and return to complete unfinished business from previous
function calls.
The recursive Bubble Sort section may start a petition going on around
questioning the sanity of one “formerly-reasonably-informed” computer science
teacher. Experienced computer science teachers may suspect that Leon Schram
had one too many wipe-outs during a recent ski trip and cerebral damage
unfortunately seems to be of the permanent variety. Who in their right mind
would consider using BubbleSort with recursion of all things? This is doubly bad.
The Bubble Sort is pitiful to start with and doing it recursively it like taking a
really, really sorry tasting dinner and putting it in a micro wave several days later
for a leftover meal. Well relax.
The essence of the Bubble Sort algorithm is the comparison loop that compares
adjacent array elements and swaps elements, which are not properly ordered. At
the conclusion of the comparison loop, one element (smallest or largest) is in its
desired location. The comparison loop is nested inside an outer loop that is
designed to repeat the comparison passes until the list is sorted.
If we are only considering a single loop at this stage, our life is not that
complicated. After all, was the linear search algorithm not a case of a single
loop? Program PROG3715.CPP creates a random list of integers, displays the
random list, sorts the list with a partial Bubble Sort, and displays the list again.
With the partial sort you will only see the results of a single comparison pass.
This program is shown completely. After this program we will only focus on the
Bubble Sort routine.
// PROG3715.CPP
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <iomanip.h>
#include "APVECTOR.H"
void main()
{
clrscr();
ListType List(12);
CreateList(List);
DisplayList(List);
SortList(List,0);
DisplayList(List);
}
PROG3715.CPP OUTPUT
1095 1035 4016 1299 4201 2954 5832 2761 7302 9549 3473
4998
1035 1095 1299 4016 2954 4201 2761 5832 7302 3473 4998
9549
The recursive SortList function shows a process that is starting to look familiar
by now. First you check if the index counter, Q, is still less than N-1. If that
condition is true, adjacent array elements are compared, possibly swapped, and a
recursive call repeats the process with the index incremented by 1.
Now you are about to follow into a potential trap. You feel that recursion works
just fine for the inner loop. A recursive call replaced the loop structure. Logic
should therefore dictate that another recursive call and another exit takes care of
the original outer loop of the bubble Sort routine.
// PROG3716.CPP
// Recursive Bubble-Sort Algorithm
// This program does not provide a proper exit
So what is the problem? Cold booting in C++ is not that much fun, and if you
also have to wait for Windows 95 to start over after a cold boot, you might as well
order some pizza. There is a very distinct problem, and it is very subtle. It goes
straight to one of the fundamental rules of recursion.
Perhaps it will help to first look at the correct recursive function. You may need
to look twice before you see that the only difference is an innocent, and very
significant, else in the correct function.
The subtlety of the problem has to do with unfinished function calls. It is your
aim to repeat the comparison pass N-P times. This is achieved by making a
recursive call, that increments Q each time. Fine, this works. Now you may be
tempted to say so Q is no longer the proper value, and the comparison pass stops.
This is precisely when you want to switch to a recursive call with P incremented.
This is exactly what you want to do, but without the else you get more than you
bargained for.
Think about it. Every time that you repeated the comparison pass, you left the
function unfinished. At some point you need to back up and clean up all the
unfinished business. Imagine that you made 50 recursive calls to complete one
comparison pass. Without the else you now have 49 unfinished functions to
return to and this means 49 recursive calls.
None of this business happens with the modest else statement. Here we perform
comparisons, complete with recursive calls, or we make a recursive call that
increments P. The else insures that one recursive call is made or the other call is
made. The bottom line is that you do not have a whole bunch of unwanted
recursive calls left to be finished.
After all this recursion business we are going to finish with a very simple
question? Why do we use recursion? The question is short and simple, but the
answer is hardly easy.
Let us start with execution speed. You may suspect that recursion executes faster
than iteration. Speed is always desirable and that would certainly make recursion
popular if more speed is gained. Do you think that recursion is faster than
iteration? Sorry ... good guess, but recursion is actually slower than iteration.
The reason is that calling functions to execute a series of program statements
takes longer because it makes a jump away from the current memory sequence.
The time penalty for calling a function is extremely slight, but a large number of
function calls can make a difference. A function like the BubbleSort, which you
saw in the last section, has a very very large number of function calls. Remember
the Fibonacci Sequence problem. The excessive number of recursive calls slowed
the execution speed down tremendously.
Well if speed is not the reason for recursion then what might it be? How about
memory? All these recursive functions seem to be quite short, and certainly
shorter than the iterative function. Yes, it must be memory. Bad news again my
good friends. Recursion uses more memory. The smaller code of the recursive
function may occupy less space than code for an iterative function, but there are
other factors to consider. Our friendly stack stores local function information and
controls the execution sequence of function calls. Lots of recursive calls can
result in lots of unfinished business, which requires lots of memory to store until
the program execution returns to clean up.
You are now officially stumped. Two of the best reasons for doing something
different in a program, speed and memory have been eliminated from contention.
You do remember that program development is a compromise between speed,
memory and readability.
Can it be that recursion is more readable? This one is a little tricky. There are
plenty of people who will claim that recursion is natural and thus more readable.
There is a good argument to be made, but there are also plenty of students, new to
recursion, who will need some more exposure before all this recursive business is
natural to them. And whether or not recursion is readable to you does not matter
because it is not the reason for using recursion. You did have a preview of the
reason for using recursion when you looked at the Tower of Hanoi problem.
That is it? Yes my good students that is precisely the reason. You may think that
iteration is simpler than recursion, and it may very well be that way for functions
like Skip, GCF, Factorial and other small functions. Those function examples
worked well to demonstrate recursive techniques. In reality there is little need to
use a recursive GCF function or any of the others early function examples.
But think about the Tower of Hanoi. You were not shown how to do that problem
iteratively and the recursion did seem pretty tricky. Yes, I agree, that problem
was certainly tougher than the Factorial function. The point is that it is easier
recursively than it is iteratively. Try it. See if you can create an iterative solution
for the Tower of Hanoi problem.
At the beginning of this chapter you were challenged to solve a special problem in
an iterative manner. You may or may not have tried to solve this problem, but the
Grid problem is shown in this chapter as a classic example of a problem that is so
much easier to do recursively than iteratively. Did you forget the problem? Do
not even go back to the beginning. The problem is repeated below.
An image can be represented as a grid of black and white cells. Two cells in an
image are part of the same object if each is black and there is a sequence of moves
from one cell to the other, where each move is either horizontal or vertical to an
adjacent black cell. For example, the diagram below represents an image that
contains two objects, one of them consisting of a single cell. Black cells are
depicted with a small black diamond.
Write function AlterGrid with the following header such that all cells in the same
object as Grid[Row,Col] are set to White; otherwise Grid is unchanged.
The solution to this problem is slightly altered from the orginal specifications. It
is a little tricky to create black and white squares in a grid. However, the spirit of
the problem can be simulated with text characters, using Xs and Os.
I will leave you to ponder over the solution of the problem. It is recursive. It is
certainly elegant. Is this elegance starting to make sense? You may need to do
some tracing to see what is happening. But it is now your show. You have a
program to work with, and you have the solution. This will be a good way to
finish this section by carefully digesting an excellent example of a problem that
was meant to be done recursively. The program may seem long and complex, but
actual recursive Alter function is quite short and shown at the end of the program.
// PROG3718.CPP
// Recursive Grid Problem Solution
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include "APMATRIX.H"
There are four statements that make recursive calls. Think why that is necessary
and what it does to the program’s execution. The program output with one
sample execution is shown on the next page.
PROG3718.CPP OUTPUT
X O X X O X X X X X
X X O O O O X O X X
O X X O X O O X O O
O X O X O X O O X X
O X X X X X X O O X
O X X O O O X X X X
X X O X X O X X X O
X X X X X O O X O O
O X X O O O O O O O
O O X O X X X X O O
O O X X O X X X X X
O O O O O O X O X X
O O O O X O O X O O
O O O O O O O O O O
O O O O O O O O O O
O O O O O O O O O O
O O O O O O O O O O
O O O O O O O O O O
O O O O O O O O O O
O O O O X X X X O O
It is not always the case that programmers must select between a recursive
solution or an iterative solution. There are times when it makes the most sense to
combine both methods. Keep in mind that recursion is slower, and recursion uses
more memory than iteration. This means it is wise to use iteration whenever it is
possible to do so without creating unnecessary complexity. One such example of
combining iteration with recursion is the Merge Sort.
In the first step of our case study we need to investigate the concept of merging
and understand why merging is beneficial to a sorting algorithm. Imagine that
two sorted lists already exist, called L1 and L2. You wish to combine the two
lists and finish with a list called L3 that is sorted as well.
One approach is to place L1 after L2 in list L3, and then use a known sorting
algorithm. Such an approach works, but the selected sorting algorithm will
probably not take advantage of the fact that L1 and L2 are already sorted. It is far
more efficient to use a merging algorithm. The diagrams that follow below, and
on the next page, show the merging process.
List L1 22 34 35 39 50 72 75 90 92
List L2 17 18 29 46 61 82 85
22 34 35 39 50 72 75 90 92
17 18 29 46 61 82 85
The drawing above shows List1 in the higher position and List2 in the lower
position. The drawing below shows the completed merging process.
17 18 22 29 34 35 39 46 50 61 72 75 82 85 90 92
// PROG3719.CPP
// Merge Case Study, Step 1
// Stub program for the MergeSort case study
void main()
{
IntList List1,List2,List3;
Initialize(List1,List2);
Merge(List1,List2,List3);
Display(List1,List2,List3);
}
PROG3719.CPP OUTPUT
Program PROG3719.CPP was not very satisfying. It did not even have any kind
of output. Not to worry, because the next program cures that problem. The next
example will take two sorted lists and merge them into one longer, sorted list.
The biggest concern with step 2 is to understand the merging process. The
Initialize function creates one list of odd integers and a second list of even
integers.
The Display function displays the contents of every list. The important stage here
is to focus on function Merge. In the next program, Merge is presented as a stub.
Check the program out to see the general flow of information, look at the program
output and then move on to see the merging logic.
// PROG3720.CPP
// Merge Case Study, Step 2
// This program demonstrates merges two sorted arrays.
#include <iostream.h>
#include <conio.h>
#include "APVECTOR.H"
void main()
{
IntList List1(100),List2(100),List3(100);
Initialize(List1,List2);
Merge(List1,List2,List3);
Display(List1,List2,List3);
}
PROG3720.CPP OUTPUT
10 12 14 16 18 20 22 24 26 28
11 13 15 17 19 21 23 25 27 29
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
28 29
The /*1*/ while loop is the work horse of the merging process. The first element
of L1 and L2 are compared and the smaller of the two elements is placed in L3.
The /*2*/ while loop merges any remaining L1 elements into L3. It is no longer
necessary to compare L1 with L2 since either L1 or L2 has been completely
transferred into L3. The /*2*/ while loop is skipped over when (I >= 10) is true,
meaning that L1 is completely merged into L3.
The /*3*/ while loop merges any remaining L2 elements into L3. The /*3*/
while loop is skipped over when (J >= 10) is true, meaning that every element of
L2 has been merged into L3.
The next stage in this case study may seem a little strange, but keep in mind that
we are slowly building up to an algorithm that will do more than merge. Our goal
is to write a function that will sort a set of random data. How then can merge be
used if there are not any sorted lists to merge? Stay tuned, we will develop this
process slowly and hopefully it will make sense when it is all over.
In the next program example there are a few significant changes. Function
Initialize places even integers in the first half of the array and odd integers in the
second half of the array. You will not see two separate arrays in this program.
The purpose of this program is to take two sorted halves of the same array and use
the merge process to end up with a completely sorted array.
Several important changes have been made in function Merge to make this
“single array” concept possible. First, a locally declared list, Temp, is used to
store the merged data. At the conclusion of the merge process, the data in Temp
will be transferred back to the Lst array. Second, a midpoint in the array is found
and the single array is really behaving like two arrays for the merge process by
logically treating each half of the Lst array as a separate array. From an actual
merging point of view, the second example is the same as the first. Merging is
merging, but the new feature introduced here is the idea of treating one array as if
it consisted of two separate arrays.
// PROG3721.CPP
// Merge Case Study, Step 3
// This program merges two sorted halves of one array
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include "APVECTOR.H"
PROG3721.CPP OUTPUT
10 12 14 16 18 20 22 24 11 13 15 17 19 21 23 25
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
In Step 3 we deliberately merged the first half of the array with the second half of
the array. This process was relatively simple and allowed us to investigate how
merging is possible in a single array. In Step 4 we will continue merging in one
array, but now we will designate one particular section of an array as requiring
merging.
The array section will be identified by using the variable First as the index where
the partial array starts, and Last as the index where the partial array ends. The
variable Mid indicates the Midpoint index between the First index and the Last
index. The importance of merging only part of an array will be explained in the
next section. It is the last programming tool that needs to be achieved before we
can get really serious, and truly start doing some honest sorting.
It is ironic that computer science makes such a big deal about solving problems
(and programs) in a top-down manner, yet it is frequently the case that the
introduction of a new topic is managed easier if it is handled with a bottom-up,
from specific to general, learning approach.
// PROG3722.CPP
// Merge Case Study, Step 4
// This program demonstrates how to merge part of one
array.
#include <iostream.h>
#include <conio.h>
#include "APVECTOR.H"
void main()
{
IntList List(100);
int N = 16;
Initialize(List,N);
Display(List,N);
int Mid = N / 2;
int First = Mid - 4;
int Last = Mid + 3;
Merge(List,First,Mid,Last);
Display(List,N);
}
PROG3722.CPP OUTPUT
10 12 14 16 18 20 22 24 11 13 15 17 19 21 23 25
10 12 14 16 11 13 15 17 18 20 22 24 19 21 23 25
Later in this case study you will see the full-blown, ready-to-get-to-work Merge
Sort, but first you need to understand how the algorithm works. Long, long ago
you were introduced to the Bubble Sort. Understanding the logic of the Bubble
Sort was based on the Swap function. Given that you have the ability to
exchange array elements with some magical Swap function, the logic of the
Bubble sort was developed.
A similar approach will be use with the Merge Sort. We used three steps to look
at just merging array elements. Hopefully, merging makes sense at this stage.
We will now move on and assume that a merge function is available whenever it
is needed for our sort routine.
Let us start by actually sorting an array of eight numbers without looking at any
program code. All we want to see are pictures and the process by which the
numbers become sorted.
We start with a list of eight, unsorted, numbers, shown in figure 1, and then
proceed to manipulate these numbers by some logical fashion until the list is
sorted. If we can discover a method for eight numbers there may be a good
chance that it will work for larger arrays as well.
Figure 1
First we need to split the array into two parts and check to see if each half of the
array can be merged into one larger array. We know that sorting is possible if we
have access to two lists that are sorted already.
Figure 2
Figure 2 shows how the array splits into two lovely halves. We have a merge
function available, but correct merging requires that the lists to be merged are
already sorted. All we have right now are two smaller arrays of four elements,
and each smaller array is still unsorted. Merging at this stage will only rearrange
the array in some useless, unsorted, fashion. How about splitting each one of the
smaller arrays? Perhaps that will help.
Figure 3
Well this is just terrific. Figure 3 has four smaller arrays, and surprise, each one
of the arrays is as unsorted as when we started. We do not have one big problem
now, we have four little problems. We are doing very little merging, but we are
sure splitting very well. Hang on, and just for fun humor us and split one more
time. Maybe, just maybe, something useful will happen.
Figure 4
Figure 5
Figure 6
Some serious celebration can start right about now. It appears that Figure 6
shows two sorted lists. We are only one merge process away from having the
whole works sorted. What you see here is custom ordered for our Merge
function. We have two lists, and they are both sorted.
Figure 7
Success in Figure 7. We performed three merge passes and the whole list is
sorted. Let us now try to summarize what was actually done in English sentences,
before we attempt to write C++ code. After we have some logical steps
sequenced in easy to understand, English sentences, we will have an easier time to
see about changing the English C++ programming source code.
The beauty of the Merge Sort algorithm is the way that recursion works. All
unfinished business sits patiently on the stack waiting to jump into action. This
feature is perfect for our Merge Sort.
Merging two lists is useless unless the two lists are sorted. For this reason two
recursive calls are made first to sort the two halves of the list. This recursive
process continues until a single element is left in the list, just like the examples,
shown with Figure 1 through Figure 7. Merging can now proceed. It starts
slowly by merging single elements, but rapidly the list grows until the whole list
is sorted.
We can also see an excellent example of mixing recursion with iteration. The
Merge function is strictly iterative and happily merges any sorted data that
comes its way. The overall MergeSort is recursive in nature and allows
continues recursive calls until the list is totally broken up into single elements.
The exit or base case of the recursive function requires recognition that the list
cannot be broken up any further. The array that is used to store the number list
does not have any parameter that keeps track of the size of the list. Somehow, we
need to know when the list is reduced to a single element.
We can borrow some logic from the Binary Search. With the Binary Search you
use a High and a Low. At the start of the search Low = 0 and High = N-1, with
N equal to the number of elements in the array. As the search progresses, High
and Low are altered depending on the comparisons made with the search item. In
the event that the search item does not exist, High will actually become smaller
than Low. When that particular condition is true (High < Low), it is time to quit
because all possibilities have been exhausted.
We will use the same logic with the MergeSort. In this case we will use First
and Last. At the first function call to MergeSort , First = 0, and Last = N-1. As
the array gets broken down with each recursive call, the values of First and Last
are altered. Eventually, Last becomes smaller than First, and when that happens
the array has been broken down as far as possible and the recursive process has
reached its base case. At that point the merging process can now get started.
We are now ready to code the actual MergeSort function. Keep in mind that
function Merge already exists. Shortly, in the final step, an entire program will
be shown that ties all the pieces together. Right now let us focus on the
MergeSort coded with the assumption of an existing Merge function.
It is hard to believe that the short MergeSort function can actually sort anything,
and it does the sorting very efficiently. The MergeSort is recognized as one of
the superior sorts. Keep in mind that another, and longer Merge, function is part
of the overall sorting process.
In line 1 the function checks to see if the base case has been reached. Once Last
is no longer greater than First, recursion stops.
Line 2 computes the midpoint index of the current array, which is specified by
the index of First and Last. This midpoint will be used to make two recursive
calls with two smaller arrays.
Line 3 calls the MergeSort function recursively with altered parameters that use
the first half of the current list. Essentially, this splits the list in two.
Line 4 calls the MergeSort function recursively with altered parameters that use
the second half of the current list.
Line 5 takes the two halves, which were sorted by the two previous recursive
calls, and merges them iteratively with our handy Merge function.
Program PROG3723.CPP shows a complete program that demonstrates the
MergeSort function. This program will generate random numbers, display the
random numbers, and then sort them. The Display function will be called a
second time to verify that the list has been sorted.
// PROG3723.CPP
// Merge Case Study, Step 5
// This program demonstrates the complete MergeSort
program
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include "APVECTOR.H"
void main()
{
IntList List(1000);
int N;
Initialize(List,N);
Display(List,N);
MergeSort(List,0,N-1);
Display(List,N);
}
PROG3723.CPP OUTPUT
109 103 401 129 420 295 583 276 730 954 347 499
198 728 607 137 248 833 716 787 844 963 297 484
957 855 930 829 505 644 695 639 594 748 202 465
209 704 527 541 607 410 881 337 261 480 725 247
584 680 661 102 808 342 514 447 439 623 643 351
253 824 365 400 467 316 472 148 544 985 101 468
162 166 328 977 419 464 277 277 290 324 782 900
914 762 515 578 131 217 512 535 637 327 203 731
684 897 887 138
101 102 103 109 129 131 137 138 148 162 166 198
202 203 209 217 247 248 253 261 276 277 277 290
Function Initialize generates the random integers and stores the integers in the
two arrays. List1 will be displayed in a column on the left side, and List2 will be
displayed in a column on the right side. Function LeftColumn and function
RightColumn display the integer values in the appropriate column and call each
other with mutual recursion.
Note that function prototypes are used. Since the early introduction of functions
you have been advised to use function prototypes. There have been many
situations where function prototypes were not required. Nevertheless you have
learned that this is an established convention that you should follow. In this case
it will be required to use function prototypes. Function RightColumn is declared
after function LeftColumn. Function LeftColumn includes a call to function
RightColumn, and this call is only possible because the program uses prototypes.
// PROG3724.CPP
// This program demonstrates mutual recursion.
void main()
{
int Count;
clrscr();
IntList List1(100),List2(100);
Initialize(Count,List1,List2);
LeftColumn(0,Count,List1,List2);
getch();
}
PROG3724.CPP OUTPUT
109 10
401 12
420 29
583 27
730 95
347 49
198 72
607 13
248 83
716 78
The previous example works fine to demonstrate mutual recursion, but the
purpose of the program example was somewhat contrived. You can probably
accept the situation where two arrays of numbers need to be displayed in separate
columns. But there will certainly be situations where one array has more
elements than the other array.
The interesting thing in this program is that there are different exits. First, there is
an exit that stops making mutual recursive calls. At that point self recursion
continues in one of the function. The function that continues with self recursion
now will need a second exist to stop the self recursive calls.
Study the next program example carefully and make sure that you can follow the
program execution as it goes through the initial stage of mutual recursion and then
later switches to self recursion.
// PROG3725.CPP
// This program demonstrates mutual and self recursion.
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include "APVECTOR.H"
void main()
{
int Count1,Count2;
clrscr();
IntList List1(100),List2(100);
Initialize(Count1,Count2,List1,List2);
LeftColumn(0,Count1,Count2,List1,List2);
getch();
PROG3725.CPP OUTPUT
109 34
103 49
401 19
129 72
420 60
295
583
276
730
954