Real-world examples of recursion

Learn real-world examples of recursion with practical examples, diagrams, and best practices. Covers recursion development techniques with visual explanations.

Real-World Recursion: Practical Examples in Programming

Hero image for Real-world examples of recursion

Explore practical applications of recursion beyond theoretical examples, understanding how this powerful programming technique solves complex problems in various domains.

Recursion, at its core, is a programming technique where a function calls itself to solve a problem. While often introduced with simple examples like factorials or Fibonacci sequences, its true power lies in tackling complex, self-similar problems that can be broken down into smaller, identical sub-problems. This article delves into real-world scenarios where recursion shines, providing practical examples and insights into its implementation.

One of the most common and intuitive applications of recursion is traversing or manipulating hierarchical data structures. Trees, file systems, and organizational charts are all inherently recursive in nature. Each node in a tree can be seen as the root of its own subtree, making recursion a natural fit for operations like searching, insertion, or deletion.

flowchart TD
    A[Start Traversal] --> B{Is Node Null?}
    B -->|Yes| C[Return]
    B -->|No| D[Process Node]
    D --> E[Traverse Left Child]
    E --> A
    D --> F[Traverse Right Child]
    F --> A

Recursive Tree Traversal Logic

Consider a file system, which is a classic example of a tree structure. A directory can contain files and other directories. To list all files within a given directory and its subdirectories, a recursive function is ideal. It would check if the current item is a file (process it) or a directory (call itself on that directory).

import os

def list_all_files(directory_path):
    for item in os.listdir(directory_path):
        item_path = os.path.join(directory_path, item)
        if os.path.isfile(item_path):
            print(item_path)
        elif os.path.isdir(item_path):
            list_all_files(item_path) # Recursive call

# Example usage:
# Create some dummy files and directories for demonstration
# os.makedirs('my_folder/sub_folder1', exist_ok=True)
# os.makedirs('my_folder/sub_folder2', exist_ok=True)
# with open('my_folder/file1.txt', 'w') as f: f.write('content')
# with open('my_folder/sub_folder1/file2.txt', 'w') as f: f.write('content')
# with open('my_folder/sub_folder2/file3.txt', 'w') as f: f.write('content')

# list_all_files('my_folder')

Backtracking Algorithms for Problem Solving

Backtracking is a general algorithmic technique that often employs recursion to find solutions to computational problems, especially constraint satisfaction problems. It systematically tries to build a solution incrementally, one piece at a time. If at any point a partial solution cannot be completed to a valid solution, the algorithm 'backtracks' to an earlier state and tries a different path.

Classic examples include solving Sudoku puzzles, the N-Queens problem, finding all permutations of a set, or generating all possible paths in a maze. The recursive nature allows the algorithm to explore a path, and if it hits a dead end, it can easily revert to the previous state and try another option.

flowchart TD
    A[Start Backtracking] --> B{Is Current State a Solution?}
    B -->|Yes| C[Add to Solutions & Return]
    B -->|No| D{Are There More Choices?}
    D -->|Yes| E[Choose Next Option]
    E --> F[Make Move]
    F --> A
    D -->|No| G[Backtrack (Undo Move) & Return]

General Backtracking Algorithm Flow

import java.util.ArrayList;
import java.util.List;

public class Permutations {

    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(result, new ArrayList<>(), nums);
        return result;
    }

    private void backtrack(List<List<Integer>> result, List<Integer> currentPermutation, int[] nums) {
        // Base case: If the current permutation is complete
        if (currentPermutation.size() == nums.length) {
            result.add(new ArrayList<>(currentPermutation));
            return;
        }

        // Recursive step: Try adding each number not already in the current permutation
        for (int num : nums) {
            if (!currentPermutation.contains(num)) {
                currentPermutation.add(num); // Choose
                backtrack(result, currentPermutation, nums); // Explore
                currentPermutation.remove(currentPermutation.size() - 1); // Unchoose (backtrack)
            }
        }
    }

    public static void main(String[] args) {
        Permutations solver = new Permutations();
        int[] nums = {1, 2, 3};
        List<List<Integer>> allPermutations = solver.permute(nums);
        System.out.println("All permutations of {1, 2, 3}: " + allPermutations);
    }
}

Parsing and Language Processing

Recursion is fundamental in the design of parsers for programming languages, markup languages (like XML or JSON), and mathematical expressions. Grammars that define these languages are often recursive, making recursive descent parsers a natural fit. Each rule in the grammar can correspond to a recursive function that parses a specific part of the input.

For instance, an arithmetic expression like (2 + 3) * 5 can be recursively broken down. A function to parse an 'expression' might call a function to parse a 'term', which in turn might call a function to parse a 'factor', and so on. Parentheses introduce another level of recursion, as an expression within parentheses is itself an expression.

flowchart LR
    Expr --> Term
    Term --> Factor
    Factor --> Num
    Factor --> "(" Expr ")"
    Expr --> Expr Op Term
    Term --> Term Op Factor
    Op["+" | "-" | "*" | "/"]

Simplified Recursive Grammar for Arithmetic Expressions

class Parser {
    constructor(tokens) {
        this.tokens = tokens;
        this.position = 0;
    }

    peek() {
        return this.tokens[this.position];
    }

    consume(expectedType) {
        const token = this.peek();
        if (token && token.type === expectedType) {
            this.position++;
            return token;
        } else {
            throw new Error(`Expected ${expectedType}, but got ${token ? token.type : 'EOF'}`);
        }
    }

    parseFactor() {
        const token = this.peek();
        if (token.type === 'NUMBER') {
            return parseInt(this.consume('NUMBER').value);
        } else if (token.type === 'LPAREN') {
            this.consume('LPAREN');
            const expr = this.parseExpression();
            this.consume('RPAREN');
            return expr;
        } else {
            throw new Error('Unexpected token in factor: ' + token.type);
        }
    }

    parseTerm() {
        let result = this.parseFactor();
        while (this.peek() && (this.peek().type === 'MULTIPLY' || this.peek().type === 'DIVIDE')) {
            const operator = this.consume(this.peek().type).type;
            const nextFactor = this.parseFactor();
            if (operator === 'MULTIPLY') result *= nextFactor;
            else result /= nextFactor;
        }
        return result;
    }

    parseExpression() {
        let result = this.parseTerm();
        while (this.peek() && (this.peek().type === 'PLUS' || this.peek().type === 'MINUS')) {
            const operator = this.consume(this.peek().type).type;
            const nextTerm = this.parseTerm();
            if (operator === 'PLUS') result += nextTerm;
            else result -= nextTerm;
        }
        return result;
    }
}

// Example Usage (simplified tokenization)
const tokens = [
    { type: 'NUMBER', value: '2' },
    { type: 'PLUS', value: '+' },
    { type: 'NUMBER', value: '3' },
    { type: 'MULTIPLY', value: '*' },
    { type: 'NUMBER', value: '5' }
];

const parser = new Parser(tokens);
// console.log(parser.parseExpression()); // Expected: 17 (2 + 3 * 5)

const tokens2 = [
    { type: 'LPAREN', value: '(' },
    { type: 'NUMBER', value: '2' },
    { type: 'PLUS', value: '+' },
    { type: 'NUMBER', value: '3' },
    { type: 'RPAREN', value: ')' },
    { type: 'MULTIPLY', value: '*' },
    { type: 'NUMBER', value: '5' }
];

const parser2 = new Parser(tokens2);
// console.log(parser2.parseExpression()); // Expected: 25 ((2 + 3) * 5)