handout 4 - recursion

29
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 1

Upload: api-3772959

Post on 10-Apr-2015

302 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Handout 4 - Recursion

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

Page 2: Handout 4 - Recursion

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

Page 3: Handout 4 - Recursion

//************** 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

Page 4: Handout 4 - Recursion

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 variables

Dynamic linkReturn addressReturn value

Arguments and local variables

Dynamic linkReturn addressReturn value

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

Activation record for f2()

Activation record for f1()

Activation record for main()

Stack Pointer

Page 5: Handout 4 - Recursion

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 variables

Dynamic linkReturn addressReturn value

Arguments and local variables

Dynamic linkReturn addressReturn value

Arguments and local variables

Dynamic linkReturn addressReturn value

Arguments and local variables

Dynamic linkReturn addressReturn value

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

Activation record for main()

Activation record for factorial(3)

Activation record for factorial(2)

Activation record for factorial(1)

Activation record for factorial(0)

Stack Pointer

Page 6: Handout 4 - Recursion

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 1 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

Page 7: Handout 4 - Recursion

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 2. 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

Page 8: Handout 4 - Recursion

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 = ‘A’Dynamic LinkReturn Address

ch = ‘B’Dynamic LinkReturn Address

ch = ‘A’Dynamic LinkReturn Address

ch = ‘\n’Dynamic LinkReturn Address

ch = ‘B’Dynamic LinkReturn Address

ch = ‘A’Dynamic LinkReturn 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

SP

SP

SP

Page 9: Handout 4 - Recursion

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

............................................................................. (1)

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

................................................................. (2)

............................................................................. (3)

............................................................................. (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 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.

9

Page 10: Handout 4 - Recursion

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

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

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

10

Page 11: Handout 4 - Recursion

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 < 2Fib(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)= 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

11

Page 12: Handout 4 - Recursion

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.

nAssignments

Iterative Algorithm Recursive Algorithm6 15 2510 27 17715 42 1,97320 57 21,89125 72 242,78530 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 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

12

Page 13: Handout 4 - Recursion

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 colif 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. 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.

13

Page 14: Handout 4 - Recursion

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 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.

14

Page 15: Handout 4 - Recursion

//******************* 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;}

15

Page 16: Handout 4 - Recursion

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); board.findSolutions(); return 0;}

Figure 7 – An implementation of the eight queens problem

16

Page 17: Handout 4 - Recursion

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.

17

Page 18: Handout 4 - Recursion

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 7.

Notes prepared by: FBE Computer Science Department.

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

18