DSA-Chapter-4.0-2024
DSA-Chapter-4.0-2024
Chapter 4: Recursion
Recursion is a programming technique where a function solves a problem by breaking it down into
smaller, more manageable sub-problems of the same type. Each of these sub-problems is then
solved using the same function, allowing for elegant and concise solutions to complex problems.
The key to successful recursion is to identify a base case, which is a problem that can be solved
directly without calling the function again, and to ensure that the function eventually reaches the
base case and returns a solution.
1. Base case - the condition that determines when the function should stop calling itself and
return a result, rather than making further recursive calls. Without a proper base case, a
recursive function could potentially call itself forever, leading to a stack overflow error
as the system's call stack runs out of space.
Note: When designing a recursive function, it is essential to ensure that each recursive step
brings the problem closer to the base case and that the base case is reached for all valid
inputs.
2. Recursion
The part of the function where the problem is broken down into smaller sub-problems, and
the function calls itself with these new sub-problems.
We can further break down the recursion into three steps:
1. Pre-processing (Optional*): Performing operation(s) to prepare for the recursive call.
(e.g., initializations, condition checks, etc.).
2. Recurse: Do the actual recursion. In this step, the function makes a recursive call to
itself, typically with a modified or smaller version of the original problem. This recursive
call allows the function to break down the original problem into smaller subproblems.
3. Post-processing (Optional*): Perform operations after the recursive call (e.g.,
performing calculations, combining results, aggregations) to solve the original problem.
*Optional depending on the specific requirements. Some algorithms may not require any
pre-processing and/or post-processing steps.
Example Problem: Calculate the sum of all integers from 1 to a given positive integer N.
Recursive Approach:
1. Base Case: If N is 1, the sum is simply 1.
Breakdown:
C++ implementation:
#include <iostream>
int main() {
int n;
std::cout << "Enter a positive integer: ";
std::cin >> n;
if (n < 1) {
std::cout << "Please enter a positive integer greater than 0." << '\n';
return 1;
}
return 0;
}
Explanation:
1. Function sumToN:
2. main Function:
6. After sumToN(1) returns, the return value of the previous function calls will be complete:
• sumToN(2) = 2 + 1 = 3
• sumToN(3) = 3 + 3 = 6
• sumToN(4) = 4 + 6 = 10
• sumToN(5) = 5 + 10 = 15
1. Missing Base Case: Leads to infinite recursion and stack overflow errors.
2. Incorrect Base Case: If the base case isn't correctly defined, the recursion might not
terminate as expected.
3. Not Progressing Toward Base Case: Each recursive call must modify the parameters to
approach the base case.
4. Excessive Memory Usage: Deep recursion can consume significant memory due to
multiple stack frames.
5. Redundant Calculations: Especially in problems like Fibonacci, where the same sub-
problems are solved multiple times, leading to inefficiency.
Example: Palindrome Checker
Problem Overview:
A palindrome is a word, phrase, or sequence that reads the same forwards and backwards,
ignoring spaces, punctuation, and capitalization. For example:
• “madam” is a palindrome.
• “racecar” is a palindrome.
• “hello” is not a palindrome.
• “A man, a plan, a canal, Panama” (ignoring spaces, punctuation, and case) is a
palindrome.
• “()()” is not a palindrome
1. The first and last characters of the string are the same.
• The base case is when the string has 0 or 1 characters (both are trivially palindromes).
• The recursive case checks the first and last characters, then makes a recursive call on the
substring (i.e., the string without its first and last characters).
Base Case:
Recursive Case:
1. If the first and last characters of the string are the same, recursively check the substring
that excludes the first and last characters.
2. If the first and last characters are not the same, the string is not a palindrome.
Pseudocode:
function isPalindrome(s, left, right) :
// Base case 1: If left index is greater than or equal to right index, it's
a palindrome
if left >= right :
return true
// Base case 2: If characters at left and right don't match, it's not a
palindrome
if s[left] != s[right] :
return false
• The function checks if the characters at the left and right positions of the string are equal.
• If left crosses right (or they are the same), the string is a palindrome.
• If the characters do not match, the string is not a palindrome.
• The recursive call moves the left value inward (by increasing it) and the right value inward
(by decreasing it), narrowing the substring at each step.
C++ Implementation:
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype> // For std::isalnum and std::tolower
// Base case: If characters at left and right indices don't match, it's not
a palindrome
if (s[left] != s[right]) {
return false;
}
// Recursive case: Move inward and check the next pair of characters
return isPalindrome(s, left + 1, right - 1);
}
int main() {
std::string input;
std::cout << "Enter a string: ";
std::getline(std::cin, input);
if (checkPalindrome(input)) {
std::cout << "\"" << input << "\"" << " is a palindrome.\n";
}
else {
std::cout << "\"" << input << "\"" << " is not a palindrome.\n";
}
return 0;
}
How This Works:
1. The checkPalindrome function first filters the input string, removing non-alphanumeric
characters (like spaces and punctuation) and converting all characters to lowercase. This
ensures that the palindrome check is case-insensitive and ignores characters like spaces
or punctuation.
3. Base Case: The recursion stops when the left index is greater than or equal to
the right index, meaning the entire string has been checked and confirmed as a palindrome.
Problem Overview:
You are given a 2D maze represented by a grid of characters. You need to find a path from a specific
start position (startChar) to an end position (endChar). The path cannot pass through walls
(wall character) and should not revisit any location.The recursive approach involves trying all
possible directions from the current position and backtracking if a path is not found.
Pseudocode:
The recursive function, walk, attempts to explore all possible paths from the current position. If it
reaches the end position, it returns true. Otherwise, it tries other directions, marking the current
position as visited to avoid cycles.
Base Case:
• If the current position is out of bounds, a wall, or already visited, return false.
• If the current position is the end position, return true.
Recursive Case:
// Base Case 4: If current position is the end, add to path and retu
rn true
if curr == end :
add curr to path
return true
Once the path has been found, you can update the maze to show the path using a simple function
that replaces the positions on the path with a marker like 'o'.
function drawPath(maze, path) :
result = copy of maze // Create a copy of the maze
for each point in path :
mark the point in result as 'o'
return result
Explanation of the Recursive Process:
1. Start from the Starting Point: Begin at the starting position found by findPosition(). Mark
the starting position as visited (seen[curr] = true).
2. Try Moving in All Directions: From the current position, attempt to move in all four
directions (up, right, down, left). For each direction, make a recursive call to walk() with the
new position.
3. Base Cases: If the new position is out of bounds, a wall, or already visited, the recursive
call returns false. If the new position is the end position, the path is complete, and the
recursive call returns true.
5. End Condition: Once the base case for the end position is met, the recursion stops and the
path is returned.
C++ implementation:
#include <iostream>
#include <vector>
#include <utility>
#include <string>
#include <stdexcept>
// If we've reached the end, add it to the path and return success
if (curr == end) {
path.push_back(curr);
return true;
}
// Function to solve the maze with flexible start and end characters
std::vector<Point> solveMaze(
const std::vector<std::string>& maze,
char wall,
char startChar,
char endChar
) {
Point start = findPosition(maze, startChar);
Point end = findPosition(maze, endChar);
std::vector<Point> path;
std::vector<std::vector<bool>> seen(maze.size(), std::vector<bool>(maze[0].
size(), false));
int main() {
std::vector<std::string> maze = {
"#S##################",
"# ##########",
"# #",
"############ #######",
"# # # ###",
"# ########## # #",
"# ####",
"# ##################",
" #",
"############ #######",
"# #####",
"#########E##########",
};
return 0;
}
Note that this recursive solution, while intuitive, is inefficient. This approach does not prioritize
directions which lead closer to the end goal, it only tries to find a path. It is unoptimized and is meant
as a basic example to demonstrate recursion.
For practical path-finding in large graphs/mazes, you can use alternative approaches like:
• Breadth-first search (iterative, uses queue)
• A* search (guided heuristic search)
• Use of additional optimization techniques (storing paths, heuristics, pruning)
Recursion is a powerful technique but not always the most efficient approach. You need to consider
time/space complexity for the problem at hand.
There are many interesting algorithms and techniques in path-finding that you can explore further.
Programming problems can be solved either by iteration or recursion. Let’s review the differences
between the two
Definition A loop structure (e.g., for, while) that A function calls itself to solve
repeats until a condition is satisfied. smaller instances of the same
problem.
Memory Generally uses less memory as it only Uses more memory due to the
Usage requires a few variables (loop function call stack (each recursive
counters). call adds a new frame).
Performance Generally more efficient in terms of Can be slower and more memory-
time and space. intensive due to function call
overhead.
Termination Terminates when the loop condition is Terminates when the base case is
no longer satisfied. reached in the recursion.
Code Often more straightforward and easier Can be more elegant and readable
Complexity to understand for simple tasks but can for problems that naturally fit a
become overly complex for divide- recursive structure (e.g., tree
and-conquer algorithms traversal).
Readability Can become verbose for problems Can be more concise and easier to
that require nested loops or complex understand for problems that fit a
conditions. recursive pattern.
Debugging Generally easier to debug because it Can be harder to debug due to the
follows a linear execution flow. non-linear nature of recursive calls
and the need to trace the call stack.
Overhead Low overhead as it avoids the function Higher overhead due to multiple
call mechanism. function calls, which involves
pushing and popping from the call
stack.
Reusability Loops are more flexible and reusable Recursive solutions can be reused
across different problems. and extended more easily for
problems that fit a recursive pattern.
Use recursion when the problem naturally breaks down into smaller subproblems (e.g., tree
traversal, factorial, Fibonacci, sorting algorithms like quicksort and mergesort). Recursion often
leads to cleaner code for these problems.
Use iteration when performance and memory efficiency are critical, and the problem can be easily
solved using loops (e.g., linear search, binary search, array traversal).