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.