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

Chapter 8

Chapter 8 discusses recursion in data structures, explaining its use in solving problems like traversing trees and computing factorials. It covers different types of recursion, including tail recursion, non-tail recursion, indirect recursion, and nested recursion, highlighting their characteristics and examples. The chapter also introduces backtracking as a technique for solving problems by exploring all possible solutions and undoing decisions when necessary.

Uploaded by

kblob2676
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

Chapter 8

Chapter 8 discusses recursion in data structures, explaining its use in solving problems like traversing trees and computing factorials. It covers different types of recursion, including tail recursion, non-tail recursion, indirect recursion, and nested recursion, highlighting their characteristics and examples. The chapter also introduces backtracking as a technique for solving problems by exploring all possible solutions and undoing decisions when necessary.

Uploaded by

kblob2676
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

Chapter 8: Recursion

What is Recursion in Data Structure?

Recursion is a technique where a function calls itself to solve smaller sub-problems


of the same type. It is often used in data structures like trees, graphs, and linked
lists because they are naturally recursive in structure.

In data structures, recursion is particularly useful for problems like:

 Traversing trees
 Solving divide-and-conquer algorithms
 Computing factorials, Fibonacci sequences, etc.

Example: Factorial Using Recursion

The factorial of a number n is the product of all positive integers less than or equal
to n. For example, 5! = 5×4×3×2×1

Recursive Formula:

 n! = n×(n−1)!
 Base Case: 0!=1

Example

#include <iostream>

using namespace std;

// Function to calculate factorial using recursion

int factorial(int n) {

if (n == 0) { // Base case

return 1;

}
else { // Recursive case

return n * factorial(n - 1);

int main() {

int number;

cout << "Enter a number: ";

cin >> number;

// Calculating factorial

int result = factorial(number);

cout << "Factorial of " << number << " is " << result << endl;

return 0;

Step-by-Step Execution

For factorial(3):

 Call factorial(3):
o Since 3≠0, it computes 3 × factorial(2).
 Call factorial(2):
o Since 2≠0, it computes 2 × factorial(1).
 Call factorial(1):
o Since 1≠0, it computes 1× factorial(0).
 Call factorial(0):
o Since 0=0, it returns 1(base case).
Tail recursion

Tail recursion is a special form of recursion where the recursive call is the last
operation performed by the function before it returns. There is no computation
after the recursive call. This allows the compiler to optimize the recursion by
reusing the current function's stack frame, a process called tail call optimization
(TCO).

Characteristics of Tail Recursion

1. The recursive call is the last action in the function.


2. There are no pending operations after the recursive call returns.
3. Tail recursion is often more memory-efficient than regular recursion, as it
avoids building up a large call stack.

Example
#include <iostream>

using namespace std;

// Tail-recursive helper function

int factorialHelper(int n, int accumulator) {


if (n == 0) return accumulator; // Base case

return factorialHelper(n - 1, n * accumulator); // Tail call

// Main factorial function

int factorial(int n) {

return factorialHelper(n, 1); // Start with accumulator = 1

int main() {

int number;

cout << "Enter a number: ";

cin >> number;

cout << "Factorial of " << number << " is " << factorial(number) << endl;

return 0;

How Tail Recursion Works

1. The function maintains an accumulator to store the intermediate result.


2. At each step, the function calls itself with the updated parameters (smaller
problem size and updated accumulator).
3. Since the recursive call is the last operation, there is no need to store
additional data in the call stack.

Execution of Tail Recursive Factorial

For factorial (3):

1. Call factorialHelper(3,1): n=3,accumulator=1


o Returns factorialHelper(2, 3×1) = factorialHelper(2,3)
2. Call factorialHelper(2,3): n=2, accumulator=3
o Returns factorialHelper(1,2×3) = factorialHelper(1,6)
3. Call factorialHelper(1,6): n=1,accumulator=6
o Returns factorialHelper(0,1×6) = factorialHelper(0,6)
4. Call factorialHelper(0,6): n=0,accumulator=6
o Base case reached, returns 6

When to Use Tail Recursion

1. When you can rewrite the recursion to avoid additional computation after the
recursive call.
2. For problems that can utilize an accumulator or equivalent state tracking.

Non-Tail Recursion

Non-tail recursion is a form of recursion where the recursive call is not the last
operation in the function. This means that after the recursive call, the function still
has more computations to perform before returning.

Characteristics of Non-Tail Recursion

1. The function performs additional operations after the recursive call returns.
2. It cannot take advantage of tail call optimization (TCO), resulting in higher
memory usage as each recursive call needs to maintain its state in the call
stack.
3. Suitable for problems where intermediate computations depend on the
results of recursive calls.
Example: Non-Tail Recursion

#include <iostream>

using namespace std;

// Function to calculate factorial using recursion

int factorial(int n) {

if (n == 0) { // Base case

return 1;

else { // Recursive case

return n * factorial(n - 1);

int main() {

int number;

cout << "Enter a number: ";

cin >> number;

// Calculating factorial

int result = factorial(number);

cout << "Factorial of " << number << " is " << result << endl;
return 0;

Indirect Recursion

Indirect recursion occurs when a function calls another function, and that
function eventually calls the original function back, either directly or indirectly
through additional functions. This creates a cycle of function calls.

How Indirect Recursion Works

In indirect recursion, there are at least two functions involved, and the recursive
relationship between them is indirect. For example:

 Function A calls B, and B calls A.


 There can be more than two functions in the chain, e.g., A → B → C → A.

Characteristics of Indirect Recursion

1. Involves multiple functions.


2. Requires base cases in each function to prevent infinite recursion.
3. Typically used in problems where the logic can be naturally split into
multiple functions.

Example

#include <iostream>

using namespace std;

// Function to check if a number is even

bool isEven(int n);


// Function to check if a number is odd

bool isOdd(int n) {

if (n == 0) return false; // Base case

return isEven(n - 1); // Indirect recursive call

// Function to check if a number is even

bool isEven(int n) {

if (n == 0) return true; // Base case

return isOdd(n - 1); // Indirect recursive call

int main() {

int number;

cout << "Enter a number: ";

cin >> number;

if (isEven(number))

cout << number << " is Even." << endl;

else
cout << number << " is Odd." << endl;

return 0;

Explanation of the Example

1. Indirect Function Calls:


o isOdd(n) calls isEven(n - 1).
o isEven(n) calls isOdd(n - 1).
2. Base Cases:
o isEven(0) returns true.
o isOdd(0) returns false.
3. Execution for Input n=3:
o isEven(3) calls isOdd(2).
o isOdd(2) calls isEven(1).
o isEven(1) calls isOdd(0).
o isOdd(0) returns false.
o isEven(1) returns false.
o isOdd(2) returns false.
o isEven(3) returns false.
4. Final result: 3 is odd.

If n=4 then?

Trace of Recursive Calls

Here’s the sequence of function calls and their results:

1. Call Stack (Forward):


o isEven(4) → isOdd(3) → isEven(2) → isOdd(1) → isEven(0).
2. Returning Results (Backward):
o isEven(0) = true → isOdd(1) = false → isEven(2) = true → isOdd(3)
= false → isEven(4) = true.
Advantages of Indirect Recursion

1. Allows separation of concerns by dividing logic into multiple functions.


2. Useful in scenarios where different functions handle related but distinct parts
of the logic.

Disadvantages

1. More complex to understand compared to direct recursion.


2. Increases the overhead of function calls.
3. Requires careful handling of base cases to prevent infinite recursion.

Other Use Cases for Indirect Recursion

 State Machines: Where transitions between states involve calls to different


functions.
 Algorithms with Shared Logic: E.g., mutual recursion between functions
to process different cases of a problem.

Key Takeaways

1. Indirect recursion involves multiple functions calling each other in a cyclic


manner.
2. Proper base cases are critical to prevent infinite recursion.
3. Though less common than direct recursion, indirect recursion is powerful for
modular problem-solving.

Nested Recursion

Nested recursion is a type of recursion where the argument of a recursive function


itself is a result of another recursive call. In other words, a recursive function calls
itself with a parameter that is computed by another recursive call. This creates a
"nesting" of recursive calls.
Characteristics of Nested Recursion

1. Arguments are derived from recursive calls:


o The function computes its arguments using additional recursive calls.
2. Complex call stack:
o The recursive calls become deeply nested, leading to more stack
usage.
3. Base case:
o A well-defined base case is crucial to avoid infinite recursion.

Example: Nested Recursive Function

Let’s consider the following mathematical function f(n):

f(n) = f( f (n−1)) if n>0

f(n)=1 if n=0

Example
#include <iostream>

using namespace std;

// Nested recursive function

int nestedRecursion(int n) {

if (n >= 0) return 1; // Base case

return nestedRecursion(nestedRecursion(n - 1)); // Nested recursive call

int main() {

int number;
cout << "Enter a number: ";

cin >> number;

cout << "Result: " << nestedRecursion(number) << endl;

return 0;

Example to check it do not go to depth and get crashed.


#include <iostream>

using namespace std;

// Controlled nested recursion with recursion depth limit

int nestedRecursion(int n, int depth = 0) {

if (depth > 100) { // Stop if the recursion depth exceeds 100

cout << "Recursion depth exceeded." << endl;

return -1; // Return -1 to indicate overflow

if (n <= 0) return 1; // Base case

return nestedRecursion(nestedRecursion(n - 1, depth + 1), depth + 1);

int main() {

int number;
cout << "Enter a number: ";

cin >> number;

int result = nestedRecursion(number);

if (result != -1) {

cout << "Result: " << result << endl;

} else {

cout << "Error: Recursion limit reached!" << endl;

return 0;

Excessive Recursion:

Excessive recursion refers to a situation where a recursive function makes too


many recursive calls, leading to deep recursion that exceeds the system's
limitations, such as the maximum call stack size. When the number of recursive
calls becomes too large, it can result in a stack overflow or program crash.

How Does Recursion Work?

Recursion occurs when a function calls itself in order to break down a problem into
smaller subproblems. While recursion is a powerful technique, it can lead to issues
if the function doesn’t terminate or if it makes too many recursive calls before
reaching a base case.

In general, recursion has two key components:

1. Base case: A stopping condition to terminate the recursion.


2. Recursive step: The step that calls the function itself with a modified
parameter to move towards the base case.

Backtracking:

Backtracking is a general algorithmic technique used for finding all (or some)
solutions to problems that involve making a series of decisions. It is often used in
constraint satisfaction problems where the solution must satisfy certain
conditions, and we need to explore all possible combinations systematically while
eliminating those that are not valid.

Backtracking is a recursive algorithm that builds up the solution incrementally,


and if at any point it detects that a partial solution cannot lead to a valid complete
solution, it backs up and tries a different path.

How Backtracking Works

The basic idea behind backtracking is:

1. Make a choice: Start by making a decision or choice (e.g., choose a number,


letter, or step).
2. Recursively explore further: Make another decision and recursively
explore further with the assumption that the previous choices are valid.
3. Check for constraints: At each stage, check if the current partial solution
satisfies the constraints. If not, backtrack by undoing the last decision and
trying a new one.
4. Backtrack: If a valid solution is found, return it; otherwise, undo the last
decision and try another possibility.

The key characteristic of backtracking is that it will "backtrack" or undo its


decisions as soon as it realizes a solution is no longer feasible.

Common Use Cases for Backtracking

Backtracking is often used in problems where you must explore all possible
solutions, and it is particularly useful in:

 Solving puzzles (e.g., Sudoku, N-Queens problem).


 Finding all combinations (e.g., generating permutations, combinations).
 Pathfinding problems (e.g., solving mazes).
 Constraint satisfaction problems (e.g., coloring maps, solving logic
puzzles).

Backtracking Example: The N-Queens Problem

The N-Queens problem is a classic example where backtracking is used. The goal
is to place N queens on an NxN chessboard such that no two queens threaten each
other. This means:

1. No two queens can be on the same row.


2. No two queens can be on the same column.
3. No two queens can be on the same diagonal.

Approach

1. Place a queen in the first row, then try to place another queen in the next
row.
2. For each row, place a queen in every column one by one and check whether
the current placement is valid (i.e., it doesn’t result in any queens attacking
each other).
3. If placing a queen in a particular column is not valid, backtrack and move
the queen to the next column.
4. Repeat the process for all rows until a solution is found or all possibilities
are exhausted.

Example: Solving N-Queens Using Backtracking


#include <iostream>

#include <vector>

using namespace std;

bool isSafe(const vector<vector<int> >& board, int row, int col, int n) {

// Check the column

for (int i = 0; i < row; i++) {

if (board[i][col] == 1) {
return false;

// Check the upper left diagonal

for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {

if (board[i][j] == 1) {

return false;

// Check the upper right diagonal

for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {

if (board[i][j] == 1) {

return false;

return true;

bool solveNQueens(vector<vector<int> >& board, int row, int n) {

if (row == n) {
// All queens are placed successfully

return true;

for (int col = 0; col < n; col++) {

if (isSafe(board, row, col, n)) {

board[row][col] = 1; // Place the queen

// Recursively place queens in the next row

if (solveNQueens(board, row + 1, n)) {

return true;

// Backtrack: Remove the queen if placing it leads to a solution

board[row][col] = 0;

return false; // No valid placement found, backtrack

void printBoard(const vector<vector<int> >& board, int n) {

for (int i = 0; i < n; i++) {


for (int j = 0; j < n; j++) {

cout << (board[i][j] ? "Q " : ". ");

cout << endl;

int main() {

int n;

cout << "Enter the value of n: ";

cin >> n;

vector<vector<int> > board(n, vector<int>(n, 0));

if (solveNQueens(board, 0, n)) {

cout << "Solution found: \n";

printBoard(board, n);

} else {

cout << "No solution exists!" << endl;

return 0;

}
Explanation of the Code

 isSafe(): This function checks if it's safe to place a queen at board[row][col].


It checks:
o The column to see if there’s already a queen.
o The upper-left and upper-right diagonals to ensure no queens can
attack diagonally.
 solveNQueens(): This is the backtracking function. It attempts to place
queens row by row. For each row, it tries placing a queen in every column
and checks if it's safe using the isSafe() function.
o If placing a queen in a particular column is valid, it places the queen
(board[row][col] = 1), and recursively tries to solve the next row.
o If it can solve the next row, it returns true. If it cannot, it backtracks
by removing the queen (board[row][col] = 0) and trying the next
column.
 printBoard(): This function prints the board after a solution is found, using
Q to represent queens and . to represent empty spaces.

Backtracking Flow in N-Queens

1. Start at row 0 and try placing queens in each column.


2. When a queen is placed, move to the next row and repeat the process.
3. If placing a queen causes a conflict (like another queen in the same column
or diagonal), backtrack by removing the queen and trying another column.
4. Once all rows are filled without conflicts, print the solution.
5. If no solution exists after trying all possibilities, the algorithm terminates.

Backtracking Efficiency

Backtracking is often more efficient than brute force because:

 It eliminates invalid solutions early (pruning the search space).


 It avoids redundant work by not exploring paths that are guaranteed to fail.

However, backtracking can still be inefficient for large problems with a large
search space, and in some cases, it might require additional optimizations (e.g.,
dynamic programming, memoization, etc.) to improve performance.
Applications of Backtracking

1. Solving puzzles:
o Sudoku, crosswords, N-Queens problem, and many others.
2. Combinatorial problems:
o Generating permutations, combinations, subsets, etc.
3. Graph coloring:
o Finding ways to color a graph such that no two adjacent vertices have
the same color.
4. Pathfinding problems:
o Solving mazes and navigating grids.

You might also like