How Recursion Works
Recursion is a method where a function calls itself to solve a problem by breaking it down into smaller, similar subproblems. Every recursive solution needs two essential components.
FAANG Interview Prep
Foundations, Memory & Complexity
Recursion Complete Guide
Arrays & Array ADT
Strings
Matrices
Linked Lists
Stack
Queue
Trees
BST & Balanced Trees
Heaps, Sorting & Hashing
Graphs, DP, Greedy & Backtracking
Two Essential Components
- Base Case: The condition that stops recursion (prevents infinite calls)
- Recursive Case: The function calling itself with a modified input that moves toward the base case
Python
# Basic Recursion Structure
def factorial(n):
# Base Case: Stop when n reaches 0 or 1
if n <= 1:
return 1
# Recursive Case: n! = n × (n-1)!
return n * factorial(n - 1)
# Test
print(f"5! = {factorial(5)}") # Output: 120
print(f"0! = {factorial(0)}") # Output: 1
C++
// Basic Recursion Structure in C++
#include <iostream>
using namespace std;
int factorial(int n) {
// Base Case: Stop when n reaches 0 or 1
if (n <= 1) {
return 1;
}
// Recursive Case: n! = n × (n-1)!
return n * factorial(n - 1);
}
int main() {
cout << "5! = " << factorial(5) << endl; // Output: 120
cout << "0! = " << factorial(0) << endl; // Output: 1
return 0;
}
Java
// Basic Recursion Structure in Java
public class Factorial {
// Recursive factorial method
public static int factorial(int n) {
// Base Case: Stop when n reaches 0 or 1
if (n <= 1) {
return 1;
}
// Recursive Case: n! = n × (n-1)!
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5)); // Output: 120
System.out.println("0! = " + factorial(0)); // Output: 1
}
}
Call Stack Tracing
Understanding how the call stack works is crucial for mastering recursion. Each recursive call creates a new stack frame with its own local variables.
What is a Stack Frame?
Every time a function is called, a stack frame is pushed onto the call stack containing: the return address, function parameters, and local variables. When the function returns, its frame is popped off. In recursion, each call adds a new frame until the base case is reached, then frames are popped as results are returned.
Python
# Visualizing Call Stack for factorial(4)
def factorial_traced(n, depth=0):
"""Factorial with call stack visualization"""
indent = " " * depth
print(f"{indent}factorial({n}) called")
if n <= 1:
print(f"{indent}factorial({n}) returns 1 (base case)")
return 1
result = n * factorial_traced(n - 1, depth + 1)
print(f"{indent}factorial({n}) returns {result}")
return result
# Trace factorial(4)
print("Call Stack Trace:")
result = factorial_traced(4)
print(f"\nFinal Result: {result}")
# Output:
# factorial(4) called
# factorial(3) called
# factorial(2) called
# factorial(1) called
# factorial(1) returns 1 (base case)
# factorial(2) returns 2
# factorial(3) returns 6
# factorial(4) returns 24
# Final Result: 24
C++
// Visualizing Call Stack for factorial(4) in C++
#include <iostream>
#include <string>
using namespace std;
int factorial_traced(int n, int depth = 0) {
string indent(depth * 2, ' '); // Create indentation
cout << indent << "factorial(" << n << ") called" << endl;
if (n <= 1) {
cout << indent << "factorial(" << n << ") returns 1 (base case)" << endl;
return 1;
}
int result = n * factorial_traced(n - 1, depth + 1);
cout << indent << "factorial(" << n << ") returns " << result << endl;
return result;
}
int main() {
cout << "Call Stack Trace:" << endl;
int result = factorial_traced(4);
cout << "\nFinal Result: " << result << endl;
return 0;
}
Java
// Visualizing Call Stack for factorial(4) in Java
public class CallStackTrace {
public static int factorialTraced(int n, int depth) {
String indent = " ".repeat(depth); // Create indentation
System.out.println(indent + "factorial(" + n + ") called");
if (n <= 1) {
System.out.println(indent + "factorial(" + n + ") returns 1 (base case)");
return 1;
}
int result = n * factorialTraced(n - 1, depth + 1);
System.out.println(indent + "factorial(" + n + ") returns " + result);
return result;
}
public static void main(String[] args) {
System.out.println("Call Stack Trace:");
int result = factorialTraced(4, 0);
System.out.println("\nFinal Result: " + result);
}
}
Types of Recursion
1. Tail Recursion
The recursive call is the last operation in the function. Can be optimized by compilers to use constant stack space (Tail Call Optimization).
Why Tail Recursion Matters
In tail recursion, there's no pending computation after the recursive call returns. This means compilers can reuse the same stack frame for each call (Tail Call Optimization), converting O(n) space to O(1). Languages like Scheme guarantee TCO; C++ and Java do not, but understanding it helps write efficient code.
Python
# Tail Recursion - recursive call is last operation
def factorial_tail(n, accumulator=1):
"""
Tail-recursive factorial
All computation done BEFORE the recursive call
"""
if n <= 1:
return accumulator
return factorial_tail(n - 1, n * accumulator) # Last operation
# Test
print(f"Tail recursive: {factorial_tail(5)}") # Output: 120
C++
// Tail Recursion in C++
#include <iostream>
using namespace std;
int factorial_tail(int n, int accumulator = 1) {
// Base case
if (n <= 1) {
return accumulator;
}
// Tail recursive call - computation done before call
return factorial_tail(n - 1, n * accumulator);
}
int main() {
cout << "Tail recursive: " << factorial_tail(5) << endl; // Output: 120
return 0;
}
Java
// Tail Recursion in Java
public class TailRecursion {
public static int factorialTail(int n, int accumulator) {
// Base case
if (n <= 1) {
return accumulator;
}
// Tail recursive call
return factorialTail(n - 1, n * accumulator);
}
public static void main(String[] args) {
System.out.println("Tail recursive: " + factorialTail(5, 1)); // Output: 120
}
}
2. Head Recursion
The recursive call is made before any other processing. Work is done during the "unwinding" phase.
Python
# Head Recursion - processing after recursive call
def print_numbers_head(n):
"""
Head-recursive: prints numbers 1 to n
Processing happens AFTER the recursive call returns
"""
if n == 0:
return
print_numbers_head(n - 1) # Recursive call FIRST
print(n) # Processing AFTER
# Test
print("Head Recursion (1 to 5):")
print_numbers_head(5) # Output: 1, 2, 3, 4, 5
C++
// Head Recursion in C++
#include <iostream>
using namespace std;
void printNumbersHead(int n) {
if (n == 0) {
return;
}
printNumbersHead(n - 1); // Recursive call FIRST
cout << n << " "; // Processing AFTER
}
int main() {
cout << "Head Recursion (1 to 5): ";
printNumbersHead(5); // Output: 1 2 3 4 5
cout << endl;
return 0;
}
Java
// Head Recursion in Java
public class HeadRecursion {
public static void printNumbersHead(int n) {
if (n == 0) {
return;
}
printNumbersHead(n - 1); // Recursive call FIRST
System.out.print(n + " "); // Processing AFTER
}
public static void main(String[] args) {
System.out.print("Head Recursion (1 to 5): ");
printNumbersHead(5); // Output: 1 2 3 4 5
System.out.println();
}
}
3. Tree Recursion
The function makes multiple recursive calls in each invocation, creating a tree-like call structure.
Warning: Exponential Growth
Tree recursion often leads to exponential time complexity O(2n) because each call spawns multiple new calls. For Fibonacci, fib(40) makes over 300 million function calls! Always consider memoization or iterative solutions for tree recursive problems.
Python
# Tree Recursion - multiple recursive calls
def fibonacci_tree(n):
"""
Tree-recursive Fibonacci
Each call branches into two more calls
Time: O(2^n) - exponential!
"""
if n <= 1:
return n
return fibonacci_tree(n - 1) + fibonacci_tree(n - 2) # Two calls
# Test
print(f"Fibonacci(10) = {fibonacci_tree(10)}") # Output: 55
C++
// Tree Recursion in C++
#include <iostream>
using namespace std;
int fibonacci_tree(int n) {
// Base case
if (n <= 1) {
return n;
}
// Tree recursion - two recursive calls
return fibonacci_tree(n - 1) + fibonacci_tree(n - 2);
}
int main() {
cout << "Fibonacci(10) = " << fibonacci_tree(10) << endl; // Output: 55
return 0;
}
Java
// Tree Recursion in Java
public class TreeRecursion {
public static int fibonacciTree(int n) {
// Base case
if (n <= 1) {
return n;
}
// Tree recursion - two recursive calls
return fibonacciTree(n - 1) + fibonacciTree(n - 2);
}
public static void main(String[] args) {
System.out.println("Fibonacci(10) = " + fibonacciTree(10)); // Output: 55
}
}
4. Indirect Recursion
Function A calls function B, which calls function A (or through more functions in a cycle).
Python
# Indirect Recursion - functions call each other
def is_even(n):
if n == 0:
return True
return is_odd(n - 1)
def is_odd(n):
if n == 0:
return False
return is_even(n - 1)
# Test
print(f"is_even(4) = {is_even(4)}") # True
print(f"is_odd(5) = {is_odd(5)}") # True
C++
// Indirect Recursion in C++
#include <iostream>
using namespace std;
// Forward declaration needed for indirect recursion
bool isOdd(int n);
bool isEven(int n) {
if (n == 0) return true;
return isOdd(n - 1);
}
bool isOdd(int n) {
if (n == 0) return false;
return isEven(n - 1);
}
int main() {
cout << "isEven(4) = " << (isEven(4) ? "true" : "false") << endl; // true
cout << "isOdd(5) = " << (isOdd(5) ? "true" : "false") << endl; // true
return 0;
}
Java
// Indirect Recursion in Java
public class IndirectRecursion {
public static boolean isEven(int n) {
if (n == 0) return true;
return isOdd(n - 1);
}
public static boolean isOdd(int n) {
if (n == 0) return false;
return isEven(n - 1);
}
public static void main(String[] args) {
System.out.println("isEven(4) = " + isEven(4)); // true
System.out.println("isOdd(5) = " + isOdd(5)); // true
}
}
5. Nested Recursion
A recursive call is used as an argument to another recursive call.
The Ackermann Function
The Ackermann function is the classic example of nested recursion. It grows incredibly fast—A(4,2) has over 19,000 digits! It's used in computer science to demonstrate that not all computable functions are primitive recursive.
Python
# Nested Recursion - recursive call as argument
def ackermann(m, n):
"""
Ackermann function - classic nested recursion example
Grows EXTREMELY fast
"""
if m == 0:
return n + 1
if n == 0:
return ackermann(m - 1, 1)
return ackermann(m - 1, ackermann(m, n - 1)) # Nested!
# Test (only small values!)
print(f"ackermann(2, 2) = {ackermann(2, 2)}") # Output: 7
print(f"ackermann(3, 2) = {ackermann(3, 2)}") # Output: 29
C++
// Nested Recursion - Ackermann Function in C++
#include <iostream>
using namespace std;
int ackermann(int m, int n) {
if (m == 0) {
return n + 1;
}
if (n == 0) {
return ackermann(m - 1, 1);
}
// Nested recursion - inner call's result becomes argument
return ackermann(m - 1, ackermann(m, n - 1));
}
int main() {
cout << "ackermann(2, 2) = " << ackermann(2, 2) << endl; // Output: 7
cout << "ackermann(3, 2) = " << ackermann(3, 2) << endl; // Output: 29
return 0;
}
Java
// Nested Recursion - Ackermann Function in Java
public class NestedRecursion {
public static int ackermann(int m, int n) {
if (m == 0) {
return n + 1;
}
if (n == 0) {
return ackermann(m - 1, 1);
}
// Nested recursion - inner call's result becomes argument
return ackermann(m - 1, ackermann(m, n - 1));
}
public static void main(String[] args) {
System.out.println("ackermann(2, 2) = " + ackermann(2, 2)); // Output: 7
System.out.println("ackermann(3, 2) = " + ackermann(3, 2)); // Output: 29
}
}
Recurrence Relations
What is a Recurrence Relation?
A recurrence relation is a mathematical equation that defines a sequence where each term is expressed in terms of preceding terms. In algorithm analysis, we use recurrence relations to describe the time complexity of recursive algorithms.
General form: T(n) = [number of subproblems] × T([subproblem size]) + [work done outside recursive calls]
Breaking Down a Recurrence
Consider Merge Sort's recurrence: T(n) = 2T(n/2) + O(n)
- 2 = number of subproblems (we split into two halves)
- T(n/2) = each subproblem is half the size
- O(n) = work to merge the two halves back together
Understanding how to write and solve these equations is crucial for analyzing recursive algorithms!
Common Recurrence Patterns
Here are the most frequently encountered recurrence patterns in algorithm analysis:
Pattern Reference Table
| Recurrence | Result | Example Algorithm | Explanation |
|---|---|---|---|
T(n) = T(n-1) + O(1) |
O(n) | Factorial, Linear Search | Constant work, n calls deep |
T(n) = T(n-1) + O(n) |
O(n²) | Selection Sort (recursive) | n + (n-1) + ... + 1 = n(n+1)/2 |
T(n) = 2T(n/2) + O(n) |
O(n log n) | Merge Sort, Quick Sort (avg) | n work at each of log n levels |
T(n) = 2T(n/2) + O(1) |
O(n) | Binary tree traversal | Visit each of n nodes once |
T(n) = T(n/2) + O(1) |
O(log n) | Binary Search | Halve problem each step |
T(n) = T(n/2) + O(n) |
O(n) | Finding median | n + n/2 + n/4 + ... = 2n |
T(n) = 2T(n-1) + O(1) |
O(2n) | Naive Fibonacci, Tower of Hanoi | Exponential - avoid if possible! |
T(n) = T(n-1) + T(n-2) |
O(fn) | Fibonacci (f ˜ 1.618) | Golden ratio growth |
Solving Recurrences: Step-by-Step Example
Let's solve T(n) = 2T(n/2) + n using the substitution method:
Step 1: Expand the recurrence (unroll it)
T(n) = 2T(n/2) + n
= 2[2T(n/4) + n/2] + n
= 4T(n/4) + n + n
= 4[2T(n/8) + n/4] + 2n
= 8T(n/8) + n + n + n
= 8T(n/8) + 3n
Step 2: Find the pattern
After k iterations: T(n) = 2? T(n/2?) + kn
Step 3: Find when to stop (base case)
When n/2? = 1, so 2? = n, meaning k = log2(n)
Step 4: Substitute back
T(n) = n × T(1) + n × log2(n)
= n × O(1) + n log n
= O(n log n) ?
Master Theorem
The Power of the Master Theorem
The Master Theorem is a "cookbook" method that lets you instantly determine the complexity of many divide-and-conquer algorithms without manual expansion. It works for recurrences of this form:
T(n) = aT(n/b) + f(n)
Where:
- a = 1 = number of subproblems
- b > 1 = factor by which input size is divided
- f(n) = cost of work done outside the recursive calls
The Three Cases
Let k = logb(a) (this represents the "work done by recursion"). Compare f(n) to nk:
| Case | Condition | Result | Intuition |
|---|---|---|---|
| 1 | f(n) = O(nc) where c < k | T(n) = T(nk) | Recursion dominates |
| 2 | f(n) = T(nk) | T(n) = T(nk log n) | Work is evenly distributed |
| 3 | f(n) = O(nc) where c > k | T(n) = T(f(n)) | Non-recursive work dominates |
Master Theorem Examples
Example 1 Merge Sort: T(n) = 2T(n/2) + O(n)
- a = 2 (two subproblems)
- b = 2 (each is half the size)
- f(n) = n (merging takes linear time)
- k = log2(2) = 1
- f(n) = n = T(n¹) = T(nk) ? Case 2!
- Result: T(n) = T(n log n)
Example 2 Binary Search: T(n) = T(n/2) + O(1)
- a = 1 (one subproblem)
- b = 2 (half the size)
- f(n) = 1 (constant comparison)
- k = log2(1) = 0
- f(n) = 1 = T(n°) = T(nk) ? Case 2!
- Result: T(n) = T(log n)
Example 3 Karatsuba Multiplication: T(n) = 3T(n/2) + O(n)
- a = 3 (three subproblems)
- b = 2 (each is half the size)
- f(n) = n (linear work to combine)
- k = log2(3) ˜ 1.585
- f(n) = n = O(n¹), and 1 < 1.585 ? Case 1!
- Result: T(n) = T(n1.585) ˜ O(n1.58)
This is better than the naive O(n²) multiplication!
Example 4 Strassen's Matrix Multiplication: T(n) = 7T(n/2) + O(n²)
- a = 7 (seven subproblems)
- b = 2 (each matrix is half the dimension)
- f(n) = n² (matrix additions)
- k = log2(7) ˜ 2.807
- f(n) = n² = O(n²), and 2 < 2.807 ? Case 1!
- Result: T(n) = T(n2.807) ˜ O(n2.81)
Better than naive O(n³) matrix multiplication!
When Master Theorem Doesn't Apply
The Master Theorem has limitations. It doesn't work for:
- Non-constant subproblem sizes: T(n) = T(n/2) + T(n/3) + n
- Subtract recurrences: T(n) = T(n-1) + n (use substitution instead)
- Non-polynomial f(n): T(n) = 2T(n/2) + n log n (special case, use extended theorem)
- a < 1 or b = 1 (violates theorem prerequisites)
For these cases, use the substitution method or recursion tree method.
Classic Recursive Problems
Fibonacci Sequence
The Fibonacci Story
The Fibonacci sequence was introduced to Western mathematics by Italian mathematician Leonardo of Pisa (nicknamed Fibonacci) in his 1202 book "Liber Abaci". He used it to model rabbit population growth:
- Month 1: Start with 1 pair of rabbits
- Month 2: Still 1 pair (too young to reproduce)
- Month 3: 2 pairs (original pair has 1 offspring)
- Month 4: 3 pairs (original pair has another offspring)
- Month 5: 5 pairs, Month 6: 8 pairs, and so on...
The sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144... where each number is the sum of the two preceding ones.
Mathematical Definition
The Fibonacci sequence is defined recursively as:
- Base cases: F(0) = 0, F(1) = 1
- Recursive case: F(n) = F(n-1) + F(n-2) for n = 2
This naturally leads to a recursive solution, but the naive approach has exponential time complexity due to repeated calculations. We'll explore three approaches to optimize it.
Approach 1: Naive Tree Recursion - O(2n)
The simplest implementation directly follows the mathematical definition, but it's extremely inefficient because it recalculates the same values many times:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) ... ... ...
Notice how fib(3) is calculated twice, fib(2) is calculated 3 times!
This exponential growth makes naive recursion impractical for n > 40.
Python - Naive Fibonacci
# Fibonacci - Naive Tree Recursion
# Time Complexity: O(2^n) - exponential
# Space Complexity: O(n) - recursion stack depth
def fib_naive(n):
"""Calculate nth Fibonacci number using naive recursion."""
# Base cases
if n <= 1:
return n
# Recursive case: F(n) = F(n-1) + F(n-2)
return fib_naive(n - 1) + fib_naive(n - 2)
# Test - only works reasonably for small n
for i in range(11):
print(f"F({i}) = {fib_naive(i)}")
# Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
C++ - Naive Fibonacci
#include <iostream>
using namespace std;
// Naive Tree Recursion - O(2^n) time
int fib_naive(int n) {
// Base cases
if (n <= 1) {
return n;
}
// Recursive case
return fib_naive(n - 1) + fib_naive(n - 2);
}
int main() {
cout << "Fibonacci Sequence (Naive):" << endl;
for (int i = 0; i <= 10; i++) {
cout << "F(" << i << ") = " << fib_naive(i) << endl;
}
// Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
return 0;
}
Java - Naive Fibonacci
public class FibonacciNaive {
// Naive Tree Recursion - O(2^n) time
public static int fibNaive(int n) {
// Base cases
if (n <= 1) {
return n;
}
// Recursive case
return fibNaive(n - 1) + fibNaive(n - 2);
}
public static void main(String[] args) {
System.out.println("Fibonacci Sequence (Naive):");
for (int i = 0; i <= 10; i++) {
System.out.println("F(" + i + ") = " + fibNaive(i));
}
// Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
}
}
Approach 2: Memoization (Top-Down DP) - O(n)
We can dramatically improve performance by storing already-computed values in a cache (memo). This technique is called memoization and is the foundation of Dynamic Programming:
Python - Memoized Fibonacci
# Fibonacci - Memoization (Top-Down DP)
# Time Complexity: O(n) - each value computed once
# Space Complexity: O(n) - memo dictionary + recursion stack
def fib_memo(n, memo=None):
"""Calculate nth Fibonacci with memoization."""
if memo is None:
memo = {}
# Check if already computed
if n in memo:
return memo[n]
# Base cases
if n <= 1:
return n
# Compute and store in memo
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
# Now we can calculate much larger values efficiently!
print(f"F(50) = {fib_memo(50)}") # 12586269025
print(f"F(100) = {fib_memo(100)}") # 354224848179261915075
C++ - Memoized Fibonacci
#include <iostream>
#include <unordered_map>
using namespace std;
// Memoization using hash map - O(n) time
unordered_map<int, long long> memo;
long long fib_memo(int n) {
// Check if already computed
if (memo.find(n) != memo.end()) {
return memo[n];
}
// Base cases
if (n <= 1) {
return n;
}
// Compute and store in memo
memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
return memo[n];
}
int main() {
cout << "Fibonacci with Memoization:" << endl;
cout << "F(50) = " << fib_memo(50) << endl; // 12586269025
// Reset memo for clean test
memo.clear();
for (int i = 0; i <= 15; i++) {
cout << "F(" << i << ") = " << fib_memo(i) << endl;
}
return 0;
}
Java - Memoized Fibonacci
import java.util.HashMap;
import java.util.Map;
public class FibonacciMemo {
// Use HashMap for memoization
private static Map<Integer, Long> memo = new HashMap<>();
public static long fibMemo(int n) {
// Check if already computed
if (memo.containsKey(n)) {
return memo.get(n);
}
// Base cases
if (n <= 1) {
return n;
}
// Compute and store in memo
long result = fibMemo(n - 1) + fibMemo(n - 2);
memo.put(n, result);
return result;
}
public static void main(String[] args) {
System.out.println("Fibonacci with Memoization:");
System.out.println("F(50) = " + fibMemo(50)); // 12586269025
// Print sequence
memo.clear();
for (int i = 0; i <= 15; i++) {
System.out.println("F(" + i + ") = " + fibMemo(i));
}
}
}
Approach 3: Iterative (Bottom-Up DP) - O(n) time, O(1) space
The most efficient approach uses iteration instead of recursion. We only need to track the last two values, reducing space complexity to O(1):
Python - Iterative Fibonacci
# Fibonacci - Iterative (Bottom-Up DP)
# Time Complexity: O(n)
# Space Complexity: O(1) - only two variables!
def fib_iterative(n):
"""Calculate nth Fibonacci iteratively - most efficient!"""
if n <= 1:
return n
# Only track the last two values
prev2, prev1 = 0, 1 # F(0), F(1)
for i in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
# Test all three approaches
print("Iterative approach (O(1) space):")
print(f"F(50) = {fib_iterative(50)}") # 12586269025
print(f"F(100) = {fib_iterative(100)}") # 354224848179261915075
C++ - Iterative Fibonacci
#include <iostream>
using namespace std;
// Iterative approach - O(n) time, O(1) space
long long fib_iterative(int n) {
if (n <= 1) {
return n;
}
// Only track the last two values
long long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
long long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
int main() {
cout << "Fibonacci Iterative (O(1) space):" << endl;
cout << "F(50) = " << fib_iterative(50) << endl; // 12586269025
// Print sequence
for (int i = 0; i <= 15; i++) {
cout << "F(" << i << ") = " << fib_iterative(i) << endl;
}
return 0;
}
Java - Iterative Fibonacci
public class FibonacciIterative {
// Iterative approach - O(n) time, O(1) space
public static long fibIterative(int n) {
if (n <= 1) {
return n;
}
// Only track the last two values
long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
public static void main(String[] args) {
System.out.println("Fibonacci Iterative (O(1) space):");
System.out.println("F(50) = " + fibIterative(50)); // 12586269025
// Print sequence
for (int i = 0; i <= 15; i++) {
System.out.println("F(" + i + ") = " + fibIterative(i));
}
}
}
Complexity Comparison
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Naive Recursion | O(2n) | O(n) | Only for teaching purposes |
| Memoization | O(n) | O(n) | When you need recursion structure |
| Iterative | O(n) | O(1) | Best for production code |
Tower of Hanoi
The Legend
The Tower of Hanoi is a mathematical puzzle invented by French mathematician Édouard Lucas in 1883. Legend has it that in a temple in Hanoi, monks have been moving 64 golden disks between three diamond pegs since the beginning of time. When they finish, the world will end!
Fun fact: With 64 disks, it would take 264 - 1 moves ˜ 18.4 quintillion moves. At one move per second, that's about 585 billion years!
Problem Statement
You have 3 pegs (rods) labeled A, B, and C. Peg A has n disks stacked in decreasing size (largest at bottom). Goal: Move all disks from A to C.
Rules:
1. Only one disk can be moved at a time
2. Each move takes the top disk from one peg and places it on another
3. No disk may be placed on top of a smaller disk
The Recursive Insight
The key insight is to think recursively: to move n disks from A to C, we need to:
- Move n-1 disks from A to B (using C as helper)
- Move the largest disk from A to C
- Move n-1 disks from B to C (using A as helper)
Visual Trace for 3 Disks
Initial State: After Move 1: After Move 2:
| | | | | | | | |
[1] | | | | | [1] | |
[2] | | [2] | | [2] | |
[3] | | [3] [1] | [3] [1] |
----- ----- ----- ----- ----- ----- ----- ----- -----
A B C A B C A B C
After Move 3: After Move 4: After Move 5:
| | | | | | | | |
| | | | | | | | [1]
[2] | | | | | | | [2]
[3] [1] [2] | [1] [2] | [1] [2]
----- ----- ----- ----- ----- ----- ----- ----- -----
A B C A B C A B C
After Move 6: After Move 7 (DONE!):
| | | | | |
| | [1] | | [1]
| | [2] | | [2]
[3] | | [3] | [3]
----- ----- ----- ----- ----- -----
A B C A B C
The 7 moves: (2^3 - 1 = 7)
1. Move disk 1 from A to C
2. Move disk 2 from A to B
3. Move disk 1 from C to B
4. Move disk 3 from A to C ? The crucial "middle" move
5. Move disk 1 from B to A
6. Move disk 2 from B to C
7. Move disk 1 from A to C
Python
# Tower of Hanoi - Classic Recursion Problem
def tower_of_hanoi(n, source, auxiliary, destination):
"""
Solve Tower of Hanoi for n disks
Time Complexity: O(2^n) - must make 2^n - 1 moves
Space Complexity: O(n) - recursion depth
"""
if n == 1:
print(f"Move disk 1 from {source} to {destination}")
return
# Step 1: Move n-1 disks from source to auxiliary (using destination as helper)
tower_of_hanoi(n - 1, source, destination, auxiliary)
# Step 2: Move the largest disk from source to destination
print(f"Move disk {n} from {source} to {destination}")
# Step 3: Move n-1 disks from auxiliary to destination (using source as helper)
tower_of_hanoi(n - 1, auxiliary, source, destination)
# Test with 3 disks
print("Tower of Hanoi with 3 disks:")
tower_of_hanoi(3, 'A', 'B', 'C')
print(f"\nTotal moves for n disks: 2^n - 1 = {2**3 - 1} moves")
C++
// Tower of Hanoi in C++
#include <iostream>
#include <string>
using namespace std;
void towerOfHanoi(int n, char source, char auxiliary, char destination) {
// Base case: only one disk
if (n == 1) {
cout << "Move disk 1 from " << source << " to " << destination << endl;
return;
}
// Step 1: Move n-1 disks from source to auxiliary
towerOfHanoi(n - 1, source, destination, auxiliary);
// Step 2: Move largest disk from source to destination
cout << "Move disk " << n << " from " << source << " to " << destination << endl;
// Step 3: Move n-1 disks from auxiliary to destination
towerOfHanoi(n - 1, auxiliary, source, destination);
}
int main() {
int n = 3;
cout << "Tower of Hanoi with " << n << " disks:" << endl;
towerOfHanoi(n, 'A', 'B', 'C');
cout << "\nTotal moves: " << (1 << n) - 1 << endl; // 2^n - 1
return 0;
}
Java
// Tower of Hanoi in Java
public class TowerOfHanoi {
public static void towerOfHanoi(int n, char source, char auxiliary, char destination) {
// Base case: only one disk
if (n == 1) {
System.out.println("Move disk 1 from " + source + " to " + destination);
return;
}
// Step 1: Move n-1 disks from source to auxiliary
towerOfHanoi(n - 1, source, destination, auxiliary);
// Step 2: Move largest disk from source to destination
System.out.println("Move disk " + n + " from " + source + " to " + destination);
// Step 3: Move n-1 disks from auxiliary to destination
towerOfHanoi(n - 1, auxiliary, source, destination);
}
public static void main(String[] args) {
int n = 3;
System.out.println("Tower of Hanoi with " + n + " disks:");
towerOfHanoi(n, 'A', 'B', 'C');
System.out.println("\nTotal moves: " + ((1 << n) - 1)); // 2^n - 1
}
}
Tower of Hanoi Complexity Analysis
Recurrence Relation: T(n) = 2T(n-1) + 1
Solving:
- T(1) = 1
- T(2) = 2(1) + 1 = 3
- T(3) = 2(3) + 1 = 7
- T(n) = 2n - 1
Time Complexity: O(2n) — exponential, unavoidable for this problem
Space Complexity: O(n) — recursion stack depth
Power Function
The Exponentiation Problem
Computing xn (x raised to the power n) is a fundamental operation in mathematics and computing. While we could simply multiply x by itself n times (O(n) time), there's a brilliant technique called exponentiation by squaring that reduces this to O(log n)!
Key insight: x8 = ((x2)2)2 — only 3 multiplications instead of 7!
Mathematical Basis
Exponentiation by squaring exploits two properties:
- Even exponent: xn = (xn/2)2
- Odd exponent: xn = x × xn-1
This halves the problem size at each step, giving us O(log n) time complexity!
Approach 1: Naive Recursion - O(n)
The straightforward approach multiplies the base n times:
Python - Naive Power
# Power Function - Naive Recursion
# Time Complexity: O(n) - makes n recursive calls
# Space Complexity: O(n) - recursion stack depth
def power_naive(base, exp):
"""Calculate base^exp using naive recursion."""
# Base case: x^0 = 1
if exp == 0:
return 1
# Recursive case: x^n = x * x^(n-1)
return base * power_naive(base, exp - 1)
# Test
print(f"2^10 = {power_naive(2, 10)}") # 1024
print(f"3^5 = {power_naive(3, 5)}") # 243
C++ - Naive Power
#include <iostream>
using namespace std;
// Naive recursion - O(n) time
long long power_naive(int base, int exp) {
// Base case: x^0 = 1
if (exp == 0) {
return 1;
}
// Recursive case: x^n = x * x^(n-1)
return base * power_naive(base, exp - 1);
}
int main() {
cout << "2^10 = " << power_naive(2, 10) << endl; // 1024
cout << "3^5 = " << power_naive(3, 5) << endl; // 243
return 0;
}
Java - Naive Power
public class PowerNaive {
// Naive recursion - O(n) time
public static long powerNaive(int base, int exp) {
// Base case: x^0 = 1
if (exp == 0) {
return 1;
}
// Recursive case: x^n = x * x^(n-1)
return base * powerNaive(base, exp - 1);
}
public static void main(String[] args) {
System.out.println("2^10 = " + powerNaive(2, 10)); // 1024
System.out.println("3^5 = " + powerNaive(3, 5)); // 243
}
}
Approach 2: Fast Power (Exponentiation by Squaring) - O(log n)
The optimized approach halves the exponent at each step:
Computing 2^10 using Fast Power:
2^10 ? exp is even ? (2^5)^2
2^5 ? exp is odd ? 2 × 2^4
2^4 ? exp is even ? (2^2)^2
2^2 ? exp is even ? (2^1)^2
2^1 ? exp is odd ? 2 × 2^0
2^0 = 1
Unrolling:
2^0 = 1
2^1 = 2 × 1 = 2
2^2 = 2^2 = 4
2^4 = 4^2 = 16
2^5 = 2 × 16 = 32
2^10 = 32^2 = 1024
Only 4 multiplications instead of 10!
Python - Fast Power
# Power Function - Exponentiation by Squaring
# Time Complexity: O(log n) - halves problem each step
# Space Complexity: O(log n) - recursion stack depth
def power_fast(base, exp):
"""
Fast exponentiation using squaring technique.
x^n = (x^(n/2))^2 if n is even
x^n = x * x^(n-1) if n is odd
"""
# Base case
if exp == 0:
return 1
# Even exponent: square the half-power
if exp % 2 == 0:
half = power_fast(base, exp // 2)
return half * half
# Odd exponent: reduce by 1 and multiply by base
else:
return base * power_fast(base, exp - 1)
# Test - works efficiently for large exponents
print(f"2^10 = {power_fast(2, 10)}") # 1024
print(f"2^30 = {power_fast(2, 30)}") # 1073741824
print(f"3^20 = {power_fast(3, 20)}") # 3486784401
C++ - Fast Power
#include <iostream>
using namespace std;
// Fast power - O(log n) time
long long power_fast(int base, int exp) {
// Base case
if (exp == 0) {
return 1;
}
// Even exponent: square the half-power
if (exp % 2 == 0) {
long long half = power_fast(base, exp / 2);
return half * half;
}
// Odd exponent: reduce by 1 and multiply by base
return base * power_fast(base, exp - 1);
}
int main() {
cout << "2^10 = " << power_fast(2, 10) << endl; // 1024
cout << "2^30 = " << power_fast(2, 30) << endl; // 1073741824
cout << "3^20 = " << power_fast(3, 20) << endl; // 3486784401
return 0;
}
Java - Fast Power
public class PowerFast {
// Fast power - O(log n) time
public static long powerFast(int base, int exp) {
// Base case
if (exp == 0) {
return 1;
}
// Even exponent: square the half-power
if (exp % 2 == 0) {
long half = powerFast(base, exp / 2);
return half * half;
}
// Odd exponent: reduce by 1 and multiply by base
return base * powerFast(base, exp - 1);
}
public static void main(String[] args) {
System.out.println("2^10 = " + powerFast(2, 10)); // 1024
System.out.println("2^30 = " + powerFast(2, 30)); // 1073741824
System.out.println("3^20 = " + powerFast(3, 20)); // 3486784401
}
}
Approach 3: Iterative Fast Power - O(log n) time, O(1) space
We can eliminate the recursion stack using an iterative approach with bit manipulation:
Python - Iterative Fast Power
# Power Function - Iterative with Bit Manipulation
# Time Complexity: O(log n)
# Space Complexity: O(1) - no recursion stack!
def power_iterative(base, exp):
"""
Iterative fast power using binary representation.
Example: 2^13 = 2^(1101 in binary) = 2^8 * 2^4 * 2^1
"""
result = 1
while exp > 0:
# If current bit is 1, multiply result by base
if exp % 2 == 1: # or: if exp & 1:
result *= base
# Square the base for next bit
base *= base
# Move to next bit
exp //= 2 # or: exp >>= 1
return result
# Test
print(f"2^10 iterative = {power_iterative(2, 10)}") # 1024
print(f"3^13 iterative = {power_iterative(3, 13)}") # 1594323
C++ - Iterative Fast Power
#include <iostream>
using namespace std;
// Iterative fast power - O(log n) time, O(1) space
long long power_iterative(int base, int exp) {
long long result = 1;
long long b = base; // Use long long to prevent overflow
while (exp > 0) {
// If current bit is 1, multiply result by base
if (exp & 1) { // Same as exp % 2 == 1
result *= b;
}
// Square the base for next bit
b *= b;
// Move to next bit
exp >>= 1; // Same as exp /= 2
}
return result;
}
int main() {
cout << "2^10 iterative = " << power_iterative(2, 10) << endl; // 1024
cout << "3^13 iterative = " << power_iterative(3, 13) << endl; // 1594323
return 0;
}
Java - Iterative Fast Power
public class PowerIterative {
// Iterative fast power - O(log n) time, O(1) space
public static long powerIterative(int base, int exp) {
long result = 1;
long b = base; // Use long to prevent overflow
while (exp > 0) {
// If current bit is 1, multiply result by base
if ((exp & 1) == 1) { // Same as exp % 2 == 1
result *= b;
}
// Square the base for next bit
b *= b;
// Move to next bit
exp >>= 1; // Same as exp /= 2
}
return result;
}
public static void main(String[] args) {
System.out.println("2^10 iterative = " + powerIterative(2, 10)); // 1024
System.out.println("3^13 iterative = " + powerIterative(3, 13)); // 1594323
}
}
Complexity Comparison
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Naive | O(n) | O(n) | Small exponents, teaching |
| Fast (Recursive) | O(log n) | O(log n) | When recursion is preferred |
| Fast (Iterative) | O(log n) | O(1) | Best for production code |
Pro tip: For modular exponentiation (xn mod m), used in cryptography, just add % mod after each multiplication!
LeetCode Practice Problems
Practice these classic recursion problems to solidify your understanding. Each problem is presented with solutions in all three languages.
Easy 509. Fibonacci Number
Problem: Given n, calculate F(n), where F(n) is the nth Fibonacci number. F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2).
Python
# LeetCode 509 - Fibonacci Number
# Time: O(n) with memoization, Space: O(n)
class Solution:
def fib(self, n: int) -> int:
memo = {0: 0, 1: 1}
def helper(n):
if n in memo:
return memo[n]
memo[n] = helper(n - 1) + helper(n - 2)
return memo[n]
return helper(n)
# Test
sol = Solution()
print(sol.fib(10)) # Output: 55
print(sol.fib(20)) # Output: 6765
C++
#include <iostream>
#include <unordered_map>
using namespace std;
// LeetCode 509 - Fibonacci Number
// Time: O(n) with memoization
class Solution {
private:
unordered_map<int, int> memo;
int helper(int n) {
if (memo.find(n) != memo.end()) {
return memo[n];
}
memo[n] = helper(n - 1) + helper(n - 2);
return memo[n];
}
public:
int fib(int n) {
memo[0] = 0;
memo[1] = 1;
return helper(n);
}
};
int main() {
Solution sol;
cout << sol.fib(10) << endl; // Output: 55
return 0;
}
Java
import java.util.HashMap;
import java.util.Map;
// LeetCode 509 - Fibonacci Number
// Time: O(n) with memoization
class Solution {
private Map<Integer, Integer> memo = new HashMap<>();
public int fib(int n) {
memo.put(0, 0);
memo.put(1, 1);
return helper(n);
}
private int helper(int n) {
if (memo.containsKey(n)) {
return memo.get(n);
}
int result = helper(n - 1) + helper(n - 2);
memo.put(n, result);
return result;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.fib(10)); // Output: 55
}
}
Easy 206. Reverse Linked List
Problem: Given the head of a singly linked list, reverse the list, and return the reversed list.
Key Insight: The recursive approach first reverses the rest of the list, then fixes the links by making the next node point back to current node.
Python
# LeetCode 206 - Reverse Linked List (Recursive)
# Time: O(n), Space: O(n) for call stack
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# Base case: empty list or single node
if not head or not head.next:
return head
# Recursively reverse the rest of the list
new_head = self.reverseList(head.next)
# Fix the links: make next node point back to current
head.next.next = head
head.next = None # Current node is now the tail
return new_head
# Helper to create and print list
def create_list(arr):
if not arr:
return None
head = ListNode(arr[0])
curr = head
for val in arr[1:]:
curr.next = ListNode(val)
curr = curr.next
return head
def print_list(head):
vals = []
while head:
vals.append(str(head.val))
head = head.next
print(" -> ".join(vals))
# Test
sol = Solution()
head = create_list([1, 2, 3, 4, 5])
print("Original:", end=" ")
print_list(head)
reversed_head = sol.reverseList(head)
print("Reversed:", end=" ")
print_list(reversed_head)
# Output: 5 -> 4 -> 3 -> 2 -> 1
C++
#include <iostream>
using namespace std;
// LeetCode 206 - Reverse Linked List
// Time: O(n), Space: O(n) for call stack
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// Base case: empty list or single node
if (!head || !head->next) {
return head;
}
// Recursively reverse the rest
ListNode* newHead = reverseList(head->next);
// Fix the links
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
// Helper function to print list
void printList(ListNode* head) {
while (head) {
cout << head->val;
if (head->next) cout << " -> ";
head = head->next;
}
cout << endl;
}
int main() {
// Create list: 1 -> 2 -> 3 -> 4 -> 5
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
cout << "Original: ";
printList(head);
Solution sol;
ListNode* reversed = sol.reverseList(head);
cout << "Reversed: ";
printList(reversed);
// Output: 5 -> 4 -> 3 -> 2 -> 1
return 0;
}
Java
// LeetCode 206 - Reverse Linked List
// Time: O(n), Space: O(n) for call stack
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
class Solution {
public ListNode reverseList(ListNode head) {
// Base case: empty list or single node
if (head == null || head.next == null) {
return head;
}
// Recursively reverse the rest
ListNode newHead = reverseList(head.next);
// Fix the links
head.next.next = head;
head.next = null;
return newHead;
}
// Helper to print list
public void printList(ListNode head) {
while (head != null) {
System.out.print(head.val);
if (head.next != null) System.out.print(" -> ");
head = head.next;
}
System.out.println();
}
public static void main(String[] args) {
// Create list: 1 -> 2 -> 3 -> 4 -> 5
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
Solution sol = new Solution();
System.out.print("Original: ");
sol.printList(head);
ListNode reversed = sol.reverseList(head);
System.out.print("Reversed: ");
sol.printList(reversed);
// Output: 5 -> 4 -> 3 -> 2 -> 1
}
}
Medium 50. Pow(x, n)
Problem: Implement pow(x, n), which calculates x raised to the power n.
Key Insight: Use fast exponentiation. Handle negative exponents by computing 1/x and using positive n.
Python
# LeetCode 50 - Pow(x, n)
# Time: O(log n), Space: O(log n) for recursion stack
class Solution:
def myPow(self, x: float, n: int) -> float:
# Handle negative exponent
if n < 0:
x = 1 / x
n = -n
return self.fastPow(x, n)
def fastPow(self, x: float, n: int) -> float:
# Base case
if n == 0:
return 1.0
# Use fast exponentiation
if n % 2 == 0:
half = self.fastPow(x, n // 2)
return half * half
else:
return x * self.fastPow(x, n - 1)
# Test
sol = Solution()
print(f"2^10 = {sol.myPow(2, 10)}") # 1024.0
print(f"2^(-2) = {sol.myPow(2, -2)}") # 0.25
print(f"2.1^3 = {sol.myPow(2.1, 3)}") # 9.261
C++
#include <iostream>
using namespace std;
// LeetCode 50 - Pow(x, n)
// Time: O(log n), Space: O(log n)
class Solution {
public:
double myPow(double x, int n) {
// Use long long to handle INT_MIN case
long long N = n;
// Handle negative exponent
if (N < 0) {
x = 1 / x;
N = -N;
}
return fastPow(x, N);
}
private:
double fastPow(double x, long long n) {
// Base case
if (n == 0) {
return 1.0;
}
// Fast exponentiation
if (n % 2 == 0) {
double half = fastPow(x, n / 2);
return half * half;
} else {
return x * fastPow(x, n - 1);
}
}
};
int main() {
Solution sol;
cout << "2^10 = " << sol.myPow(2, 10) << endl; // 1024
cout << "2^(-2) = " << sol.myPow(2, -2) << endl; // 0.25
cout << "2.1^3 = " << sol.myPow(2.1, 3) << endl; // 9.261
return 0;
}
Java
// LeetCode 50 - Pow(x, n)
// Time: O(log n), Space: O(log n)
class Solution {
public double myPow(double x, int n) {
// Use long to handle Integer.MIN_VALUE case
long N = n;
// Handle negative exponent
if (N < 0) {
x = 1 / x;
N = -N;
}
return fastPow(x, N);
}
private double fastPow(double x, long n) {
// Base case
if (n == 0) {
return 1.0;
}
// Fast exponentiation
if (n % 2 == 0) {
double half = fastPow(x, n / 2);
return half * half;
} else {
return x * fastPow(x, n - 1);
}
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println("2^10 = " + sol.myPow(2, 10)); // 1024.0
System.out.println("2^(-2) = " + sol.myPow(2, -2)); // 0.25
System.out.println("2.1^3 = " + sol.myPow(2.1, 3)); // 9.261
}
}
Easy 21. Merge Two Sorted Lists
Problem: Merge two sorted linked lists and return it as a sorted list.
Key Insight: Compare heads, take the smaller one, then recursively merge the rest.
Python
# LeetCode 21 - Merge Two Sorted Lists
# Time: O(n + m), Space: O(n + m) for recursion stack
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
# Base cases
if not l1:
return l2
if not l2:
return l1
# Recursive case: take smaller head, merge rest
if l1.val <= l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
# Test
def create_list(arr):
if not arr:
return None
head = ListNode(arr[0])
curr = head
for val in arr[1:]:
curr.next = ListNode(val)
curr = curr.next
return head
def print_list(head):
vals = []
while head:
vals.append(str(head.val))
head = head.next
print(" -> ".join(vals))
sol = Solution()
l1 = create_list([1, 2, 4])
l2 = create_list([1, 3, 4])
merged = sol.mergeTwoLists(l1, l2)
print_list(merged) # 1 -> 1 -> 2 -> 3 -> 4 -> 4
C++
#include <iostream>
using namespace std;
// LeetCode 21 - Merge Two Sorted Lists
// Time: O(n + m), Space: O(n + m)
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
// Base cases
if (!l1) return l2;
if (!l2) return l1;
// Take smaller head, merge rest
if (l1->val <= l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
void printList(ListNode* head) {
while (head) {
cout << head->val;
if (head->next) cout << " -> ";
head = head->next;
}
cout << endl;
}
int main() {
// Create l1: 1 -> 2 -> 4
ListNode* l1 = new ListNode(1);
l1->next = new ListNode(2);
l1->next->next = new ListNode(4);
// Create l2: 1 -> 3 -> 4
ListNode* l2 = new ListNode(1);
l2->next = new ListNode(3);
l2->next->next = new ListNode(4);
Solution sol;
ListNode* merged = sol.mergeTwoLists(l1, l2);
printList(merged); // 1 -> 1 -> 2 -> 3 -> 4 -> 4
return 0;
}
Java
// LeetCode 21 - Merge Two Sorted Lists
// Time: O(n + m), Space: O(n + m)
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// Base cases
if (l1 == null) return l2;
if (l2 == null) return l1;
// Take smaller head, merge rest
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
public void printList(ListNode head) {
while (head != null) {
System.out.print(head.val);
if (head.next != null) System.out.print(" -> ");
head = head.next;
}
System.out.println();
}
public static void main(String[] args) {
// Create l1: 1 -> 2 -> 4
ListNode l1 = new ListNode(1);
l1.next = new ListNode(2);
l1.next.next = new ListNode(4);
// Create l2: 1 -> 3 -> 4
ListNode l2 = new ListNode(1);
l2.next = new ListNode(3);
l2.next.next = new ListNode(4);
Solution sol = new Solution();
ListNode merged = sol.mergeTwoLists(l1, l2);
sol.printList(merged); // 1 -> 1 -> 2 -> 3 -> 4 -> 4
}
}
More Practice Problems
Try these additional recursion problems on LeetCode:
- Easy: 70. Climbing Stairs, 344. Reverse String, 226. Invert Binary Tree
- Medium: 779. K-th Symbol in Grammar, 894. All Possible Full Binary Trees, 247. Strobogrammatic Number II
- Hard: 761. Special Binary String, 44. Wildcard Matching, 10. Regular Expression Matching