Change Theme
Font Size
# Lecture 21: Recursion Deep Dive *Advanced recursive algorithms and their applications.* --- ## Introduction to Recursion **Recursion:** A function that calls itself to solve smaller instances of the same problem **Key Components:** * **Base Case:** Stopping condition to prevent infinite recursion * **Recursive Case:** Function calls itself with modified parameters * **Call Stack:** Memory structure tracking function calls --- **Why Recursion Matters:** * **Divide and Conquer:** Break complex problems into simpler subproblems * **Elegant Solutions:** Often more intuitive than iterative approaches * **Tree/Graph Problems:** Natural fit for hierarchical data structures * **Mathematical Modeling:** Direct implementation of recursive mathematical definitions --- **Recursion vs Iteration:** * **Recursion:** Uses call stack, can be memory-intensive * **Iteration:** Uses loops, generally more memory-efficient * **Trade-offs:** Recursion often clearer, iteration often faster
## Call Stack Visualizer **Understanding the Call Stack:** * Each recursive call creates a new stack frame * Stack frames contain local variables and return address * Stack unwinds as functions return * Stack overflow occurs when too many frames are created --- **Factorial Example - Call Stack:** ``` factorial(5) ├── factorial(4) │ ├── factorial(3) │ │ ├── factorial(2) │ │ │ ├── factorial(1) │ │ │ │ └── returns 1 │ │ │ └── returns 2 * 1 = 2 │ │ └── returns 3 * 2 = 6 │ └── returns 4 * 6 = 24 └── returns 5 * 24 = 120 ``` --- **Visualization:** Interactive factorial calculation showing stack frames
Call Stack Visualizer
Calculate factorial of:
▶ Run
Step →
↻ Reset
## Factorial Implementation **Recursive Factorial:** ```cpp int factorial(int n) { // Base case: prevents infinite recursion if (n == 0 || n == 1) { return 1; } // Recursive case: calls itself with smaller problem return n * factorial(n - 1); } ``` --- **Execution Trace - factorial(5):** * factorial(5) → 5 * factorial(4) * factorial(4) → 4 * factorial(3) * factorial(3) → 3 * factorial(2) * factorial(2) → 2 * factorial(1) * factorial(1) → returns 1 * factorial(2) → returns 2 * 1 = 2 * factorial(3) → returns 3 * 2 = 6 * factorial(4) → returns 4 * 6 = 24 * factorial(5) → returns 5 * 24 = 120 --- **Time Complexity:** O(n) - n recursive calls **Space Complexity:** O(n) - call stack depth
## Linked List Reversal **Recursive Linked List Reversal:** ```cpp Node* reverseList(Node* head) { // Base case: empty list or single node if (!head || !head->next) { return head; } // Recursive case: reverse the rest of the list Node* newHead = reverseList(head->next); // Reverse the current node's pointer head->next->next = head; head->next = nullptr; return newHead; } ``` --- **Algorithm Steps:** 1. **Base Case:** If list is empty or has one node, return head 2. **Recursive Call:** Reverse everything after current node 3. **Pointer Reversal:** Make current node's next point back to current node 4. **Cleanup:** Set current node's next to null 5. **Return:** New head of reversed list --- **Visualization:** Step-by-step reversal of linked list nodes --- **Time Complexity:** O(n) - visits each node once **Space Complexity:** O(n) - recursion depth equals list length
Linked List Reversal
▶ Run
Step →
↻ Reset
## Array Permutations **Recursive Permutation Generation:** ```cpp void permute(vector
& arr, int l, int r) { // Base case: when left index reaches right if (l == r) { printArray(arr); // Print current permutation return; } // Recursive case: try each element at current position for (int i = l; i <= r; i++) { swap(arr[l], arr[i]); // Fix element at position l permute(arr, l + 1, r); // Recurse for remaining elements swap(arr[l], arr[i]); // Backtrack - restore original order } } ``` --- **Backtracking Concept:** * **Fix:** Choose an element and place it at current position * **Recurse:** Generate permutations of remaining elements * **Backtrack:** Undo the choice to try next element --- **Example - permute([1,2,3], 0, 2):** ``` Original: [1,2,3] ├── Swap 0↔0: [1,2,3] → permute([1,2,3], 1, 2) │ ├── Swap 1↔1: [1,2,3] → permute([1,2,3], 2, 2) → [1,2,3] │ ├── Swap 1↔2: [1,3,2] → permute([1,3,2], 2, 2) → [1,3,2] │ └── Backtrack: [1,2,3] ├── Swap 0↔1: [2,1,3] → permute([2,1,3], 1, 2) │ ├── Swap 1↔1: [2,1,3] → permute([2,1,3], 2, 2) → [2,1,3] │ ├── Swap 1↔2: [2,3,1] → permute([2,3,1], 2, 2) → [2,3,1] │ └── Backtrack: [2,1,3] └── Swap 0↔2: [3,2,1] → permute([3,2,1], 1, 2) ├── Swap 1↔1: [3,2,1] → permute([3,2,1], 2, 2) → [3,2,1] ├── Swap 1↔2: [3,1,2] → permute([3,1,2], 2, 2) → [3,1,2] └── Backtrack: [3,2,1] ``` --- **Time Complexity:** O(n!) - n factorial permutations
Array Permutations
▶ Run
↻ Reset
## Longest Common Subsequence **Memoized Recursive LCS:** ```cpp int lcs(string s1, string s2, int m, int n, vector
>& memo) { // Base case: empty string if (m == 0 || n == 0) return 0; // Check memoization table if (memo[m][n] != -1) return memo[m][n]; // Recursive cases if (s1[m-1] == s2[n-1]) { // Characters match - add 1 and recurse on diagonals memo[m][n] = 1 + lcs(s1, s2, m-1, n-1, memo); } else { // Characters don't match - take max of left or top memo[m][n] = max(lcs(s1, s2, m-1, n, memo), lcs(s1, s2, m, n-1, memo)); } return memo[m][n]; } ``` --- **LCS Algorithm:** * **Base Case:** If either string is empty, LCS length is 0 * **Match Case:** If characters match, LCS = 1 + LCS of remaining strings * **No Match:** LCS = max(LCS without last char of s1, LCS without last char of s2) * **Memoization:** Store results to avoid recomputation --- **Example:** LCS of "ABCD" and "ACBD" * Common subsequence: "ABD" (length 3) * Other possibilities: "ACD" (length 3), "BCD" (length 3) --- **Visualization:** Step-by-step memoization table filling --- **Time Complexity:** O(m*n) - each cell computed once **Space Complexity:** O(m*n) - memoization table
Longest Common Subsequence
▶ Run
Step →
↻ Reset
## Bucket Sort **Bucket Sort Algorithm:** ```cpp void bucketSort(vector
& arr) { int n = arr.size(); vector
> buckets(n); // Step 1: Distribute elements into buckets for (float num : arr) { int idx = n * num; // Assumes elements in [0,1) buckets[idx].push_back(num); } // Step 2: Sort individual buckets for (auto& bucket : buckets) { sort(bucket.begin(), bucket.end()); } // Step 3: Concatenate sorted buckets int idx = 0; for (auto& bucket : buckets) { for (float num : bucket) { arr[idx++] = num; } } } ``` --- **Algorithm Steps:** 1. **Create buckets:** Initialize n empty buckets 2. **Distribute:** Place each element in appropriate bucket based on value 3. **Sort buckets:** Sort each individual bucket 4. **Concatenate:** Combine sorted buckets back into original array --- **Requirements:** * Elements must be uniformly distributed in a known range * Usually requires elements in [0,1) or normalization * Works best when input is uniformly distributed --- **Visualization:** Step-by-step bucket sort demonstration --- **Time Complexity:** O(n + k) average case, where k = number of buckets **Space Complexity:** O(n + k) - additional space for buckets
Bucket Sort Visualization
▶ Run
Step →
↻ Reset
## Quiz: Recursion Basics **What happens if a recursive function lacks a base case?** * Returns null * Infinite recursion/stack overflow * Compiles with error * Works normally
## Quiz: Recursion Basics - Answer **What happens if a recursive function lacks a base case?** **Answer: Infinite recursion/stack overflow** Without a base case, the function calls itself forever until the stack overflows. This is why every recursive function must have a proper base case to terminate the recursion.
## Quiz: Complexity Analysis **What is the time complexity of factorial(n)?** * O(1) * O(log n) * O(n) * O(n²)
## Quiz: Complexity Analysis - Answer **What is the time complexity of factorial(n)?** **Answer: O(n)** The function makes n recursive calls, each doing constant work. For factorial(5), there are 5 calls: factorial(5) → factorial(4) → factorial(3) → factorial(2) → factorial(1).
## Quiz: Algorithm Application **Which data structure is essential for recursion?** * Array * Linked List * Stack * Queue
## Quiz: Algorithm Application - Answer **Which data structure is essential for recursion?** **Answer: Stack** Recursion uses the call stack to manage function calls and local variables. Each recursive call creates a new stack frame, and the stack unwinds as functions return.
## Summary & Best Practices **Recursion Trade-offs:** * **Elegant & intuitive** for tree/graph problems * **Higher memory overhead** (call stack) * **Risk of stack overflow** for deep recursion --- **Debugging Tips:** * Print function parameters at entry/exit * Verify base case correctness * Trace small examples manually --- **Practice Problems:** * Tower of Hanoi * Recursive Binary Search * Merge Sort --- **Key Takeaways:** * **Base Case:** Essential to prevent infinite recursion * **Recursive Case:** Must progress toward base case * **Call Stack:** Understand memory implications * **Memoization:** Optimize repeated subproblems --- **When to Use Recursion:** * Problems with natural recursive structure * Tree/graph traversals * Divide and conquer algorithms * Mathematical definitions (factorial, Fibonacci)
## Navigation * [Back to Course Outline](index.html) * [Previous: OOP in Practice](lecture19.html) * [Next: Recursive Matrix Multiplication & Radix Sort](lecture21.html)