0% found this document useful (0 votes)
515 views

Handout 4 - Recursion

The document describes recursion and how it is implemented using a run-time stack. Recursion involves defining a problem in terms of itself, with an anchor base case and inductive step that calls the function again. When a function is called recursively, an activation record is created and pushed onto the run-time stack to store arguments, return address, dynamic link, local variables, and return value. This allows multiple instances of recursively called functions to execute by maintaining separate activation records on the stack.

Uploaded by

api-3772959
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
515 views

Handout 4 - Recursion

The document describes recursion and how it is implemented using a run-time stack. Recursion involves defining a problem in terms of itself, with an anchor base case and inductive step that calls the function again. When a function is called recursively, an activation record is created and pushed onto the run-time stack to store arguments, return address, dynamic link, local variables, and return value. This allows multiple instances of recursively called functions to execute by maintaining separate activation records on the stack.

Uploaded by

api-3772959
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOC, PDF, TXT or read online on Scribd
You are on page 1/ 21

Mekelle University Faculty of Business & Economics

Computer Science Department

ICT241: Data Structures and Algorithms

Handout 4 – Recursion

Handout Overview

This handout describes what is meant by a recursive definition to a problem, and


how this translates into recursive C++ code. In particular, the way in which
recursion is handled using the run-time stack is explained. A number of different
types of recursion are highlighted: tail recursion, nontail recursion, indirect
recursion and nested recursion. The dangers of excessive recursion are pointed
out. Finally, the algorithmic concept of backtracking is described.

1. Recursive Definitions

Many programming languages, including C++, support the use of recursion to


solve problems. Recursion is the ability of a function to call itself. For many
problems, a solution that is defined in terms of itself seems a natural and intuitive
one. For example, the factorial function can be defined as follows:

1 if n=0 (anchor)
n! =
n∙(n – 1)! if n>0 (inductive step)

In mathematical terms, we can say that this recursive definition consists of two
parts: the anchor and the inductive step. The anchor is also sometimes called the
ground case, and is the case where we know how to compute the answer without
recursion. The inductive step is where we can only specify the answer by referring
to the function itself. In the factorial case, the inductive step states that the
factorial of n is equal to n multiplied by the factorial of n-1. So if we want to
compute the factorial of 3 using this definition, we first use the inductive step, 3!
= 3 * 2!. Next we need to find the result of the 2! term: again using the inductive
step we find that 2! = 2 * 1!. To compute the 1! term, we use the inductive step
again: 1! = 1 * 0!. Finally to calculate 0! we use the anchor, and get the result that
0! = 1. Therefore, the overall result of the calculation is that 3! = 3 * (2 * (1 * 1))
= 6.

As another example, consider the following recursive definition:

1 if n=0 (anchor)
g(n) =
2∙g(n - 1) if n>0 (inductive step)

1
This recursive definition can be converted into the following simple non-recursive
formula:

g(n) = 2n

In fact it is often the case that a recursive definition can be replaced with a non-
recursive one. Later in this handout we will look at how to choose the best
definition (i.e. recursive or non-recursive) for a particular situation.

Converting simple recursive definitions like those given above into C++ code is
often a trivial task. For example, the C++ equivalent of the factorial definition is:

int factorial (int n) {


if (n == 0)
return 1;
else
return n * factorial (n – 1);
}

This code may appear strange at first. How can a C++ function call itself? To
answer this question, it will be useful to examine exactly what happens when a
function is called.

2. Function Calls and Recursive Implementation

When a function is called, the operating system needs to store some information
about the function. First, the values of any arguments to the function need to be
remembered. Also, if the function has any local variables, then their values must
also be stored. If the function returns a value, then memory must be allocated to
hold this return value. Finally, after the function has finished executing, the
system needs to know something about the function that originally called this
function, so that execution can resume where it left off before the function call.

As an example, consider the code in Figure 1. This contains a simple program


with two functions, f1 and f2, in addition to the main function. To begin with,
the main function calls f1 with a single argument. Next, f1 calls f2 with two
arguments. Therefore, during program execution, there are three function calls in
total (the system calls the main function to start the program). Whenever a
function call is made we need to store information about the called function, as
described above. However, we also need to remember the information about the
calling function, and when the called function finishes execution we need to recall
this information. This remembering and recalling of information needs to be done
on a last-in/first-out basis, e.g. when f2 finishes execution we need to recall the
information for f1, and when f1 finishes execution we need to recall the
information for the main function. In other words, when a function finishes
execution we need to remember the most recently stored function information:
that of its calling function. We have already seen in Handout 3 that a stack
provides last-in/first-out data access, so this would seem the natural choice of data
structure for this situation.

2
//************** Figure 1.cpp *****************
// simple program to illustrate function calling

#include <iostream.h>

int f1 (int);
int f2 (int, int);

main () {
int x;
cout << “Enter n:\n”;
cin >> x;
cout << “Result = “ << f1(x) << endl;
}

int f1 (int n) {
int x;
x = f2 (n – 1, n – 2);
return x * 2;
}

int f2 (int a, int b) {


int x;
x = a * b / 2;
return x;
}

Figure 1 – A simple program to illustrate function calling

The run-time stack is a data structure that is used by the operating system to store
activation records, or stack frames. Activation records typically contain the
following information:
• Values for arguments to the function.
• The return address to resume control by the caller.
• A dynamic link, which is a pointer to the caller’s activation record.
• Values for all local variables in the function.
• The return value of the function, if it has one.
Each time a function is called, the system creates an activation record that
contains space to record all this information, assigns values for the first three, and
pushes the record onto the run-time stack. When a function finishes execution, the
dynamic link is used to restore the caller’s activation record, and the return
address is used to resume execution from the correct place. At all times there is a
stack pointer that points to the current activation record. Figure 2 illustrates the
contents of the run-time stack during execution of f2() in the above example.
When f2() finishes execution its activation record will be popped by moving the
stack pointer down to the activation record for f1(), as indicated by the dynamic
link in the activation record for f2().

3
Notice that in the code in Figure 1 there are three different variables called x.
Each of these should be considered by the compiler to be separate variables, as
their scope is local to the function they are declared in. This is achieved by the
fact that each instance of x is stored in a different activation record – the record of
its own function.

Arguments and local Stack


Activation variables Pointer
record for Dynamic link
f2() Return address
Return value
Arguments and local
Activation variables
record for Dynamic link
f1() Return address
Return value

Activation …
record for
main()

Figure 2 – Contents of the run-time stack when f2() is executing

How does the operating system use the run-time stack to implement recursion?
Actually recursive calls are not treated any differently from ordinary function
calls. When a recursive call is made an activation record is created and pushed
onto the run-time stack just like it is for any other function call. In this way,
multiple instances of a single function, each with its own activation record, can be
executed by the operating system.

Consider the factorial code given in Section above. Figure 3 shows the contents of
the run-time stack when the main function calls the factorial function with
an argument of 3. This first call of factorial results in an activation record
being created and pushed onto the stack just above the main function activation
record. Next, the first recursive call is made to factorial, with an argument of
2. This results in a second factorial activation record being created and
pushed onto to stack. In the execution of this call, the anchor condition (n=0) has
still not been met, so a second recursive call is made with argument 1, and a
corresponding activation record pushed onto the stack. Still the anchor condition
is not satisfied, so we make a third recursive call with argument 0, and another
activation record is pushed onto the stack. This time the anchor condition is true,
since the value of the argument is 0, so no recursive calls are made and the value 1
is assigned into the return value. Figure 3 shows the situation when this final call
is still executing, as the stack pointer is pointing to the top activation record. After
this final call returns the value 0, the stack pointer is moved down to the next
activation record. However, the top activation record is not yet deleted, as it

4
contains the return value that is needed by the calling function. Notice that the
return value is the last element of each activation record. This makes it easy for
the calling function to access its value, as it is directly above its own activation
record. After the factorial(1) call has finished executing, the return value is
recorded and the stack pointer moved down again. This process continues until we
reach the original calling function, main(), which can access the return value 6
from the factorial(3) call.

Arguments and local Stack


variables Pointer
Activation record for
factorial(0) Dynamic link
Return address
Return value
Arguments and local
variables
Activation record for
Dynamic link
factorial(1)
Return address
Return value
Arguments and local
variables
Activation record for Dynamic link
factorial(2)
Return address
Return value
Arguments and local
variables
Activation record for Dynamic link
factorial(3)
Return address
Return value

Activation record for


main() …

Figure 3 – The contents of the run-time stack for a recursive call

The factorial function can be implemented differently, using iteration instead of


recursion, as follows:

int iterativeFactorial (int n) {


int result = 1;
for (int i = 2; i <= n; i++)
result *= i;
return result;
}

5
Which implementation is best? In this case, both implementations are short and
easy to understand, but often recursive solutions result in shorter and clearer code.
However, the iterative solution is probably faster. Recursive implementations
usually involve more function calls than iterative implementations, and whenever
a function call is made it takes time to create and initialise the activation record
and push it onto the run-time stack. Therefore to decide which implementation is
best we need to know what is the priority: speed or clarity. If a recursive program
executes in 100ms, and an iterative program in 50ms, then this speed difference
will be indistinguishable to us, so program clarity will become more important.
But if the execution times are 2 hours and 1 hour respectively, then clearly the
iterative solution will be preferable.

3. Tail Recursion

All recursive functions contain a call to themselves. However, there are a number
of different types of recursive function, based on how many recursive calls there
are, and where in the function they occur.

The simplest type of recursion is called tail recursion. In tail recursion, there is a
single recursive call that occurs at the very end of the function implementation. In
other words, when the recursive call is made, there are no more statements to be
executed in the function. The single recursive call is the last thing to happen when
the function is executed. The recursive factorial function introduced in
Section is an example of tail recursion. The function tail() defined as

void tail (int n) {


if (n > 0)
cout << n << “, “;
tail (n – 1);
}
}

is another example, whereas the function nonTail() is not an example of tail


recursion:

void nonTail (int n) {


if (n > 0) {
nonTail (n – 1);
cout << n << “, “;
nonTail (n – 1);
}
}

The nonTail() function is not tail recursion for two reasons: there are two
recursive calls, whereas tail recursion features only a single recursive call; and
there are statements following a recursive call, whereas in tail recursion the
recursive call must be the final statement in the function.

6
The reason these restrictions are made on the definition of tail recursion is that if
they apply to a function it can easily be converted into a loop. For example, the
factorial() function was easily converted into the non-recursive
iterativeFactorial() function, and the tail() function can easily be
converted into the following iterative equivalent:

void iterativeTail (int n) {


for (int x = n; x > 0; x--)
cout << x << “, “;
}

Is there any advantage in using tail recursion over iteration? Usually, the answer is
no, although you should bear in mind the discussion of the trade-off between
program clarity and execution speed in Section . If you think that the iterative
version is just as easy to understand as the recursive version, then it is probably
better to use the iterative version. If you think that the recursive version is easier
to understand, then you need to consider whether execution speed will be an issue.

However, there are some programming languages, such as Prolog, that have no
explicit looping statement. Languages like Prolog rely on tail recursion as a way
to simulate iteration.

4. Nontail Recursion

We have just seen that if a function is using tail recursion it is straightforward to


convert it to an iterative equivalent. If a function uses nontail recursion, the
conversion is not so simple. Consider the following code to read a sequence of
characters and print them in reverse order:

void reverse () {
char ch;
cin.get(ch);
if (ch != ‘\n’) {
reverse();
cout.put(ch);
}
}

It may be difficult to appreciate how this code works at first. The function first
reads a single character from the keyboard using the get() member function of
istream. Next, so long as the character is not a newline character (the anchor
condition), a recursive call is made to reverse. Notice that the recursive call is
made before the current character is printed to the screen. This means that the first
call of the function will print all remaining characters in the input stream and then
print the first character typed. The first recursive call will read in the second
character typed, and print all remaining characters followed by the second
character, and so on until the anchor condition is reached. The result of this is that
the last character typed will be the first one print, the second-last character typed
will be the second one printed, and so on.

7
This function will be called once for each character in the input stream. For each
function call an activation record will be created and pushed onto the run-time
stack. Therefore if the input stream is long we will end up with many activation
records on the stack. Figure 4 shows the contents of the run-time stack if the input
stream consists of “AB\n”. Notice that there is no return value in the activation
record this time, since the reverse() function has a return type of void. The
figure shows the contents of the stack during the final recursive call of
reverse(), when the newline character has been typed, so the stack pointer
(SP) is pointing to the activation record where ch is equal to the newline
character..

ch = ‘\n’ SP
Dynamic Link
Return Address
ch = ‘B’ ch = ‘B’
SP
Dynamic Link Dynamic Link
Return Address Return Address
ch = ‘A’ ch = ‘A’
SP ch = ‘A’
Dynamic Link Dynamic Link Dynamic Link
Return Address Return Address Return Address

Figure 4 – The contents of the run-time stack for the reverse() function

How can the reverse() function be implemented non-recursively? Because


reverse() does not use tail recursion the conversion is not so straightforward.
Consider the following iterative version of reverse():

void iterativeReverse() {
char stack[80];
int top = 0;
cin.getline(stack, 80);
for (top = strlen(stack) - 1; top >= 0; top--)
cout.put(stack[top]);
}

This function is still quite short, but not as concise as the recursive
implementation. First it reads the input stream into the array stack, then prints
out each array element in turn, beginning with the last one typed. The choice of
variable name for the character array stack is not an accident. Actually we are
using this array as if it were a stack data structure: we store the characters in the
array in one order, and then access them in the reverse order. In fact, converting a
nontail recursive program into an iterative equivalent usually involves the explicit
handling of a stack. Often, program clarity can be diminished as a result.

8
5. Indirect Recursion

So far we have only looked at recursive cases where functions call themselves.
However, recursion can also happen indirectly. For example, the function f()
may call the function g(), and then g() may call f() again. This type of
recursion is known as indirect recursion.

The example given above is the simplest case of indirect recursion. Generally, we
can say that indirect recursion occurs when there is a chain of calls of the form:

f() -> f1() -> f2() -> ... -> fn() -> f()

In other words, f() calls f1(), which in turn calls f2(), and so on until fn()
calls f(). It is also possible that there may be more one than one such chain of
function calls that leads to indirect recursion. For example, in addition to the chain
given above the following chain may be possible:

f() -> g1() -> g2() -> ... -> gm() -> f()

An example of when indirect recursion might be necessary is in the calculation of


trigonometric functions. For small values, the approximation

x3
sin( x) ≈ x − .............................................................................
6
(1)

will hold, but for larger values of x, the following indirect recursive definitions
can be used:

x
3 − tan 2 ( )
x 3
sin( x) = sin( ) ⋅ .................................................................
3 1 + tan 2 ( ) x
3
(2)

sin( x)
tan( x) = .............................................................................
cos( x )
(3)

x
cos( x ) = 1 − sin( ) .............................................................................
2
(4)

Therefore if we wish to compute the sin of x, we first check if the absolute value
of x is smaller than a constant value e. If it is (the anchor condition), then we can
use the approximation in Equation (1). If it isn’t, we use the definition in Equation
(2). This requires a recursive call to the sin function, and also calls to the tan
function. The tan function is defined in terms of the sin and cos functions, so here

9
we need to make an indirect recursive call to sin. The recursive calls will continue
until the anchor condition is reached. Assuming that the value of x/3 is small
enough to use the approximation (i.e. no more recursive calls are necessary), we
can view this sequence of function calls as a tree, as illustrated in Figure 5.
With all recursion it is important to ensure that the anchor condition is reached at
some point. However, with indirect recursion, it is not always straightforward to
ensure that this is the case. If the anchor condition is never reached the program
will continue to execute infinitely until interrupted.

Figure 5 – A tree of indirect recursive call for computing sin(x)

6. Nested Recursion

We have now seen examples of a number of different types of recursion, all of


which feature functions defined in terms of themselves. A more complicated form
of recursion can be found in definitions in which a recursive reference is included
as one of the arguments to another recursive reference. Consider the following
definition:

0 if n = 0

h(n) = n if n > 4

h(2 + h(2n)) if 0 < n ≤ 4

In this definition, there is a recursive call in the case where n is greater than zero
but less than or equal to 4, but to compute the argument for this recursive call
involves another recursive call. This is known as nested recursion.

Another example of nested recursion is known as the Ackermann function, as it


was originally suggested by the mathematician Wilhelm Ackermann in 1928:

m+1 if n = 0

10
A(n, m) = A(n – 1, 1) if n > 0, m = 0

A(n – 1, A(n, m – 1)) otherwise

This is a good example of a function that is easily expressed in recursive form, but
it is troublesome to convert into an iterative equivalent.

7. Excessive Recursion

It was mentioned above that the advantage of using recursion is its simplicity and
readability. The disadvantage is the computational overheads involved in
manipulating the run-time stack. So long as the number of recursive calls is not
too large, recursion can be useful. However, we should be careful that we are sure
exactly how many recursive calls will be made.

Consider the example of computing Fibonacci numbers. The sequence of


Fibonacci numbers is defined as follows:

n if n < 2
Fib(n) =
Fib(n – 2) + Fib(n – 1) otherwise

This definition states that the first two numbers in the Fibonacci sequence are 0
and 1, and any subsequent Fibonacci number can be computed as the sum of its
two predecessors in the sequence. So the sequence is 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
and so on.

Implementing this definition in C++ is trivial:

int Fib (int n) {


if (n < 2)
return n;
else
return Fib(n – 2) + Fib(n – 1);
}

Since the Fibonacci definition is inherently recursive, it may initially seem to be


preferable to use this simple and elegant implementation. But let us look more
closely at what happens when it is executed. Suppose we wish to compute
Fib(6). The sequence of function calls is as follows:

Fib(6) = Fib(4) +
Fib(5)
= Fib(2) + Fib(3) +
Fib(5)
= Fib(0)+Fib(1) + Fib(3) +
Fib(5)
= Fib(0)+Fib(1) + Fib(1) + Fib(2) +
Fib(5)

11
= Fib(0)+Fib(1) + Fib(1)+Fib(0)+Fib(1) +
Fib(5)
etc.

We can already see from the beginning of the calculation process that there are
duplicate function calls: Fib(0) appears twice and Fib(1) appears three times,
and we haven’t even started to evaluate the Fib(5) term yet. This is very
inefficient. In fact, as the value of n grows, the number of function calls grows
very quickly. Table 1 shows a comparison of the number of assignments required
by the recursive implementation and an equivalent iterative implementation. Note
that for the recursive implementation the number of assignments id the same as
the number of function calls. In addition to the large number of assignments, we
need to consider the computational overheads of creating activation records and
pushing them onto the run-time stack. Overall, it seems as if the recursive version
can only really be used for small values of n.

Assignments
n
Iterative Algorithm Recursive Algorithm
6 15 25
10 27 177
15 42 1,973
20 57 21,891
25 72 242,785
30 87 2,692,537

Table 1 – Comparison of iterative and recursive implementations for


calculating Fibonacci numbers

The iterative algorithm executes in O(n). It is not straightforward to compute the


complexity of the recursive version, but a glance at Table 1 shows that it is much
more than O(n).

The recursive Fibonacci implementation is a good example of excessive recursion,


or making too many unnecessary recursive calls, thus reducing the efficiency of
the implementation.

8. Backtracking

When solving some problems, it is necessary to try a number of different ways


leading from a given position, none of them known to lead to a solution. You can
think of this as being like trying to find a path out of maze: if we reach a
crossroads, after trying one path unsuccessfully, we return to the crossroads and
try a different path. This process is known as backtracking, and is often used with
recursive solutions to problems. Backtracking is a common technique in games
programming. For example, to write a program to play chess against a human
opponent we need to make the computer choose the best move possible, so we try
each possible move, and even each possible human response to each possible

12
move. If a particular move leads to a bad situation for the computer, we backtrack
to the original board position and try another possibility.

A problem that illustrates the concept of backtracking quite well is the eight
queens problem. The eight queens problem attempts to place eight queens onto a
chessboard, such that no queen is attacking any other. According to the rules of
chess, a queen is attacking another piece if it is on the same row, the same column
or the same diagonal as that piece. To solve this problem, we try to put the first
queen on the board, then the second so that it doesn’t attack the first, then the third
so that it doesn’t attack the first or second, and so on. If we reach the situation
where it is impossible to place the next queen without attacking another queen,
then we backtrack to the previous queen and try a new position. If there are no
new positions that are untried, we backtrack to the previous queen and try a new
position for that. If this algorithm is followed, all possible solutions are guaranteed
to be found. This is known as an exhaustive search of the solution space (i.e. every
possible valid board position is tried).

Although this algorithm requires quite a lot of effort, a lot of which is spent
backtracking to previous positions, the actual algorithm is quite simple.
Pseudocode for this algorithm is as follows:

putQueen(row):
for every position col on the same row:
if position col is valid (i.e. no attacking pieces)
place the next queen at position col
if row < 8
putQueen(row + 1)
else
success ...
remove the queen from position col

This pseudocode states that to place a queen on a particular row, we place it in the
first valid position, where ‘valid’ means there are no other queens attacking that
square. If we place a queen and this is the last row of the board, then we have
found a solution, so we remember it and then backtrack. Otherwise, we make a
recursive call to place a queen on the next row. If there are no valid positions left
on this row, the function exits and we backtrack to try another possibility. After we
have made the recursive call to place a queen on the next row, then we remove the
queen and backtrack anyway.

Figure 6 illustrates the backtracking that leads to the first successful solution in a
simplified version of this problem, where the chessboard is 4x4 and we only need
to place 4 queens. We place the first queen at the top left on row 0, and then the
second queen at the first valid position on row 1, at column 2 (Figure 6a). There
are now no valid positions on row 2, so we need to backtrack and reposition the
second queen. The second queen is repositioned at column 3, then we place the
third queen at the first valid square on row 2, column 1 (Figure 6b). Now there are
no valid squares on row 3, so we backtrack. First we backtrack to the third queen,
but there are no valid squares left, so we backtrack again to the second queen.

13
Again, there are no valid squares left, so we backtrack to the first queen. The first
queen is repositioned to column 1. Next, the second queen is placed at the first
valid square on row 1, column 3. The third queen is placed at the first valid square
on row 2, at column 0. Finally the fourth queen is placed at the first valid square
on row 3, at column 2. Because we have now placed all 4 queens, we have found a
solution. Try stepping through the pseudocode given above using this example to
make sure you understand how it works.

Figure 7 shows an implementation of the eight queens problem in C++. This


implementation includes a special feature to improve the efficiency of the code. In
particular, when searching for a valid square to place a queen, we could search
every other square on the board to see if a queen is attacking each square.
However, if we did this for every potential positioning it would be very
inefficient. So we use the arrays leftDiagonal, rightDiagonal and
column to indicate whether particular diagonals or columns are currently free
from attack. For example, if leftDiagonal[3] is available this means that
the central top-right-to-bottom-left diagonal is free (i.e. any square where row +
column == 3). Whenever a queen is placed on the board, these three arrays are
updated so that they indicate which columns and diagonals are safe. Read through
the code, comparing the putQueen() function to the pseudocode given above,
to make sure you understand the implementation.

Figure 6 – The four queens problem: backtracking leading to a successful


solution

9. Recursion vs. Iteration

After considering all of these examples or different types of recursion, what can
we say about recursion as a programming tool? We have seen that it is almost
always possible to write an iterative equivalent for a recursive function, so which
approach is best?

There is no easy answer to this question. Recursive solutions are often clearer and
more concise than iterative ones, but they are usually slower than iterative ones,
although even this is not necessarily the case. Some computers may have
specialised hardware that make operations using the run-time stack very efficient.
If the iterative solution involves explicit manipulation of a stack then the resulting
compiled code may not be so efficient as it will not utilise the specialised
hardware. The only real way to find out which implementation is faster is to add

14
some timing code to the two implementations and compare the results. Also, with
recursive solutions we need to be careful to avoid implementations that involve
excessive recursion, as in the Fibonacci function.

In the end, it is up to you as a programmer to decide whether to use recursion or


iteration. You should decide which implementation is easiest to understand, which
is faster, and which of these two criteria are most important for the problem you
are trying to solve.

15
//******************* queens.cpp
********************
// program to solve the eight queens problem

#include <iostream>

class ChessBoard {
public:
ChessBoard(); // 8 x 8 chessboard;
ChessBoard(int); // n x n chessboard;
void findSolutions(); // solve problem

private:
const bool available;
const int squares, norm;
bool *column, *leftDiagonal, *rightDiagonal;
int *positionInRow, howMany;
void putQueen(int);
void printBoard(ostream&);
void initializeBoard();
};

ChessBoard::ChessBoard(): available(true),
squares(8),
norm(squares-1) {
initializeBoard();
}

ChessBoard::ChessBoard(int n): available(true),


squares(n),
norm(squares-1) {
initializeBoard();
}

void ChessBoard::initializeBoard() {
int i;
column = new bool[squares];
positionInRow = new int[squares];
leftDiagonal = new bool[squares*2 - 1];
rightDiagonal = new bool[squares*2 - 1];
for (i = 0; i < squares; i++)
positionInRow[i] = -1;
for (i = 0; i < squares; i++)
column[i] = available;
for (i = 0; i < squares*2 - 1; i++)
leftDiagonal[i] = rightDiagonal[i] =
available;
howMany = 0;
}

16
void ChessBoard::printBoard(ostream& out) {
for (int i = 0; i < squares; i++) {
cout << "+";
for (int j = 0; j < squares; j++)
cout << "-+";
cout << "\n|";
for (int j = 0; j < positionInRow[i]; j++)
cout << " |";
cout << "Q|";
for (int j=positionInRow[i]+1; j<squares;
j++)
cout << " |";
cout << "\n";
}
cout << "+";
for (int j = 0; j < squares; j++)
cout << "-+";
cout << endl << endl;
howMany++;
}

void ChessBoard::putQueen(int row) {


for (int col = 0; col < squares; col++)
if (column[col] == available &&
leftDiagonal [row+col] == available &&
rightDiagonal[row-col+norm] == available)
{
positionInRow[row] = col;
column[col] = !available;
leftDiagonal[row+col] = !available;
rightDiagonal[row-col+norm] = !available;
if (row < squares-1)
putQueen(row+1);
else printBoard(cout);
column[col] = available;
leftDiagonal[row+col] = available;
rightDiagonal[row-col+norm] = available;
}
}

void ChessBoard::findSolutions() {
putQueen(0);
cout << howMany << " solutions found.\n";
}

int main() {
ChessBoard board(4);

17
board.findSolutions();
return 0;
}

Figure 7 – An implementation of the eight queens problem

18
Summary of Key Points

The following points summarize the key concepts in this handout:


• A recursive definition is one that is defined in terms of itself.
• All recursive definitions consist of at least one anchor, and at least one
inductive step.
• The anchor is the termination condition of the recursive definition; it specifies
when no more recursive calls are necessary.
• The inductive step is the recursive part of the definition.
• When a function call is made in C++ (or any other language), an activation
record is created and pushed onto the run-time stack.
• The run-time stack contains information about any function that has not
completed execution; it typically contains local variable values, a return
address, a dynamic link and a return value for the function.
• Recursion is implemented by creating a new activation record for a function
each time a recursive call is made.
• Tail recursion is when there is a single recursive call, and it is the last
statement in the function.
• Functions that use tail recursion can easily be converted into iterative
equivalents.
• Nontail recursion is when there is more than one recursive call, or there are
statements after a recursive call.
• Converting nontail recursive functions into iterative equivalents usually
involves explicit handling of a stack data structure.
• In the simplest case of indirect recursion, a function f() calls a function g(),
which in turn calls f() again.
• In the general case, indirect recursion occurs when we have one or more
chains of function calls such as f()->f1()->f2()->...->fn()->f().
• Nested recursion is when a recursive call is required to compute an argument
of another recursive call.
• Excessive recursion is when many duplicated recursive calls are made.
• Backtracking is when an algorithm, usually a recursive one, tries a number of
different paths to a solution; if a path is unsuccessful, the algorithm backtracks
and tries another path.
• Recursive implementations are usually slower than iterative implementations,
because of the computational overheads involved in creating activation
records and pushing them onto the run-time stack.
• Recursive implementations are often more concise and easy to understand than
iterative ones.

19
Exercises

For the following exercises, any necessary source code can be found on the course
intranet page. Solutions will also be made available after classes for this chapter.

1) Write a recursive function GCD(n, m) that returns the greatest common divisor
of two integers n and m according to the following definition:

m if m ≤ n and n mod m = 0

GCD(n, m) = GCD(m, n) if n < m

GCD(m, n mod m) otherwise

2) Write a recursive function that calculates and returns the length of a singly linked
list. You can reuse the IntSLList class from Handout 2.
(Hint: you may find it easier if the function takes an IntSLLNode* as an
argument instead of an IntSLList.)

3) Write a function that recursively converts a string of digits into an integer value.
For example, convert(“1234”) would return the integer 1234.
(Hint: think about how each recursive call will know how much it’s digit is worth,
e.g. 1’s, 10’s, 100’s etc.)

4) Write a recursive function to add the first n terms of the series

1 - 1/2 + 1/3 - 1/4 + 1/5 - ...

5) Write a recursive function to check if a word is a palindrome.


(Hint: you may find it useful to use the built-in string class – see Cohoon &
Davidson, p813)

6) Write functions to implement sin, cos and tan using indirect recursion according
to the definitions in Equations (1)-(4). Assume that the approximation in Equation
(1) holds for all x ≤ 0.1 radians.

7) Write C++ code to compute the Ackermann function using nested recursion.

8) Write an iterative version of the recursive Fibonacci() function given in


Section .

Notes prepared by: FBE Computer Science Department.

20
Sources: Data Structures and Algorithms in C++, A. Drozdek, 2001

21

You might also like