03: Recursion Fundamentals

Recursion Fundamentals Intermediate
Section titled “Recursion Fundamentals Intermediate”Overview
Section titled “Overview”Recursion is one of the most powerful and elegant problem-solving techniques in computer science. A recursive function is one that calls itself to solve smaller instances of the same problem. While it may seem circular at first, recursion provides natural solutions to problems involving hierarchical data, divide-and-conquer algorithms, and backtracking scenarios.
In this chapter, you’ll develop recursive thinking—the ability to break problems down into smaller, self-similar subproblems. You’ll learn the critical elements every recursive function needs (base case and recursive case), understand how the call stack manages recursion, and master optimization techniques like memoization that transform exponential algorithms into linear ones.
By the end, you’ll confidently write recursive solutions to real-world problems including tree traversal, file system navigation, parsing, and backtracking algorithms like Sudoku solvers. You’ll also learn when recursion is the right tool and when iteration is better—a critical skill for production PHP development.
What You’ll Learn
Section titled “What You’ll Learn”Estimated time: 70 minutes
By the end of this chapter, you will:
- Master recursive thinking and understand how the call stack manages recursive function calls
- Learn to write correct base cases and recursive cases that make progress toward termination
- Understand when to use recursion vs iteration and their performance trade-offs
- Optimize recursive functions with memoization techniques and tail recursion patterns
- Apply recursion to real-world scenarios including tree traversal, parsing, backtracking, and divide-and-conquer
Prerequisites
Section titled “Prerequisites”Before starting this chapter, you should have:
- ✓ Understanding of functions and how they work (10 mins review from Chapter 4 if needed)
- ✓ Familiarity with the call stack concept (15 mins learning if needed)
- ✓ Completion of Chapters 0-2 or equivalent foundations (180 mins if not done)
- ✓ Basic understanding of arrays and loops
Estimated Time: ~70 minutes
What You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will have created:
- A recursive factorial calculator demonstrating base case and recursive case patterns
- An optimized Fibonacci implementation using memoization (100x+ performance improvement)
- A directory tree traversal function for file system navigation
- A recursive array flattener handling deeply nested data structures
- A Sudoku solver using recursive backtracking
- A recursive descent parser for evaluating arithmetic expressions
What Is Recursion? (~5 min)
Section titled “What Is Recursion? (~5 min)”Recursion occurs when a function calls itself. It’s like looking into two mirrors facing each other—you see infinite reflections, each slightly smaller.
::: tip Recursive Thinking The key to recursion is breaking a problem into smaller versions of itself. Each recursive call should work on a simpler problem, eventually reaching a base case that stops the recursion. :::
A Simple Example
Section titled “A Simple Example”<?php
declare(strict_types=1);
function countdown(int $n): void{ if ($n <= 0) { echo "Blast off!\n"; return; }
echo "$n...\n"; countdown($n - 1); // Function calls itself}
countdown(5);Output:
5...4...3...2...1...Blast off!The Two Essential Parts of Recursion (~10 min)
Section titled “The Two Essential Parts of Recursion (~10 min)”Every recursive function must have:
::: warning Critical Rule Every recursive function MUST have both a base case (stopping condition) and a recursive case (progress toward the base case). Missing either leads to infinite recursion and stack overflow! :::
1. Base Case (Stopping Condition)
Section titled “1. Base Case (Stopping Condition)”The condition that stops the recursion. Without it, you get infinite recursion and a stack overflow!
<?php
declare(strict_types=1);
// Bad: No base case = infinite recursionfunction badRecursion(int $n): int{ return badRecursion($n - 1); // Never stops!}
// Good: Has base casefunction goodRecursion(int $n): int{ if ($n <= 0) { // BASE CASE return 0; } return goodRecursion($n - 1); // RECURSIVE CASE}2. Recursive Case (Progress Toward Base Case)
Section titled “2. Recursive Case (Progress Toward Base Case)”The part where the function calls itself with a “smaller” problem, moving toward the base case.
<?php
declare(strict_types=1);
function factorial(int $n): int{ // Base case: factorial of 0 or 1 is 1 if ($n <= 1) { return 1; }
// Recursive case: n! = n × (n-1)! return $n * factorial($n - 1);}
echo factorial(5); // 5 × 4 × 3 × 2 × 1 = 120How Recursion Works: The Call Stack (~10 min)
Section titled “How Recursion Works: The Call Stack (~10 min)”PHP uses a call stack to track function calls:
factorial(3)│├─ 3 * factorial(2)│ ││ ├─ 2 * factorial(1)│ │ ││ │ └─ return 1 (base case)│ ││ └─ return 2 * 1 = 2│└─ return 3 * 2 = 6Each function call is pushed onto the stack. When a base case is reached, calls start returning (popping off the stack).
Visualizing the Stack
Section titled “Visualizing the Stack”<?php
declare(strict_types=1);
function visualizeFactorial(int $n, int $depth = 0): int{ $indent = str_repeat(' ', $depth); echo "{$indent}factorial({$n}) called\n";
if ($n <= 1) { echo "{$indent}Base case reached: returning 1\n"; return 1; }
$result = $n * visualizeFactorial($n - 1, $depth + 1); echo "{$indent}factorial({$n}) returning {$result}\n"; return $result;}
visualizeFactorial(4);Output:
factorial(4) called factorial(3) called factorial(2) called factorial(1) called Base case reached: returning 1 factorial(2) returning 2 factorial(3) returning 6factorial(4) returning 24Classic Recursive Problems (~15 min)
Section titled “Classic Recursive Problems (~15 min)”Fibonacci Sequence
Section titled “Fibonacci Sequence”Each number is the sum of the two preceding ones: 0, 1, 1, 2, 3, 5, 8, 13…
<?php
declare(strict_types=1);
function fibonacci(int $n): int{ // Base cases if ($n === 0) return 0; if ($n === 1) return 1;
// Recursive case return fibonacci($n - 1) + fibonacci($n - 2);}
echo fibonacci(6); // 8::: warning Performance Warning
This naive Fibonacci implementation is O(2ⁿ)—exponentially slow! For fib(40), you’ll wait several seconds. We’ll fix this with memoization shortly.
:::
Sum of Array
Section titled “Sum of Array”<?php
declare(strict_types=1);
function sumArray(array $arr): int{ // Base case: empty array if (empty($arr)) { return 0; }
// Recursive case: first element + sum of rest $first = array_shift($arr); return $first + sumArray($arr);}
echo sumArray([1, 2, 3, 4, 5]); // 15Power Function
Section titled “Power Function”<?php
declare(strict_types=1);
function power(int $base, int $exponent): int{ // Base case: anything to the power of 0 is 1 if ($exponent === 0) { return 1; }
// Recursive case: base × base^(exponent-1) return $base * power($base, $exponent - 1);}
echo power(2, 5); // 2^5 = 32Reverse a String
Section titled “Reverse a String”<?php
declare(strict_types=1);
function reverseString(string $str): string{ // Base case: empty or single character if (strlen($str) <= 1) { return $str; }
// Recursive case: last char + reverse of remaining string return substr($str, -1) . reverseString(substr($str, 0, -1));}
echo reverseString('hello'); // 'olleh'Recursion vs Iteration (~10 min)
Section titled “Recursion vs Iteration (~10 min)”Many recursive problems can be solved iteratively. Let’s compare:
Factorial: Recursive vs Iterative
Section titled “Factorial: Recursive vs Iterative”// Recursivefunction factorialRecursive(int $n): int{ if ($n <= 1) return 1; return $n * factorialRecursive($n - 1);}
// Iterativefunction factorialIterative(int $n): int{ $result = 1; for ($i = 2; $i <= $n; $i++) { $result *= $i; } return $result;}Comparison:
- Recursive: More elegant, easier to understand for some problems
- Iterative: Usually faster, uses less memory (no stack overhead)
When to Use Recursion
Section titled “When to Use Recursion”✅ Use recursion when:
- Problem naturally divides into smaller subproblems (tree traversal, divide-and-conquer)
- Code is clearer and more maintainable
- Stack depth won’t be excessive
❌ Avoid recursion when:
- Simple iteration is clearer
- Stack depth could be very deep (risk of stack overflow)
- Performance is critical and iteration is faster
Optimizing Recursion: Tail Recursion (~5 min)
Section titled “Optimizing Recursion: Tail Recursion (~5 min)”Tail recursion occurs when the recursive call is the last operation in the function. Some languages optimize this, but PHP doesn’t automatically.
::: info PHP Limitation Unlike functional languages (Scheme, Erlang), PHP doesn’t perform tail-call optimization. However, understanding tail recursion is still valuable for converting to iterative solutions. :::
// Not tail recursive: operation after recursive callfunction factorial(int $n): int{ if ($n <= 1) return 1; return $n * factorial($n - 1); // Multiplication happens AFTER return}
// Tail recursive: no operation after recursive callfunction factorialTail(int $n, int $accumulator = 1): int{ if ($n <= 1) { return $accumulator; } return factorialTail($n - 1, $n * $accumulator); // Nothing after this}While PHP doesn’t optimize tail recursion automatically, it’s still a good pattern to know.
Common Recursive Patterns (~10 min)
Section titled “Common Recursive Patterns (~10 min)”1. Linear Recursion
Section titled “1. Linear Recursion”Each function call makes one recursive call:
function countDown(int $n): void{ if ($n <= 0) return; echo "$n "; countDown($n - 1); // One recursive call}2. Binary Recursion
Section titled “2. Binary Recursion”Each function call makes two recursive calls:
function fibonacci(int $n): int{ if ($n <= 1) return $n; return fibonacci($n - 1) + fibonacci($n - 2); // Two calls!}3. Multiple Recursion
Section titled “3. Multiple Recursion”Function makes many recursive calls:
function printCombinations(array $items, int $k, array $current = []): void{ if ($k === 0) { echo implode(', ', $current) . "\n"; return; }
foreach ($items as $item) { printCombinations($items, $k - 1, array_merge($current, [$item])); }}Recursive Data Structures (~15 min)
Section titled “Recursive Data Structures (~15 min)”Some data structures are inherently recursive:
::: tip Natural Fit for Recursion Trees, nested arrays, file systems, and JSON structures are naturally recursive. Recursion often provides the clearest solution for these problems. :::
Directory Tree Traversal
Section titled “Directory Tree Traversal”function listFiles(string $directory, int $depth = 0): void{ $indent = str_repeat(' ', $depth);
$items = scandir($directory);
foreach ($items as $item) { if ($item === '.' || $item === '..') continue;
$path = $directory . '/' . $item; echo $indent . $item . "\n";
// Recursive case: if directory, traverse it if (is_dir($path)) { listFiles($path, $depth + 1); } }}
listFiles('./docs');Nested Array Sum
Section titled “Nested Array Sum”function sumNested(array $arr): int{ $sum = 0;
foreach ($arr as $item) { if (is_array($item)) { // Recursive case: nested array $sum += sumNested($item); } else { // Base case: single value $sum += $item; } }
return $sum;}
$nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]];echo sumNested($nested); // 45Avoiding Stack Overflow (~5 min)
Section titled “Avoiding Stack Overflow (~5 min)”PHP has a limited call stack. Deep recursion can cause stack overflow:
::: warning Stack Overflow Risk PHP’s default recursion limit is around 100-512 levels (varies by configuration). For large datasets (n > 10,000), prefer iterative solutions or tail-recursive patterns convertible to loops. :::
<?php
declare(strict_types=1);
// This will likely crash with large nfunction deepRecursion(int $n): int{ if ($n <= 0) return 0; return 1 + deepRecursion($n - 1);}
// Stack overflow!// deepRecursion(100000);Measuring Recursion Depth
Section titled “Measuring Recursion Depth”Before deploying recursive code, measure your system’s limits:
<?php
declare(strict_types=1);
function measureRecursionDepth(int $n, int $depth = 0): int{ if ($n <= 0) { return $depth; } return measureRecursionDepth($n - 1, $depth + 1);}
// Test your system's limittry { $maxDepth = measureRecursionDepth(10000); echo "System supports recursion depth: {$maxDepth}\n";} catch (Error $e) { echo "Recursion limit reached around depth " . substr($e->getMessage(), 0, 50) . "\n";}
// Alternative: Use debug_backtrace() to inspect call stackfunction trackRecursion(int $n): int{ $depth = count(debug_backtrace()); echo "Current depth: {$depth}\n";
if ($n <= 0) return $depth; return trackRecursion($n - 1);}
// trackRecursion(5); // Shows depth increasingProduction-Safe Recursion with Error Handling
Section titled “Production-Safe Recursion with Error Handling”<?php
declare(strict_types=1);
class RecursionLimitException extends RuntimeException {}
function safeRecursion( int $n, int $maxDepth = 1000, int $currentDepth = 0): int { if ($currentDepth > $maxDepth) { throw new RecursionLimitException( "Maximum recursion depth ({$maxDepth}) exceeded at n={$n}" ); }
if ($n <= 0) { return 0; // Base case }
return 1 + safeRecursion($n - 1, $maxDepth, $currentDepth + 1);}
// Safe usage in productiontry { $result = safeRecursion(500); // Safe limit echo "Result: {$result}\n";} catch (RecursionLimitException $e) { error_log("Recursion limit error: " . $e->getMessage()); // Fallback to iterative approach echo "Switching to iterative solution...\n";}Solution 1: Use Iteration
Section titled “Solution 1: Use Iteration”<?php
declare(strict_types=1);
function iterativeCount(int $n): int{ $count = 0; for ($i = 0; $i < $n; $i++) { $count++; } return $count;}
echo iterativeCount(100000); // Works fine!Solution 2: Increase Stack Size (Not Recommended)
Section titled “Solution 2: Increase Stack Size (Not Recommended)”ini_set('xdebug.max_nesting_level', 10000); // Requires XdebugBetter to redesign with iteration or tail recursion where possible.
Memory Usage: Recursion vs Iteration
Section titled “Memory Usage: Recursion vs Iteration”Recursive calls consume stack memory. Let’s compare:
<?php
declare(strict_types=1);
// Recursive: Uses O(n) stack spacefunction sumRecursive(int $n): int{ if ($n <= 0) return 0; return $n + sumRecursive($n - 1);}
// Iterative: Uses O(1) spacefunction sumIterative(int $n): int{ $sum = 0; for ($i = 1; $i <= $n; $i++) { $sum += $i; } return $sum;}
// Measure memory$n = 1000;
memory_reset_peak_usage();$result = sumRecursive($n);$recursiveMemory = memory_get_peak_usage();
memory_reset_peak_usage();$result = sumIterative($n);$iterativeMemory = memory_get_peak_usage();
echo "Recursive memory: " . number_format($recursiveMemory) . " bytes\n";echo "Iterative memory: " . number_format($iterativeMemory) . " bytes\n";echo "Difference: " . number_format($recursiveMemory - $iterativeMemory) . " bytes\n";Key Takeaway: Recursion uses O(n) stack space, iteration uses O(1). For large n, this matters!
Memoization: Optimizing Recursive Functions (~10 min)
Section titled “Memoization: Optimizing Recursive Functions (~10 min)”Memoization caches results to avoid redundant calculations:
::: tip Performance Boost
Memoization can transform Fibonacci from O(2ⁿ) to O(n)—that’s the difference between seconds and microseconds for fib(35)! Always consider memoization for recursive functions with overlapping subproblems.
:::
// Slow: O(2ⁿ) - recalculates same valuesfunction fibonacciSlow(int $n): int{ if ($n <= 1) return $n; return fibonacciSlow($n - 1) + fibonacciSlow($n - 2);}
// Fast: O(n) - caches resultsfunction fibonacciFast(int $n, array &$memo = []): int{ if ($n <= 1) return $n;
if (isset($memo[$n])) { return $memo[$n]; // Return cached result }
$memo[$n] = fibonacciFast($n - 1, $memo) + fibonacciFast($n - 2, $memo); return $memo[$n];}
// Compare performance$start = microtime(true);echo fibonacciSlow(35) . "\n"; // Takes several secondsecho "Time: " . (microtime(true) - $start) . "s\n";
$start = microtime(true);echo fibonacciFast(35) . "\n"; // Nearly instant!echo "Time: " . (microtime(true) - $start) . "s\n";We’ll explore memoization deeply in the Dynamic Programming section.
Real-World PHP Examples (~15 min)
Section titled “Real-World PHP Examples (~15 min)”JSON Validation
Section titled “JSON Validation”function validateJSON($data): bool{ if (is_scalar($data) || $data === null) { return true; // Base case: primitive types are valid }
if (is_array($data)) { foreach ($data as $item) { if (!validateJSON($item)) { // Recursive validation return false; } } return true; }
if (is_object($data)) { foreach ($data as $value) { if (!validateJSON($value)) { // Recursive validation return false; } } return true; }
return false;}
$data = ['name' => 'John', 'nested' => ['age' => 30, 'tags' => ['php', 'dev']]];echo validateJSON($data) ? 'Valid' : 'Invalid';Menu Hierarchy Rendering
Section titled “Menu Hierarchy Rendering”function renderMenu(array $items, int $depth = 0): string{ $html = str_repeat(' ', $depth) . "<ul>\n";
foreach ($items as $item) { $html .= str_repeat(' ', $depth + 1) . "<li>{$item['title']}";
// Recursive case: render children if (!empty($item['children'])) { $html .= "\n" . renderMenu($item['children'], $depth + 2); $html .= str_repeat(' ', $depth + 1); }
$html .= "</li>\n"; }
$html .= str_repeat(' ', $depth) . "</ul>\n"; return $html;}
$menu = [ ['title' => 'Home', 'children' => []], ['title' => 'Products', 'children' => [ ['title' => 'Electronics', 'children' => []], ['title' => 'Clothing', 'children' => []] ]], ['title' => 'About', 'children' => []]];
echo renderMenu($menu);Mutual Recursion (~5 min)
Section titled “Mutual Recursion (~5 min)”Functions can call each other recursively:
// Check if a number is even/odd using mutual recursionfunction isEven(int $n): bool{ if ($n === 0) { return true; } return isOdd($n - 1);}
function isOdd(int $n): bool{ if ($n === 0) { return false; } return isEven($n - 1);}
echo isEven(4) ? 'true' : 'false'; // trueecho isOdd(4) ? 'true' : 'false'; // falsePractical example: State machine
interface State{ public function handle(string $input): ?State;}
class ReadingState implements State{ public function handle(string $input): ?State { if ($input === 'START_TAG') { return new TagState(); } return $this; // Stay in reading state }}
class TagState implements State{ public function handle(string $input): ?State { if ($input === 'END_TAG') { return new ReadingState(); } return $this; }}
// Parser using mutually recursive statesfunction parseHTML(array $tokens, State $state = null): void{ if (empty($tokens)) { return; }
$state = $state ?? new ReadingState(); $token = array_shift($tokens); $newState = $state->handle($token);
parseHTML($tokens, $newState); // Recursive call with new state}Converting Recursion to Iteration (~10 min)
Section titled “Converting Recursion to Iteration (~10 min)”Some recursive algorithms can be converted to iterative ones for better performance.
Technique 1: Using a Stack
Section titled “Technique 1: Using a Stack”// Recursive: Depth-first traversalfunction dfsRecursive(TreeNode $node): void{ if ($node === null) return;
echo $node->value . " "; dfsRecursive($node->left); dfsRecursive($node->right);}
// Iterative: Using explicit stackfunction dfsIterative(TreeNode $node): void{ $stack = [$node];
while (!empty($stack)) { $current = array_pop($stack);
if ($current === null) continue;
echo $current->value . " ";
// Push right first so left is processed first if ($current->right) $stack[] = $current->right; if ($current->left) $stack[] = $current->left; }}Technique 2: Accumulator Pattern
Section titled “Technique 2: Accumulator Pattern”// Recursive: Uses call stackfunction sumRecursive(array $arr): int{ if (empty($arr)) return 0; return $arr[0] + sumRecursive(array_slice($arr, 1));}
// Iterative: Uses accumulatorfunction sumIterative(array $arr): int{ $sum = 0; foreach ($arr as $value) { $sum += $value; } return $sum;}
// Tail-recursive with accumulator (still recursive but more efficient)function sumTailRecursive(array $arr, int $acc = 0): int{ if (empty($arr)) return $acc; return sumTailRecursive(array_slice($arr, 1), $acc + $arr[0]);}Technique 3: Loop with State
Section titled “Technique 3: Loop with State”// Recursive: Factorialfunction factorialRecursive(int $n): int{ if ($n <= 1) return 1; return $n * factorialRecursive($n - 1);}
// Iterative: Loop with statefunction factorialIterative(int $n): int{ $result = 1; for ($i = 2; $i <= $n; $i++) { $result *= $i; } return $result;}
// When to convert:// - Deep recursion (risk of stack overflow)// - Performance-critical code// - When iteration is equally clearAdvanced Recursion Patterns (~15 min)
Section titled “Advanced Recursion Patterns (~15 min)”Divide and Conquer with Recursion
Section titled “Divide and Conquer with Recursion”// Binary search - divides problem in half each timefunction binarySearchRecursive(array $arr, int $target, int $left = 0, int $right = null): int{ if ($right === null) { $right = count($arr) - 1; }
if ($left > $right) { return -1; // Not found }
$mid = (int)(($left + $right) / 2);
if ($arr[$mid] === $target) { return $mid; } elseif ($arr[$mid] < $target) { return binarySearchRecursive($arr, $target, $mid + 1, $right); } else { return binarySearchRecursive($arr, $target, $left, $mid - 1); }}
// Merge sort - divides, sorts, then mergesfunction mergeSortRecursive(array $arr): array{ if (count($arr) <= 1) { return $arr; }
$mid = (int)(count($arr) / 2); $left = mergeSortRecursive(array_slice($arr, 0, $mid)); $right = mergeSortRecursive(array_slice($arr, $mid));
return mergeArrays($left, $right);}
function mergeArrays(array $left, array $right): array{ $result = []; $i = $j = 0;
while ($i < count($left) && $j < count($right)) { if ($left[$i] <= $right[$j]) { $result[] = $left[$i++]; } else { $result[] = $right[$j++]; } }
return array_merge($result, array_slice($left, $i), array_slice($right, $j));}Recursion with Multiple Parameters
Section titled “Recursion with Multiple Parameters”// Longest Common Subsequence (LCS)function lcs(string $str1, string $str2, int $m = null, int $n = null): int{ $m = $m ?? strlen($str1); $n = $n ?? strlen($str2);
// Base case: empty string if ($m === 0 || $n === 0) { return 0; }
// If last characters match if ($str1[$m - 1] === $str2[$n - 1]) { return 1 + lcs($str1, $str2, $m - 1, $n - 1); }
// If don't match, try both options return max( lcs($str1, $str2, $m - 1, $n), lcs($str1, $str2, $m, $n - 1) );}
echo lcs("ABCDGH", "AEDFHR"); // 3 (ADH)
// With memoization for O(m×n) instead of exponentialfunction lcsMemo(string $str1, string $str2, int $m = null, int $n = null, array &$memo = []): int{ $m = $m ?? strlen($str1); $n = $n ?? strlen($str2);
if ($m === 0 || $n === 0) return 0;
$key = "{$m},{$n}"; if (isset($memo[$key])) { return $memo[$key]; }
if ($str1[$m - 1] === $str2[$n - 1]) { $memo[$key] = 1 + lcsMemo($str1, $str2, $m - 1, $n - 1, $memo); } else { $memo[$key] = max( lcsMemo($str1, $str2, $m - 1, $n, $memo), lcsMemo($str1, $str2, $m, $n - 1, $memo) ); }
return $memo[$key];}Recursion in PHP Frameworks (~10 min)
Section titled “Recursion in PHP Frameworks (~10 min)”Laravel Collections (Recursive Operations)
Section titled “Laravel Collections (Recursive Operations)”// Flattening nested collections$collection = collect([ 'users' => [ ['name' => 'John', 'posts' => [1, 2, 3]], ['name' => 'Jane', 'posts' => [4, 5]] ]]);
// Recursive flattenfunction flattenCollection($items): array{ $result = [];
foreach ($items as $item) { if (is_array($item)) { $result = array_merge($result, flattenCollection($item)); } else { $result[] = $item; } }
return $result;}Symfony Finder (Directory Recursion)
Section titled “Symfony Finder (Directory Recursion)”use Symfony\Component\Finder\Finder;
// Built on recursive directory traversal$finder = new Finder();$finder->files()->in(__DIR__)->name('*.php');
// Manual implementation of recursive file findingfunction findFiles(string $dir, string $pattern): array{ $result = []; $items = scandir($dir);
foreach ($items as $item) { if ($item === '.' || $item === '..') continue;
$path = $dir . '/' . $item;
if (is_dir($path)) { // Recursive case: descend into directory $result = array_merge($result, findFiles($path, $pattern)); } elseif (fnmatch($pattern, $item)) { // Base case: file matches pattern $result[] = $path; } }
return $result;}Doctrine ORM (Entity Loading)
Section titled “Doctrine ORM (Entity Loading)”// Recursive relationship loadingclass Category{ private $id; private $name; private $parent; private $children;
// Recursively get all ancestors public function getAncestors(): array { if ($this->parent === null) { return []; // Base case: no parent }
// Recursive case: parent's ancestors + parent return array_merge( $this->parent->getAncestors(), [$this->parent] ); }
// Recursively get all descendants public function getAllDescendants(): array { $descendants = [];
foreach ($this->children as $child) { $descendants[] = $child; $descendants = array_merge( $descendants, $child->getAllDescendants() ); }
return $descendants; }}Complex Real-World Examples (~20 min)
Section titled “Complex Real-World Examples (~20 min)”Expression Evaluator
Section titled “Expression Evaluator”// Evaluate mathematical expressions recursivelyinterface Expression{ public function evaluate(): float;}
class NumberExpr implements Expression{ public function __construct(private float $value) {}
public function evaluate(): float { return $this->value; }}
class BinaryExpr implements Expression{ public function __construct( private Expression $left, private string $operator, private Expression $right ) {}
public function evaluate(): float { $left = $this->left->evaluate(); // Recursive $right = $this->right->evaluate(); // Recursive
return match($this->operator) { '+' => $left + $right, '-' => $left - $right, '*' => $left * $right, '/' => $left / $right, default => throw new Exception("Unknown operator"), }; }}
// Build: (3 + 5) * (10 - 2)$expr = new BinaryExpr( new BinaryExpr(new NumberExpr(3), '+', new NumberExpr(5)), '*', new BinaryExpr(new NumberExpr(10), '-', new NumberExpr(2)));
echo $expr->evaluate(); // 64Recursive Backtracking: Sudoku Solver
Section titled “Recursive Backtracking: Sudoku Solver”class SudokuSolver{ private const SIZE = 9;
public function solve(array &$board): bool { $empty = $this->findEmpty($board);
if ($empty === null) { return true; // Base case: puzzle solved }
[$row, $col] = $empty;
for ($num = 1; $num <= 9; $num++) { if ($this->isValid($board, $row, $col, $num)) { $board[$row][$col] = $num;
// Recursive case: try to solve rest if ($this->solve($board)) { return true; }
// Backtrack $board[$row][$col] = 0; } }
return false; // Trigger backtracking }
private function findEmpty(array $board): ?array { for ($i = 0; $i < self::SIZE; $i++) { for ($j = 0; $j < self::SIZE; $j++) { if ($board[$i][$j] === 0) { return [$i, $j]; } } } return null; }
private function isValid(array $board, int $row, int $col, int $num): bool { // Check row if (in_array($num, $board[$row])) { return false; }
// Check column for ($i = 0; $i < self::SIZE; $i++) { if ($board[$i][$col] === $num) { return false; } }
// Check 3x3 box $boxRow = (int)($row / 3) * 3; $boxCol = (int)($col / 3) * 3;
for ($i = $boxRow; $i < $boxRow + 3; $i++) { for ($j = $boxCol; $j < $boxCol + 3; $j++) { if ($board[$i][$j] === $num) { return false; } } }
return true; }}Recursive Descent Parser
Section titled “Recursive Descent Parser”// Simple arithmetic parserclass Parser{ private array $tokens; private int $pos = 0;
public function parse(string $expr): int { $this->tokens = str_split(str_replace(' ', '', $expr)); $this->pos = 0; return $this->parseExpression(); }
private function parseExpression(): int { $result = $this->parseTerm();
while ($this->pos < count($this->tokens) && in_array($this->tokens[$this->pos], ['+', '-'])) { $op = $this->tokens[$this->pos++]; $right = $this->parseTerm(); $result = $op === '+' ? $result + $right : $result - $right; }
return $result; }
private function parseTerm(): int { $result = $this->parseFactor();
while ($this->pos < count($this->tokens) && in_array($this->tokens[$this->pos], ['*', '/'])) { $op = $this->tokens[$this->pos++]; $right = $this->parseFactor(); $result = $op === '*' ? $result * $right : (int)($result / $right); }
return $result; }
private function parseFactor(): int { if ($this->tokens[$this->pos] === '(') { $this->pos++; // skip '(' $result = $this->parseExpression(); // Recursive! $this->pos++; // skip ')' return $result; }
// Parse number $num = ''; while ($this->pos < count($this->tokens) && is_numeric($this->tokens[$this->pos])) { $num .= $this->tokens[$this->pos++]; }
return (int)$num; }}
$parser = new Parser();echo $parser->parse("3 + 5 * (2 + 3)"); // 28Common Pitfalls and Solutions (~10 min)
Section titled “Common Pitfalls and Solutions (~10 min)”Pitfall 1: Forgetting Base Case
Section titled “Pitfall 1: Forgetting Base Case”// Wrong: Infinite recursionfunction countDown(int $n): void{ echo "$n "; countDown($n - 1); // Never stops!}
// Correct: Has base casefunction countDown(int $n): void{ if ($n <= 0) return; // Base case echo "$n "; countDown($n - 1);}Pitfall 2: Not Making Progress
Section titled “Pitfall 2: Not Making Progress”// Wrong: Doesn't get closer to base casefunction broken(int $n): int{ if ($n === 0) return 0; return broken($n); // Same n, infinite loop!}
// Correct: Makes progressfunction working(int $n): int{ if ($n === 0) return 0; return broken($n - 1); // Moves toward base case}Pitfall 3: Inefficient Recursion
Section titled “Pitfall 3: Inefficient Recursion”// Inefficient: O(2ⁿ) - recalculates same valuesfunction fib(int $n): int{ if ($n <= 1) return $n; return fib($n - 1) + fib($n - 2);}
// Efficient: O(n) with memoizationfunction fibMemo(int $n, array &$memo = []): int{ if ($n <= 1) return $n; if (isset($memo[$n])) return $memo[$n];
$memo[$n] = fibMemo($n - 1, $memo) + fibMemo($n - 2, $memo); return $memo[$n];}
// Or iterative: O(n) time, O(1) spacefunction fibIterative(int $n): int{ if ($n <= 1) return $n;
$prev = 0; $curr = 1;
for ($i = 2; $i <= $n; $i++) { $next = $prev + $curr; $prev = $curr; $curr = $next; }
return $curr;}Practice Exercises (~45 min total)
Section titled “Practice Exercises (~45 min total)”::: tip Learning by Doing These exercises reinforce recursion fundamentals. Try solving each without looking at the solution first. If stuck, review the patterns from earlier sections. :::
Exercise 1: Greatest Common Divisor (GCD) (~10 min)
Section titled “Exercise 1: Greatest Common Divisor (GCD) (~10 min)”Use Euclid’s algorithm recursively:
function gcd(int $a, int $b): int{ // Your code here}
echo gcd(48, 18); // Should output: 6Solution
function gcd(int $a, int $b): int{ // Base case: if b is 0, GCD is a if ($b === 0) { return $a; }
// Recursive case: GCD(a, b) = GCD(b, a % b) return gcd($b, $a % $b);}Exercise 2: Palindrome Checker (~10 min)
Section titled “Exercise 2: Palindrome Checker (~10 min)”Check if a string is a palindrome recursively:
function isPalindrome(string $str): bool{ // Your code here}
echo isPalindrome('racecar') ? 'Yes' : 'No'; // Should output: YesSolution
function isPalindrome(string $str): bool{ // Remove non-alphanumeric and convert to lowercase $str = strtolower(preg_replace('/[^a-z0-9]/', '', $str));
// Base case: empty or single character if (strlen($str) <= 1) { return true; }
// Check if first and last characters match if ($str[0] !== $str[strlen($str) - 1]) { return false; }
// Recursive case: check middle substring return isPalindrome(substr($str, 1, -1));}Exercise 3: Flatten Nested Array (~10 min)
Section titled “Exercise 3: Flatten Nested Array (~10 min)”Flatten a multi-dimensional array into a single-level array:
function flatten(array $arr): array{ // Your code here}
$nested = [1, [2, [3, 4], 5], 6];print_r(flatten($nested)); // Should output: [1, 2, 3, 4, 5, 6]Solution
function flatten(array $arr): array{ $result = [];
foreach ($arr as $item) { if (is_array($item)) { // Recursive case: merge flattened nested array $result = array_merge($result, flatten($item)); } else { // Base case: add single item $result[] = $item; } }
return $result;}Exercise 4: Tower of Hanoi (~15 min)
Section titled “Exercise 4: Tower of Hanoi (~15 min)”The Tower of Hanoi is a classic recursive puzzle. Move n disks from source rod to destination rod, using an auxiliary rod, following these rules:
- Only one disk can be moved at a time
- A disk can only be placed on top of a larger disk or an empty rod
- Only the top disk can be moved
function towerOfHanoi(int $n, string $source, string $auxiliary, string $destination): void{ // Your code here}
// Move 3 disks from 'A' to 'C' using 'B'towerOfHanoi(3, 'A', 'B', 'C');Expected output:
Move disk 1 from A to CMove disk 2 from A to BMove disk 1 from C to BMove disk 3 from A to CMove disk 1 from B to AMove disk 2 from B to CMove disk 1 from A to CSolution
<?php
declare(strict_types=1);
function towerOfHanoi(int $n, string $source, string $auxiliary, string $destination): void{ // Base case: only 1 disk if ($n === 1) { echo "Move disk 1 from {$source} to {$destination}\n"; return; }
// Step 1: Move n-1 disks from source to auxiliary (using destination) towerOfHanoi($n - 1, $source, $destination, $auxiliary);
// Step 2: Move largest disk from source to destination echo "Move disk {$n} from {$source} to {$destination}\n";
// Step 3: Move n-1 disks from auxiliary to destination (using source) towerOfHanoi($n - 1, $auxiliary, $source, $destination);}
// Test with 3 disksecho "Solution for 3 disks:\n";towerOfHanoi(3, 'A', 'B', 'C');
echo "\nTotal moves for n disks: 2^n - 1\n";echo "For 3 disks: 2^3 - 1 = 7 moves\n";Why it works:
The Tower of Hanoi perfectly demonstrates recursive problem-solving:
- Base case: If n=1, simply move the disk
- Recursive case:
- Move n-1 disks out of the way (to auxiliary)
- Move the largest disk to destination
- Move the n-1 disks from auxiliary to destination
Time complexity: O(2ⁿ) - each disk doubles the moves
- 1 disk: 1 move
- 2 disks: 3 moves
- 3 disks: 7 moves
- 4 disks: 15 moves
- n disks: 2ⁿ - 1 moves
Space complexity: O(n) - recursion depth equals number of disks
Wrap-up
Section titled “Wrap-up”Congratulations! You’ve mastered recursion fundamentals—one of the most powerful problem-solving techniques in programming. Here’s what you’ve accomplished:
- ✓ Understood recursive thinking and how functions can call themselves to solve problems
- ✓ Mastered the two essential parts of recursion: base case (stopping condition) and recursive case (progress toward solution)
- ✓ Learned how the call stack works and how PHP manages recursive function calls
- ✓ Implemented classic recursive algorithms including factorial, Fibonacci, and array operations
- ✓ Optimized recursive functions using memoization (transforming O(2ⁿ) to O(n))
- ✓ Applied recursion to real-world problems including file system traversal, JSON validation, menu rendering, and backtracking
- ✓ Built complex recursive solutions including Sudoku solver, recursive descent parser, and Tower of Hanoi
- ✓ Learned production-safe recursion with depth tracking, error handling, and memory management
- ✓ Learned when to use recursion vs iteration based on clarity, performance, and stack depth considerations
- ✓ Avoided common pitfalls including infinite recursion, stack overflow, and inefficient implementations
- ✓ Measured recursion limits and implemented safe recursion patterns for production code
Key Takeaways
Section titled “Key Takeaways”- Recursion is when a function calls itself to solve smaller subproblems
- Every recursive function needs a base case (stopping condition) and recursive case (progress toward base)
- The call stack tracks recursive calls; too many can cause stack overflow (PHP limit: ~100-512 levels)
- Memoization can dramatically improve recursive performance (O(2ⁿ) → O(n) for Fibonacci)
- Many recursive problems can be solved iteratively—choose based on clarity and performance
- Recursion shines with naturally recursive data structures (trees, nested data, file systems)
- Always make progress toward the base case to avoid infinite recursion
- Consider stack depth limits for production code with large datasets
- Recursion uses O(n) stack space vs iteration’s O(1)—memory matters for large n
- Implement recursion depth tracking and error handling for production safety
- Measure your system’s recursion limits before deployment
You now have the foundation to tackle complex recursive algorithms. In the next chapter, we’ll explore problem-solving strategies that combine recursion with techniques like divide-and-conquer, backtracking, and dynamic programming.
Further Reading
Section titled “Further Reading”Deepen your understanding with these resources:
- PHP Functions Documentation — Official PHP documentation on user-defined functions
- Call Stack on Wikipedia — Deep dive into how the call stack works in programming languages
- Tail Call Optimization — Understanding tail recursion and why some languages optimize it
- Dynamic Programming Overview — Where memoization fits into the bigger picture of algorithmic optimization
- Recursion Visualizer — Interactive tool to visualize recursive function calls
- Master Theorem — Mathematical framework for analyzing divide-and-conquer recursive algorithms
- Backtracking Algorithms — Problem-solving technique that uses recursion to explore all possibilities
What’s Next
Section titled “What’s Next”In the next chapter, we’ll explore Problem-Solving Strategies that combine recursion with other techniques like divide-and-conquer, backtracking, and dynamic programming.
💻 Code Samples
Section titled “💻 Code Samples”All code examples from this chapter are available in the GitHub repository:
Files included:
01-recursion-basics.php- Complete recursion examples including basic recursion, Fibonacci (naive, memoized, iterative), recursive data structures, divide and conquer, and backtrackingREADME.md- Complete documentation and usage guide
Clone the repository to run the examples locally:
git clone https://github.com/dalehurley/codewithphp.gitcd codewithphp/code/php-algorithms/chapter-03php 01-recursion-basics.phpContinue to Chapter 04: Problem-Solving Strategies.