Data Structure And AlgorithmsUnit 39 min read
Recursion & Backtracking: Definitions, Factorial, Fibonacci, Tail Recursion, Backtracking Basics
Unit 3 of Data Structure And Algorithms covers recursion (base case, recursive case, stack frames), tail recursion, backtracking (permutation, N-Queens), and their applications in problem-solving with visual traces of call stacks and state changes.
TAKEAWAYS:
- Recursion breaks problems into smaller subproblems using a base case and recursive case, visualized via call stack growth.
- Tail recursion optimizes stack usage by moving computations to the base case, but compilers must support it.
- Backtracking systematically explores all possibilities by undoing choices (e.g., permutations, maze solving).
- Time complexity of recursive algorithms often follows or patterns, depending on branching.
- Real-world uses include eSewa’s transaction validation (recursive state checks) and Pathao’s route optimization (backtracking).
- Always trace recursive calls step-by-step to avoid stack overflow or infinite loops.
Core Concepts: Recursion
Recursion is a technique where a function calls itself to solve smaller instances of the same problem. It consists of two key parts:
- Base Case: The simplest instance that can be solved directly (terminates recursion).
- Recursive Case: The function calls itself with a modified input, moving toward the base case.
How Recursion Works: Call Stack Visualization
graph TD
A["factorial(3)"] --> B["factorial(2) + 3*factorial(2)"]
B --> C["factorial(1) + 2*factorial(1)"]
C --> D["factorial(0) = 1"]
D -->|"returns"| C
C -->|"returns"| B
B -->|"returns"| AExample: Factorial of 3
The call stack grows until the base case (factorial(0) = 1) is reached. Each frame holds:
- Function name (
factorial) - Parameters (
n) - Return address (next instruction)
- Local variables (e.g., intermediate results).
Trace Table for factorial(3)
| Step | Call Stack (Top to Bottom) | Action |
|---|---|---|
| 1 | factorial(3) |
Calls factorial(2) |
| 2 | factorial(2), factorial(3) |
Calls factorial(1) |
| 3 | factorial(1), factorial(2), factorial(3) |
Calls factorial(0) |
| 4 | factorial(0), ... |
Returns 1 (base case) |
| 5 | factorial(1), ... |
Computes 1 * 1 = 1 |
| 6 | factorial(2), ... |
Computes 2 * 1 = 2 |
| 7 | factorial(3) |
Computes 3 * 2 = 6 |
Code Example (Python)
def factorial(n):
if n == 0: # Base case
return 1
else:
return n * factorial(n - 1) # Recursive case
Tail Recursion: Optimization
Definition: A recursive call is tail-recursive if it is the last operation in the function. Compilers can optimize this to reuse the stack frame, avoiding overflow.
Example: Tail-Recursive Factorial
def factorial_tail(n, accumulator=1):
if n == 0:
return accumulator
else:
return factorial_tail(n - 1, n * accumulator) # Tail call
Trace Table
| Step | Call Stack | Action |
|---|---|---|
| 1 | factorial_tail(3, 1) |
Calls factorial_tail(2, 3) |
| 2 | factorial_tail(2, 3) |
Calls factorial_tail(1, 6) |
| 3 | factorial_tail(1, 6) |
Calls factorial_tail(0, 6) |
| 4 | factorial_tail(0, 6) |
Returns 6 (no new frame) |
Advantages:
- Constant stack space (no growth).
- Faster execution (no pending operations).
Disadvantages:
- Not all languages optimize tail calls (e.g., Python does not).
Backtracking: Exploring Possibilities
Backtracking is a brute-force search that:
- Makes a choice.
- Recursively explores consequences.
- Undoes choices if they lead to dead ends.
Example: Generating Permutations
graph TD
A["Permute([1,2,3])"] --> B["Fix 1, Permute([2,3])"]
B --> C["Fix 1,2, Permute([3])"]
C --> D["Fix 1,2,3 → [1,2,3]"]
C --> E["Fix 1,3, Permute([2])"]
E --> F["Fix 1,3,2 → [1,3,2]"]
B --> G["Fix 1,3,2 → [1,3,2]"]
A --> H["Fix 2, Permute([1,3])"]
H --> I["Fix 2,1,3 → [2,1,3]"]
H --> J["Fix 2,3,1 → [2,3,1]"]
A --> K["Fix 3, Permute([1,2])"]
K --> L["Fix 3,1,2 → [3,1,2]"]
K --> M["Fix 3,2,1 → [3,2,1]"]Code Example (Python)
def permute(nums):
def backtrack(start, end):
if start == end:
result.append(nums.copy())
else:
for i in range(start, end):
nums[start], nums[i] = nums[i], nums[start] # Swap
backtrack(start + 1, end) # Recurse
nums[start], nums[i] = nums[i], nums[start] # Undo
result = []
backtrack(0, len(nums))
return result
Trace for permute([1,2])
| Step | State of nums |
Action |
|---|---|---|
| 1 | [1, 2] |
Swap 1 and 2 → [2, 1] |
| 2 | [2, 1] |
Base case: add [2, 1] |
| 3 | [2, 1] |
Undo swap → [1, 2] |
| 4 | [1, 2] |
Base case: add [1, 2] |
## In the Real World
eSewa Transaction Validation
- Idea Used: Recursion to validate nested transaction dependencies (e.g., a bill payment may trigger a fine payment, which may require another approval).
- How: Each transaction is checked recursively until all dependencies resolve to a base case (e.g., "user has sufficient balance").
Pathao’s Route Optimization
- Idea Used: Backtracking to explore all possible delivery routes and select the fastest one without traffic jams.
- How: The algorithm tries every possible path, backtracks when a route hits a dead end (e.g., a closed road), and keeps the shortest valid path.
Nepal Rastra Bank’s Loan Amortization
- Idea Used: Recursion to calculate monthly payments for loans with compound interest.
- Worked Example:
- Problem: A loan of ₹1,000,000 at 10% annual interest, repaid in 5 years.
- Recursive Formula: where , , .
- Trace: The bank’s system recursively computes each month’s principal and interest, reducing the loan balance until it reaches zero.
Comparison Table: Recursion vs. Iteration
| Feature | Recursion | Iteration |
|---|---|---|
| Stack Usage | Grows with depth (risk of overflow) | Constant (no extra stack) |
| Readability | Often more intuitive | Can be verbose for complex logic |
| Performance | Slower (function call overhead) | Faster (direct loops) |
| Use Case | Problems with self-similarity | Problems with linear progression |
| Tail Recursion | Optimizable (if supported) | N/A |
Common Pitfalls and Exam Tips
Infinite Recursion
- Cause: Missing or incorrect base case.
- Fix: Always verify the base case covers the smallest input.
- Example:
factorial(-1)would recurse infinitely without a check forn < 0.
Stack Overflow
- Cause: Deep recursion (e.g.,
fibonacci(1000)without memoization). - Fix: Use tail recursion or iteration for large
n.
- Cause: Deep recursion (e.g.,
Backtracking Inefficiency
- Cause: Exploring all possibilities without pruning.
- Fix: Add constraints early (e.g., skip permutations with duplicate elements).
Tail Recursion Misconception
- Mistake: Assuming all recursive functions are tail-recursive.
- Truth: Only the last call in the function qualifies. Example:
def bad_tail(n): if n == 0: return 1 return n + bad_tail(n - 1) # Not tail-recursive (pending `n + ...`)
## Exam Tip
For Algorithm Questions:
- Always write the base case first, then the recursive case.
- Use pseudocode if coding isn’t required, but show the recursive call clearly.
- Trace 2–3 steps in your answer to demonstrate understanding (e.g.,
factorial(4)).
For Backtracking Questions:
- Draw a decision tree (like the permutation example above) to show how choices are explored.
- Highlight the undo step (e.g., swapping back in permutations).
For Tail Recursion:
- Explicitly state whether the function is tail-recursive and why.
- If asked to convert to tail recursion, show the accumulator variable.
Time Complexity:
- Recursive algorithms often have time (e.g., factorial) or (e.g., permutations with branching factor
b). - Memoization can reduce to (e.g., Fibonacci).
- Recursive algorithms often have time (e.g., factorial) or (e.g., permutations with branching factor
Visual Summary
mindmap
root((Recursion & Backtracking))
Concepts
Recursion
Base Case
Recursive Case
Call Stack
Tail Recursion
Optimization
Accumulator
Applications
Factorial
Fibonacci
Permutations
N-Queens
Real-World
eSewa
Pathao
Bank Loans
Pitfalls
Infinite Recursion
Stack Overflow
Inefficient BacktrackingBased on the TU BITM syllabus for Data Structure And Algorithms (IT238), unit 3.
Discussion
Loading…