Introduction to Recursion:
Recursion is a programming technique where a function calls itself to solve a problem by breaking it down into smaller, similar sub-problems. It’s a powerful approach used in various algorithms and data structures.
Fundamentals of Recursive Functions
A recursive function consists of two main components:
- Base Case: The condition that terminates the recursion.
- Recursive Case: The part where the function calls itself with modified parameters.
Example: Factorial calculation
#include <iostream>
long long factorial(int n) {
if (n == 0 || n == 1) // Base case
return 1;
else // Recursive case
return n * factorial(n - 1);
}
Explanation:
- The
factorialfunction calculates the factorial of a given numbern. - Base case: If
nis 0 or 1, the function returns 1 (as 0! and 1! are both defined as 1). - Recursive case: For any other value of
n, the function returnsnmultiplied by the factorial ofn-1. - The recursion continues until it reaches the base case, effectively computing n * (n-1) * (n-2) * … * 2 * 1.
Types of Recursion
- Direct Recursion: Function calls itself directly.
- Indirect Recursion: Function calls another function, which then calls the original function.
- Linear Recursion: One recursive call per function call.
- Tree Recursion: Multiple recursive calls per function call.
Example of Tree Recursion (Fibonacci sequence):
int fibonacci(int n) {
if (n <= 1)
return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Explanation:
- This
fibonaccifunction calculates the nth Fibonacci number. - Base case: If
nis 0 or 1, it returnsnitself. - Recursive case: For n > 1, it calls itself twice:
fibonacci(n - 1): Calculates the (n-1)th Fibonacci numberfibonacci(n - 2): Calculates the (n-2)th Fibonacci number
- The results of these two calls are added to get the nth Fibonacci number.
- This is tree recursion because each function call spawns two more recursive calls, creating a tree-like structure of function calls.
Recursion vs Iteration
| Aspect | Recursion | Iteration |
|---|---|---|
| Implementation | Uses function call stack | Uses explicit loop constructs |
| Memory usage | Can be memory-intensive | Generally more memory-efficient |
| Code complexity | Often more concise and intuitive for certain problems | May require more explicit state management |
| Performance | Can be slower due to function call overhead | Generally faster |
| Suitability | Natural for problems with recursive structure (e.g., tree traversal) | Better for simple repetitive tasks |
Common Recursive Algorithms
- Merge Sort:
#include <vector> void merge(std::vector<int>& arr, int l, int m, int r) { int n1 = m - l + 1; int n2 = r - m; std::vector<int> L(n1), R(n2); // Copy data to temporary arrays L[] and R[] for (int i = 0; i < n1; i++) L[i] = arr[l + i]; for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j]; // Merge the temporary arrays back into arr[l..r] int i = 0, j = 0, k = l; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } // Copy the remaining elements of L[], if any while (i < n1) { arr[k] = L[i]; i++; k++; } // Copy the remaining elements of R[], if any while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(std::vector<int>& arr, int l, int r) { if (l >= r) return; int m = l + (r - l) / 2; mergeSort(arr, l, m); mergeSort(arr, m + 1, r); merge(arr, l, m, r); }Explanation:
mergeSortfunction:- Base case: If
l >= r, the subarray has 0 or 1 element, so it’s already sorted. - Recursive case:
- Find the middle point
mto divide the array into two halves. - Call
mergeSortfor the first half. - Call
mergeSortfor the second half. - Merge the two sorted halves.
- Find the middle point
- Base case: If
mergefunction:- Creates temporary arrays
L[]andR[]to hold the two halves. - Copies data to these temporary arrays.
- Merges the two sorted halves back into the original array.
- Creates temporary arrays
- Quick Sort:
#include <vector> int partition(std::vector<int>& arr, int low, int high) { int pivot = arr[high]; // Choose the last element as pivot int i = (low - 1); // Index of smaller element for (int j = low; j <= high - 1; j++) { // If current element is smaller than the pivot if (arr[j] < pivot) { i++; // Increment index of smaller element std::swap(arr[i], arr[j]); } } std::swap(arr[i + 1], arr[high]); return (i + 1); } void quickSort(std::vector<int>& arr, int low, int high) { if (low < high) { // pi is partitioning index, arr[pi] is now at right place int pi = partition(arr, low, high); // Separately sort elements before partition and after partition quickSort(arr, low, pi - 1); quickSort(arr, pi + 1, high); } }Explanation:
quickSortfunction:- Base case: If
low >= high, the subarray has 0 or 1 element, so it’s already sorted. - Recursive case:
- Partition the array and get the partitioning index.
- Recursively sort the left subarray (elements smaller than the pivot).
- Recursively sort the right subarray (elements greater than the pivot).
- Base case: If
partitionfunction:- Chooses the last element as the pivot.
- Places the pivot at its correct position in the sorted array.
- Places all smaller elements to the left of the pivot and all greater elements to the right.
- Binary Search:
int binarySearch(int arr[], int low, int high, int x) { if (high >= low) { int mid = low + (high - low) / 2; // If the element is present at the middle itself if (arr[mid] == x) return mid; // If element is smaller than mid, then it can only be present in left subarray if (arr[mid] > x) return binarySearch(arr, low, mid - 1, x); // Else the element can only be present in right subarray return binarySearch(arr, mid + 1, high, x); } // Element is not present in array return -1; }Explanation:
- Base case: If
high < low, the element is not in the array, so return -1. - Recursive case:
- Calculate the middle index
mid. - If the element at
midis the target, returnmid. - If the target is less than the element at
mid, search the left half. - If the target is greater than the element at
mid, search the right half.
- Calculate the middle index
- This function assumes the input array is sorted in ascending order.
- Base case: If
Optimization Techniques
- Tail Recursion: A special case where the recursive call is the last operation in the function.
long long factorialTail(int n, long long accumulator = 1) { if (n == 0 || n == 1) return accumulator; return factorialTail(n - 1, n * accumulator); }Explanation:
- This is a tail-recursive version of the factorial function.
- Base case: If
nis 0 or 1, return the accumulator. - Recursive case: Multiply
nwith the accumulator and pass it to the next recursive call. - The recursive call is the last operation, making it tail-recursive.
- Many compilers can optimize tail recursion into iteration, potentially saving stack space.
- Memoization: Storing results of expensive function calls to avoid redundant computations.
#include <unordered_map> int fibonacciMemo(int n, std::unordered_map<int, int>& memo) { if (memo.find(n) != memo.end()) return memo[n]; if (n <= 1) return n; memo[n] = fibonacciMemo(n-1, memo) + fibonacciMemo(n-2, memo); return memo[n]; } int fibonacci(int n) { std::unordered_map<int, int> memo; return fibonacciMemo(n, memo); }Explanation:
- This implementation uses memoization to optimize the Fibonacci sequence calculation.
memois an unordered_map that stores previously computed Fibonacci numbers.- Before computing a Fibonacci number, we check if it’s already in the memo.
- If it’s not in the memo, we compute it and store the result in the memo.
- This approach significantly reduces the number of recursive calls for larger values of n.
Practice Problems
Medium Level
-
Tower of Hanoi : https://www.geeksforgeeks.org/problems/tower-of-hanoi-1587115621/1
-
Flood Fill Algorithm : https://www.geeksforgeeks.org/flood-fill-algorithm/
-
Binary to Gray Conversion : https://www.geeksforgeeks.org/program-convert-binary-code-equivalent-gray-code-using-recursion/
-
Length of Longest Pallindromic Substring : https://www.geeksforgeeks.org/length-of-longest-palindromic-sub-string-recursion/?ref=lbp
-
All Possible Paths : https://www.geeksforgeeks.org/print-all-possible-paths-from-top-left-to-bottom-right-of-a-mxn-matrix/
Hard level
-
Find the Value of a Number raised to its reverse: https://www.geeksforgeeks.org/find-the-value-of-a-number-raised-to-its-reverse/
-
Pallindromic Partition: https://www.geeksforgeeks.org/given-a-string-print-all-possible-palindromic-partition/
-
N Queen Problem: https://www.geeksforgeeks.org/n-queen-problem-backtracking-3/
-
Generate all N digit numbers having absolute difference as K between adjacent digits: https://www.geeksforgeeks.org/generate-all-n-digit-numbers-having-absolute-difference-as-k-between-adjacent-digits/
-
Knight tour Problem: https://www.geeksforgeeks.org/the-knights-tour-problem/
These problems cover various aspects of recursion and will help in developing a deeper understanding of recursive problem-solving techniques in C++.
Top 50 Interview Questions: https://www.geeksforgeeks.org/top-50-interview-problems-on-recursion-algorithm/