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

DSA-Chapter-4.0-2024

Uploaded by

roinieva22
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)
7 views

DSA-Chapter-4.0-2024

Uploaded by

roinieva22
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/ 13

Data Structures and Algorithms

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.

There are two steps in recursion:

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.

For example: sumToN(5) = 1 + 2 + 3 + 4 + 5 = 15

Recursive Approach:
1. Base Case: If N is 1, the sum is simply 1.

2. Recursive Case: sumToN(N) = N + sumToN(N - 1)

Breakdown:

o To find sumToN(5), compute 5 + sumToN(4)

o To find sumToN(4), compute 4 + sumToN(3)

o Continue until the base case is reached.

C++ implementation:
#include <iostream>

// Function to compute sum from 1 to N using recursion


int sumToN(int n) {
// Base Case
if (n == 1)
return 1;
// Recursive Case
else
return n + sumToN(n - 1);
}

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

int result = sumToN(n);


std::cout << "Sum from 1 to " << n << " is: " << result << '\n';

return 0;
}

Explanation:

1. Function sumToN:

• Base Case: If N is 1, return 1.


• Recursive Case: Return N plus the result of sumToN(N - 1).

2. main Function:

• Prompts the user to enter a positive integer.


• Validates the input.
• Calls sumToN and displays the result.

Execution Flow for sumToN(5):

1. sumToN(5) returns 5 + sumToN(4)

2. sumToN(4) returns 4 + sumToN(3)

3. sumToN(3) returns 3 + sumToN(2)

4. sumToN(2) returns 2 + sumToN(1)

5. sumToN(1) returns 1 (Base Case)

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

Common Pitfalls in Recursion

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

To solve this problem using recursion, we need to check if:

1. The first and last characters of the string are the same.

2. The substring between those two characters is a palindrome.

This forms a natural recursive structure because:

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

Recursive Approach Breakdown

Base Case:

1. A string with 0 or 1 characters is always a palindrome (because there's nothing to


compare).

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

// Recursive case: Check the substring by moving inward


return isPalindrome(s, left + 1, right - 1)
Explanation:

• 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

// Recursive function to check if a string is a palindrome


bool isPalindrome(const std::string& s, int left, int right) {
// Base case: If left index is greater or equal to right, it's a palindrome
if (left >= right) {
return true;
}

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

// Main palindrome checker function (after preprocessing to ignore non-


alphanumeric chars)
bool checkPalindrome(const std::string& input) {
// Construct a filtered string that ignores non-
alphanumeric characters and converts to lowercase
std::string filtered;
for (char c : input) {
if (std::isalnum(static_cast<unsigned char>(c))) { // Keep only alphan
umeric characters
filtered += static_cast<char>(std::tolower(static_cast<unsigned cha
r>(c))); // Convert to lowercase
}
}
// Call the recursive palindrome checker on the filtered string
return isPalindrome(filtered, 0, static_cast<int>(filtered.length()) - 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.

2. Recursive Palindrome Check: The filtered string is passed to the recursive


function isPalindrome, which checks if the string reads the same forwards and
backwards.The recursion continues by checking pairs of characters from the outside in: first
comparing the first and last characters, then moving towards the center.

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.

Example: Maze-solving using recursion

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:

1. Find the Start and End Positions


Before starting the recursion, you need to find the positions of the start and end characters in the
maze.
function findPosition(maze, target) :
for each row in maze :
for each column in row :
if cell contains target :
return (x, y) // Return the position of the target
throw error if target not found

2. Recursive Maze Walking Function

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:

• Mark the current position as visited.


• Try to move in four possible directions (up, right, down, left).
• Recursively call the walk function for each direction.
• If any recursive call returns true, stop and return true.
• If no direction works, backtrack by unmarking the current position and return false.
function walk(maze, wall, curr, end, seen, path) :
// Base Case 1: If current position is out of bounds, return false
if curr is out of bounds :
return false

// Base Case 2: If current position is a wall, return false


if maze[curr] == wall :
return false

// Base Case 3: If current position is already visited, return false


if seen[curr] :
return false

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

// Recursive Case: Mark the current position as visited


mark curr as seen
add curr to path

// Try moving in all 4 directions (up, right, down, left)


for each direction in {up, right, down, left}:
next = curr + direction // Calculate next position
if walk(maze, wall, next, end, seen, path) :
return true // If successful, return true

// Backtrack: If no direction works, remove current position from path a


nd return false
remove curr from path
return false
3. Solve the Maze

The solveMaze function sets up the recursive process. It:

• Finds the start and end positions.


• Initializes a 2D seen array to track visited positions.
• Calls the recursive walk function.
• Returns the path if a solution is found.
function solveMaze(maze, wall, startChar, endChar) :
// Find the start and end positions in the maze
start = findPosition(maze, startChar)
end = findPosition(maze, endChar)

// Initialize the "seen" array to track visited positions


seen = 2D array of false values with the same dimensions as maze

// Initialize an empty path


path = empty list

// Call the recursive walk function


walk(maze, wall, start, end, seen, path)

// Return the path (empty if no solution found)


return path
4. Draw the Path on the Maze

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.

4. Backtracking: If none of the directions from the current position led to a


solution, backtrack by undoing the changes (removing the current position from the path)
and try a different direction. This ensures that if a wrong path is taken, the algorithm can
"back up" and try other options.

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>

// Define a type alias for clarity


using Point = std::pair<int, int>;

// Directions for movement in the maze


const std::vector<Point> dir = {
{0, -1}, // Up
{1, 0}, // Right
{0, 1}, // Down
{-1, 0} // Left
};

// Function to find the position of a target character in the maze


Point findPosition(const std::vector<std::string>& maze, char target) {
for (size_t y = 0; y < maze.size(); ++y) {
for (size_t x = 0; x < maze[y].length(); ++x) {
if (maze[y][x] == target) {
return { static_cast<int>(x), static_cast<int>(y) };
}
}
}
throw std::runtime_error("Target not found in maze");
}

// Recursive function to solve the maze


bool walk(
const std::vector<std::string>& maze,
char wall,
Point curr,
Point end,
std::vector<std::vector<bool>>& seen,
std::vector<Point>& path
) {
// Check if current position is out of bounds, wall, or already visited
if (curr.first < 0 || curr.first >= maze[0].size() ||
curr.second < 0 || curr.second >= maze.size() ||
maze[curr.second][curr.first] == wall ||
seen[curr.second][curr.first]) {
return false;
}

// If we've reached the end, add it to the path and return success
if (curr == end) {
path.push_back(curr);
return true;
}

// Mark current position as seen


seen[curr.second][curr.first] = true;
path.push_back(curr);

// Try all four directions


for (const auto& d : dir) {
Point next(curr.first + d.first, curr.second + d.second);
if (walk(maze, wall, next, end, seen, path)) {
return true;
}
}

// If no path found, backtrack


path.pop_back();
return false;
}

// 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));

walk(maze, wall, start, end, seen, path);


return path;
}
// Function to draw the path on the maze
std::vector<std::string> drawPath(const std::vector<std::string>& maze, const s
td::vector<Point>& path) {
std::vector<std::string> result = maze;
for (const auto& p : path) {
result[p.second][p.first] = 'o';
}
return result;
}

int main() {
std::vector<std::string> maze = {
"#S##################",
"# ##########",
"# #",
"############ #######",
"# # # ###",
"# ########## # #",
"# ####",
"# ##################",
" #",
"############ #######",
"# #####",
"#########E##########",
};

// Solve the maze with custom start and end characters


std::vector<Point> result = solveMaze(maze, '#', 'S', 'E');

// Print the path coordinates


std::cout << "Path coordinates:\n";
for (const auto& p : result) {
std::cout << "(" << p.first << ", " << p.second << ")\n";
}

// Draw the path on the maze


std::vector<std::string> pathDrawnMaze = drawPath(maze, result);

// Print the maze with the path


std::cout << "\nMaze with path:\n";
for (const auto& row : pathDrawnMaze) {
std::cout << row << '\n';
}

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

Criteria Iteration Recursion

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.

State Uses loop variables to keep track of Relies on function parameters to


Management the iteration state. pass state between recursive calls,
and the call stack implicitly
manages the state of each recursive
call.

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.

Stack No risk of stack overflow, as loops do Risk of stack overflow if the


Overflow Risk not use the call stack. recursion depth is too large (i.e., too
many function calls).

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

Problem Suitable for problems where the Suitable for divide-and-conquer


Suitability solution is easily expressed with problems or problems that naturally
loops (e.g., array traversal, finding decompose into smaller
maximum/minimum). subproblems (e.g., quicksort, tree
traversal, Fibonacci numbers).

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.

General Rule of Thumb:

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

You might also like