Day 5: Recursion & Divide and Conquer
What You'll Learn Today
- Fundamentals of recursion and the call stack
- Base cases and recursive cases
- The divide and conquer paradigm
- Practical recursive algorithms
What Is Recursion?
Recursion is when a function calls itself. It breaks a large problem into smaller subproblems of the same structure.
flowchart TB
subgraph Recursion["Recursion Flow"]
F5["factorial(5)"] --> F4["5 Γ factorial(4)"]
F4 --> F3["4 Γ factorial(3)"]
F3 --> F2["3 Γ factorial(2)"]
F2 --> F1["2 Γ factorial(1)"]
F1 --> BASE["1 (base case)"]
end
style Recursion fill:#3b82f6,color:#fff
style BASE fill:#22c55e,color:#fff
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
console.log(factorial(5)); // 120
Two Essential Components
Every recursive function needs:
1. Base Case
The condition that stops the recursion. Without it, you get infinite recursion.
2. Recursive Case
The part that reduces the problem and calls itself.
function countdown(n) {
if (n <= 0) {
console.log("Done!");
return;
}
console.log(n);
countdown(n - 1);
}
countdown(5); // 5, 4, 3, 2, 1, Done!
The Call Stack
Each recursive call is pushed onto the call stack. When the base case is reached, return values propagate back up.
flowchart TB
subgraph CallStack["Call Stack"]
direction TB
S1["factorial(1) β 1"]
S2["factorial(2) β 2 Γ 1"]
S3["factorial(3) β 3 Γ 2"]
S4["factorial(4) β 4 Γ 6"]
S5["factorial(5) β 5 Γ 24"]
end
S5 --> S4 --> S3 --> S2 --> S1
S1 -.->|return 1| S2
S2 -.->|return 2| S3
S3 -.->|return 6| S4
S4 -.->|return 24| S5
style CallStack fill:#8b5cf6,color:#fff
Stack Overflow
Too-deep recursion causes a stack overflow.
// This will crash!
function infinite() {
return infinite();
}
// RangeError: Maximum call stack size exceeded
Common Recursive Patterns
Sum of an Array
function sum(arr) {
if (arr.length === 0) return 0;
return arr[0] + sum(arr.slice(1));
}
console.log(sum([1, 2, 3, 4, 5])); // 15
String Reversal
function reverse(str) {
if (str.length <= 1) return str;
return reverse(str.slice(1)) + str[0];
}
console.log(reverse("hello")); // "olleh"
Exponentiation
// O(n) version
function power(base, exp) {
if (exp === 0) return 1;
return base * power(base, exp - 1);
}
// O(log n) version using divide and conquer
function fastPower(base, exp) {
if (exp === 0) return 1;
if (exp % 2 === 0) {
const half = fastPower(base, exp / 2);
return half * half;
}
return base * fastPower(base, exp - 1);
}
console.log(fastPower(2, 10)); // 1024
Divide and Conquer
Divide and conquer solves problems in three steps:
flowchart TB
P["Problem"] --> D["1. Divide\nBreak into subproblems"]
D --> C["2. Conquer\nSolve recursively"]
C --> M["3. Combine\nMerge results"]
style P fill:#3b82f6,color:#fff
style D fill:#22c55e,color:#fff
style C fill:#f59e0b,color:#fff
style M fill:#8b5cf6,color:#fff
Example: Merge Sort
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
const sortedLeft = mergeSort(left);
const sortedRight = mergeSort(right);
return merge(sortedLeft, sortedRight);
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
}
console.log(mergeSort([38, 27, 43, 3, 9, 82, 10]));
// [3, 9, 10, 27, 38, 43, 82]
flowchart TB
A["[38, 27, 43, 3, 9, 82, 10]"] --> B["[38, 27, 43]"]
A --> C["[3, 9, 82, 10]"]
B --> D["[38]"]
B --> E["[27, 43]"]
E --> F["[27]"]
E --> G["[43]"]
C --> H["[3, 9]"]
C --> I["[82, 10]"]
H --> J["[3]"]
H --> K["[9]"]
I --> L["[82]"]
I --> M["[10]"]
F -.-> N["[27, 43]"]
G -.-> N
D -.-> O["[27, 38, 43]"]
N -.-> O
J -.-> P["[3, 9]"]
K -.-> P
L -.-> Q["[10, 82]"]
M -.-> Q
P -.-> R["[3, 9, 10, 82]"]
Q -.-> R
O -.-> S["[3, 9, 10, 27, 38, 43, 82]"]
R -.-> S
style A fill:#3b82f6,color:#fff
style S fill:#22c55e,color:#fff
Binary Search (Divide and Conquer)
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
if (left > right) return -1;
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) {
return binarySearch(arr, target, mid + 1, right);
}
return binarySearch(arr, target, left, mid - 1);
}
const sorted = [1, 3, 5, 7, 9, 11, 13, 15];
console.log(binarySearch(sorted, 7)); // 3
console.log(binarySearch(sorted, 6)); // -1
Recursion vs. Iteration
Every recursive solution can be rewritten as an iterative one.
// Recursive fibonacci: O(2βΏ)
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// Iterative fibonacci: O(n)
function fibIterative(n) {
if (n <= 1) return n;
let prev = 0, curr = 1;
for (let i = 2; i <= n; i++) {
[prev, curr] = [curr, prev + curr];
}
return curr;
}
// Memoized fibonacci: O(n)
function fibMemo(n, memo = {}) {
if (n <= 1) return n;
if (memo[n]) return memo[n];
memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
return memo[n];
}
| Approach | Time | Space |
|---|---|---|
| Naive recursion | O(2βΏ) | O(n) |
| Memoized recursion | O(n) | O(n) |
| Iteration | O(n) | O(1) |
Tail Recursion
Tail recursion is when the recursive call is the very last operation. Some languages optimize this.
// Not tail recursive
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Tail recursive
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
Summary
| Concept | Description |
|---|---|
| Recursion | A function calling itself |
| Base Case | Condition that stops recursion |
| Call Stack | Stack where recursive calls accumulate |
| Divide & Conquer | Divide β Conquer β Combine |
| Memoization | Caching results to avoid redundant computation |
Key Takeaways
- Every recursion needs a base case and a recursive case
- Divide and conquer splits problems into divide β conquer β combine
- Memoization can reduce exponential time to linear
- Too-deep recursion causes stack overflow
Practice Problems
Problem 1: Basic
Implement a recursive function to find the maximum value in an array.
Problem 2: Intermediate
Implement a recursive function to check whether a string is a palindrome.
Challenge
Implement a recursive function to generate all subsets (power set) of an array with N elements.
References
Next up: In Day 6, we'll explore "Sorting Algorithms" β comparing the mechanics and performance of major sorting algorithms.