IT238 Data Structure and Algorithms

Data Structure and AlgorithmsUnit 311 min read

Recursion and Backtracking: Techniques, Analysis, and Applications

Unit 3 of Data Structure and Algorithms explores recursion (self-referential function calls) and backtracking (systematic trial-and-error), covering definitions, mechanics, time/space complexity, and real-world applications in pathfinding, combinatorial problems, and divide-and-conquer algorithms.

TAKEAWAYS:

  • Recursion replaces loops by breaking problems into smaller subproblems with a base case and recursive case, but risks stack overflow if not bounded.
  • Backtracking explores all possible solutions incrementally, undoing ("backtracking") invalid choices—ideal for puzzles like the N-Queens problem.
  • Time complexity of recursive algorithms often follows the recurrence relation , solvable via the Master Theorem.
  • Real-world uses include Pathao’s route optimization (recursive path splitting), Nepal Rastra Bank’s loan amortization (recursive interest calculation), and eSewa’s transaction validation (backtracking for fraud checks).
  • Tail recursion can optimize stack usage, but most languages (except Python) require explicit tail-call optimization.
  • Debugging recursion requires tracing the call stack and verifying base case termination.

Core Concepts: Recursion

1. Definition and Structure

Recursion is a programming technique where a function calls itself to solve smaller instances of the same problem. It consists of:

  • Base Case: The simplest, non-recursive solution (termination condition).
  • Recursive Case: The function calls itself with a modified input, moving toward the base case.
flowchart TD
    A["factorial(n)"] -->|"n == 0?"| B["Base Case: return 1"]
    A -->|"n > 0"| C["Recursive Case: return n * factorial(n-1)"]
    C --> A

2. How It Works: Factorial Example

Problem: Compute . Trace Table:

Call Stack (Top to Bottom) Operation Result
factorial(5)
factorial(4)
factorial(3)
factorial(2)
factorial(1)
factorial(0) Base Case: return 1 1
Unwinding: 1
2
6
24
120

Code:

def factorial(n):
    if n == 0:          # Base case
        return 1
    else:               # Recursive case
        return n * factorial(n - 1)

3. Time and Space Complexity

  • Time: for linear recursion (e.g., factorial).
  • Space: due to the call stack (each recursive call adds a stack frame).
  • Optimization: Tail recursion (where the recursive call is the last operation) can reduce space to if the compiler optimizes it (rare in Python).

Example: Fibonacci sequence (inefficient recursive version).

def fib(n):
    if n <= 1:          # Base case
        return n
    else:               # Recursive case
        return fib(n-1) + fib(n-2)

Trace for fib(4):

fib(4) → fib(3) + fib(2)
       → (fib(2) + fib(1)) + (fib(1) + fib(0))
       → ((fib(1) + fib(0)) + 1) + (1 + 0)
       → ((1 + 0) + 1) + (1 + 0) = 3 + 1 = 4

Time Complexity: (exponential) due to repeated calculations.


Backtracking: Systematic Trial and Error

1. Definition

Backtracking is a brute-force search algorithm that:

  1. Builds candidates incrementally.
  2. Abandons ("backtracks") a candidate as soon as it determines it cannot lead to a valid solution.
  3. Uses recursion to explore all possibilities.

Key Components:

  • State: Current partial solution.
  • Constraints: Rules to validate the state.
  • Choices: Possible next steps.

2. Example: N-Queens Problem

Problem: Place queens on an chessboard so no two queens threaten each other. Visualization for :

![4 queens puzzle solution](/media/3ce1075ccf765003de95.png "A valid arrangement where no two queens share a row, column, or diagonal. (Image: Encik Tekateki, CC BY-SA 4.0, via Wikimedia Commons)")

Algorithm Steps:

  1. Place a queen in the first row.
  2. Move to the next row and try all safe columns.
  3. If no safe column exists, backtrack to the previous row and try the next column.
flowchart TD
    A["Start: Row 0"] --> B["Place queen in column 0"]
    B --> C["Check constraints"]
    C -->|"Valid"| D["Move to Row 1"]
    C -->|"Invalid"| E["Backtrack: Try column 1"]
    D --> F["Place queen in column 2"]
    F --> G["Check constraints"]
    G -->|"Valid"| H["Move to Row 2"]
    G -->|"Invalid"| I["Backtrack: Try column 3"]
    H --> J["Place queen in column 1"]
    J --> K["Check constraints"]
    K -->|"Valid"| L["Move to Row 3"]
    L --> M["Place queen in column 3"]
    M --> N["All queens placed: Solution found"]

Code:

def solve_n_queens(n):
    def is_safe(board, row, col):
        # Check column and diagonals
        for i in range(row):
            if board[i] == col or abs(board[i] - col) == abs(i - row):
                return False
        return True

    def backtrack(row, board, solutions):
        if row == n:
            solutions.append(board.copy())
            return
        for col in range(n):
            if is_safe(board, row, col):
                board[row] = col
                backtrack(row + 1, board, solutions)
                board[row] = -1  # Backtrack

    solutions = []
    backtrack(0, [-1] * n, solutions)
    return solutions

Trace for :

Step Row Column Board State (Row: Column) Action
1 0 0 [0, -1, -1, -1] Place queen at (0,0)
2 1 2 [0, 2, -1, -1] Place queen at (1,2)
3 2 1 [0, 2, 1, -1] Place queen at (2,1)
4 3 3 [0, 2, 1, 3] Solution found
5 3 0 [0, 2, 1, 0] Invalid (conflict) → Backtrack

Real-World Applications

1. Pathao’s Route Optimization

  • Idea Used: Recursive path splitting (divide-and-conquer).
  • How: Pathao’s algorithm recursively divides the map into quadrants, checking the shortest path in each sub-region before combining results. This mirrors the recursive maze-solving approach but optimized for real-time GPS data.
  • Example: For a rider in Kathmandu going from Thapathali to Lakankhel, the algorithm might:
    1. Split the map into 4 quadrants.
    2. Recursively compute the shortest path in each quadrant.
    3. Combine the results to find the global shortest path.

2. Nepal Rastra Bank’s Loan Amortization

  • Idea Used: Recursive calculation of loan installments.
  • How: The bank uses recursion to break down a loan into monthly payments, where each recursive call computes the remaining principal after interest. The base case is when the loan is fully repaid.
  • Example: For a ₹1,000,000 loan at 10% annual interest over 5 years:
    • Recursive Function: calculate_payment(remaining_principal, months_left).
    • Base Case: If months_left == 0, return remaining_principal.
    • Recursive Case: Compute monthly interest, subtract payment, and recurse.

3. eSewa’s Transaction Validation

  • Idea Used: Backtracking for fraud detection.
  • How: When a user pays for an electricity bill via eSewa, the system uses backtracking to:
    1. Validate the transaction step-by-step (e.g., check account balance, NTC bill status).
    2. If any step fails (e.g., insufficient funds), it backtracks to the previous step and tries an alternative (e.g., deduct from a linked bank account).
    • Example: If a user’s Khalti balance is insufficient but their Ncell wallet has funds, eSewa backtracks to the payment method selection.

Comparison: Recursion vs. Iteration

Feature Recursion Iteration
Structure Self-referential function calls Loops (for/while)
Readability Often more intuitive for divide-and-conquer Can be verbose for complex logic
Stack Usage High (each call adds a stack frame) Low (constant stack space)
Termination Relies on base case Relies on loop condition
Use Cases Tree/graph traversals, backtracking Linear processing, simple loops

Common Pitfalls and Debugging

1. Infinite Recursion

Cause: Missing or incorrect base case. Example: Fibonacci without memoization recalculates the same values repeatedly. Fix: Add a base case or use memoization (caching results).

2. Stack Overflow

Cause: Recursion depth exceeds the call stack limit (e.g., factorial(10000) in Python). Fix:

  • Use iteration for large inputs.
  • Optimize with tail recursion (if supported).

3. Inefficient Recursion

Cause: Exponential time complexity (e.g., naive Fibonacci). Fix: Use dynamic programming or memoization.

Example: Memoized Fibonacci

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

Time Complexity: (linear) with memoization.


Advanced: Divide and Conquer

Recursion is the backbone of divide-and-conquer algorithms, which:

  1. Divide the problem into smaller subproblems.
  2. Conquer each subproblem recursively.
  3. Combine the results.

Example: Merge Sort

flowchart TD
    A["MergeSort(arr)"] --> B["Divide: Split arr into left/right halves"]
    B --> C["MergeSort(left)"]
    B --> D["MergeSort(right)"]
    C --> E["Base Case: Return if len(arr) <= 1"]
    D --> E
    E --> F["Merge(left, right)"]

Code:

def merge_sort(arr):
    if len(arr) > 1:
        mid = len(arr) // 2
        left = merge_sort(arr[:mid])
        right = merge_sort(arr[mid:])
        return merge(left, right)
    return arr

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

Trace for [38, 27, 43, 3, 9, 82, 10]:

Divide: [38, 27, 43] | [3, 9, 82, 10]
Recurse:
  Left: [38] | [27, 43] → [27, 38, 43]
  Right: [3, 9] | [82, 10] → [3, 9, 10, 82]
Merge: [27, 38, 43] + [3, 9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]

Exam Tip

  1. Always show the base case and recursive case in your answers. Examiners check for completeness.
  2. Trace the call stack for recursive functions (e.g., draw a table like the factorial example above).
  3. Compare time/space complexity of recursive vs. iterative solutions (e.g., factorial with loop vs. recursion).
  4. For backtracking, explain:
    • How the algorithm builds and abandons partial solutions.
    • The constraints used to prune invalid paths.
  5. Real-world questions often ask:
    • "How would you use recursion to optimize [X]?" (e.g., a Daraz order processing system).
    • "Explain backtracking with an example from [Y]." (e.g., NTC’s load-shedding schedule optimization).
  6. Avoid common mistakes:
    • Forgetting to backtrack (e.g., not resetting the board in N-Queens).
    • Incorrect base case (e.g., fib(1) = 1 + 1 instead of fib(1) = 1).

Pro Tip: Practice implementing recursion for:

  • Tree traversals (in-unit 7).
  • Graph algorithms (in-unit 9, e.g., DFS).
  • Dynamic programming (e.g., knapsack problem).

Based on the TU BIM syllabus for Data Structure and Algorithms (IT238), unit 3.

Discussion

Loading…