Recursion
Why Recursion Exists
Some problems are naturally self-similar - tree traversal, mathematical sequences, divide-and-conquer algorithms. Recursion provides an elegant way to express solutions that work on progressively smaller versions of the same problem.
When to Use Recursion
Use when:
- Problem has self-similar subproblems
- Working with recursive data structures (trees, graphs)
- Divide-and-conquer algorithms
- Backtracking problems (N-queens, sudoku)
- Mathematical sequences and calculations
- Parsing nested structures
Don’t use when:
- Simple iteration suffices
- Stack space is limited (embedded systems)
- Performance is critical and iterative solution exists
- Deep recursion might cause stack overflow
Modern reality: Essential for interviews and certain algorithms, but iterative solutions are often preferred in production for performance and stack safety.
Essential Recursion Components
1. Base Case
The condition that stops recursion. Without it, you get infinite recursion and stack overflow.
2. Recursive Case
The function calls itself with a modified (usually smaller) input that moves toward the base case.
3. Progress Toward Base Case
Each recursive call must get “closer” to the base case to ensure termination.
Classic Examples
Factorial
public static int Factorial(int n)
{
// Base case
if (n <= 1)
return 1;
// Recursive case: n! = n * (n-1)!
return n * Factorial(n - 1);
}
// Example usage
Console.WriteLine(Factorial(5)); // 120
Console.WriteLine(Factorial(0)); // 1
Fibonacci Sequence
// Naive recursive fibonacci - exponential time complexity
public static long FibonacciNaive(int n)
{
if (n <= 1)
return n;
return FibonacciNaive(n - 1) + FibonacciNaive(n - 2);
}
// This is inefficient for large n due to repeated calculations
Console.WriteLine(FibonacciNaive(10)); // 55, but slow for n > 30
Optimized Fibonacci with Memoization
public static class Fibonacci
{
private static Dictionary<int, long> memo = new Dictionary<int, long>();
public static long FibonacciMemo(int n)
{
if (memo.ContainsKey(n))
return memo[n];
if (n <= 1)
return n;
memo[n] = FibonacciMemo(n - 1) + FibonacciMemo(n - 2);
return memo[n];
}
// Alternative with explicit memo parameter
public static long FibonacciMemoExplicit(int n, Dictionary<int, long> memo = null)
{
if (memo == null)
memo = new Dictionary<int, long>();
if (memo.ContainsKey(n))
return memo[n];
if (n <= 1)
return n;
memo[n] = FibonacciMemoExplicit(n - 1, memo) + FibonacciMemoExplicit(n - 2, memo);
return memo[n];
}
}
// Much faster for large n
Console.WriteLine(Fibonacci.FibonacciMemo(50)); // 12586269025
Tree Recursion
Tree Traversal
public class TreeNode<T>
{
public T Value { get; set; }
public TreeNode<T> Left { get; set; }
public TreeNode<T> Right { get; set; }
public TreeNode(T value)
{
Value = value;
Left = null;
Right = null;
}
}
public static class TreeRecursion
{
// In-order: left -> root -> right
public static List<T> InorderTraversal<T>(TreeNode<T> node)
{
if (node == null)
return new List<T>();
var result = new List<T>();
result.AddRange(InorderTraversal(node.Left)); // Left subtree
result.Add(node.Value); // Root
result.AddRange(InorderTraversal(node.Right)); // Right subtree
return result;
}
// Calculate height of tree
public static int TreeHeight<T>(TreeNode<T> node)
{
if (node == null)
return 0;
int leftHeight = TreeHeight(node.Left);
int rightHeight = TreeHeight(node.Right);
return 1 + Math.Max(leftHeight, rightHeight);
}
// Sum all values in tree
public static int TreeSum(TreeNode<int> node)
{
if (node == null)
return 0;
return node.Value + TreeSum(node.Left) + TreeSum(node.Right);
}
}
// Example usage
var root = new TreeNode<int>(1);
root.Left = new TreeNode<int>(2);
root.Right = new TreeNode<int>(3);
root.Left.Left = new TreeNode<int>(4);
root.Left.Right = new TreeNode<int>(5);
var inorder = TreeRecursion.InorderTraversal(root);
Console.WriteLine($"Inorder: [{string.Join(", ", inorder)}]"); // [4, 2, 5, 1, 3]
Console.WriteLine($"Height: {TreeRecursion.TreeHeight(root)}"); // 3
Console.WriteLine($"Sum: {TreeRecursion.TreeSum(root)}"); // 15
Backtracking with Recursion
N-Queens Problem
public static class NQueens
{
public static List<List<int>> SolveNQueens(int n)
{
bool IsSafe(int[] board, int row, int col)
{
// Check column
for (int i = 0; i < row; i++)
{
if (board[i] == col)
return false;
}
// Check diagonals
for (int i = 0; i < row; i++)
{
if (Math.Abs(board[i] - col) == Math.Abs(i - row))
return false;
}
return true;
}
List<List<int>> Backtrack(int[] board, int row)
{
if (row == n)
{
return new List<List<int>> { new List<int>(board) }; // Found solution
}
var solutions = new List<List<int>>();
for (int col = 0; col < n; col++)
{
if (IsSafe(board, row, col))
{
board[row] = col; // Place queen
solutions.AddRange(Backtrack(board, row + 1)); // Recurse
// No need to remove queen - we overwrite board[row] in next iteration
}
}
return solutions;
}
var board = new int[n]; // board[i] = column position of queen in row i
for (int i = 0; i < n; i++)
board[i] = -1;
return Backtrack(board, 0);
}
}
// Example usage
var solutions = NQueens.SolveNQueens(4);
Console.WriteLine($"Found {solutions.Count} solutions for 4-Queens");
for (int i = 0; i < solutions.Count; i++)
{
Console.WriteLine($"Solution {i + 1}: [{string.Join(", ", solutions[i])}]");
}
Generate All Subsets
public static class Backtracking
{
// Generate all possible subsets using recursion
public static List<List<int>> GenerateSubsets(int[] nums)
{
var result = new List<List<int>>();
void Backtrack(int start, List<int> currentSubset)
{
// Add current subset to results
result.Add(new List<int>(currentSubset)); // Make a copy
// Try adding each remaining element
for (int i = start; i < nums.Length; i++)
{
currentSubset.Add(nums[i]); // Choose
Backtrack(i + 1, currentSubset); // Explore
currentSubset.RemoveAt(currentSubset.Count - 1); // Unchoose (backtrack)
}
}
Backtrack(0, new List<int>());
return result;
}
}
// Example usage
int[] nums = {1, 2, 3};
var subsets = Backtracking.GenerateSubsets(nums);
Console.WriteLine("All subsets:");
foreach (var subset in subsets)
{
Console.WriteLine($"[{string.Join(", ", subset)}]");
}
// Output: [], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]
Divide and Conquer
Merge Sort (Recursive)
public static class MergeSort
{
// Recursive merge sort implementation
public static int[] MergeSortArray(int[] arr)
{
// Base case
if (arr.Length <= 1)
return arr;
// Divide
int mid = arr.Length / 2;
int[] left = new int[mid];
int[] right = new int[arr.Length - mid];
Array.Copy(arr, 0, left, 0, mid);
Array.Copy(arr, mid, right, 0, arr.Length - mid);
left = MergeSortArray(left);
right = MergeSortArray(right);
// Conquer (merge)
return Merge(left, right);
}
// Merge two sorted arrays
private static int[] Merge(int[] left, int[] right)
{
var result = new List<int>();
int i = 0, j = 0;
while (i < left.Length && j < right.Length)
{
if (left[i] <= right[j])
{
result.Add(left[i]);
i++;
}
else
{
result.Add(right[j]);
j++;
}
}
// Add remaining elements
while (i < left.Length)
{
result.Add(left[i]);
i++;
}
while (j < right.Length)
{
result.Add(right[j]);
j++;
}
return result.ToArray();
}
}
// Example usage
int[] arr = {64, 34, 25, 12, 22, 11, 90};
int[] sortedArr = MergeSort.MergeSortArray(arr);
Console.WriteLine($"Original: [{string.Join(", ", arr)}]");
Console.WriteLine($"Sorted: [{string.Join(", ", sortedArr)}]");
Binary Search (Recursive)
public static class BinarySearch
{
// Recursive binary search
public static int BinarySearchRecursive(int[] arr, int target, int left = 0, int right = -1)
{
if (right == -1)
right = arr.Length - 1;
// Base case: not found
if (left > right)
return -1;
int mid = (left + right) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] > target)
return BinarySearchRecursive(arr, target, left, mid - 1);
else
return BinarySearchRecursive(arr, target, mid + 1, right);
}
}
// Example usage
int[] sortedArr = {1, 3, 5, 7, 9, 11, 13, 15};
int index = BinarySearch.BinarySearchRecursive(sortedArr, 7);
Console.WriteLine($"Found 7 at index: {index}"); // Found 7 at index: 3
Recursion vs Iteration
When to Choose Each
Recursion is better when:
- Problem naturally recursive (trees, fractals)
- Code is much cleaner and easier to understand
- Stack depth is reasonable
- Performance is not critical
Iteration is better when:
- Simple loops suffice
- Memory is limited
- Performance is critical
- Stack overflow is a concern
Converting Recursion to Iteration
Tail Recursion → Loop
// Tail recursive (easy to convert)
public static int FactorialTailRecursive(int n, int acc = 1)
{
if (n <= 1)
return acc;
return FactorialTailRecursive(n - 1, n * acc);
}
// Iterative equivalent
public static int FactorialIterative(int n)
{
int acc = 1;
while (n > 1)
{
acc *= n;
n--;
}
return acc;
}
General Recursion → Stack
// Recursive tree traversal
public static List<T> InorderRecursive<T>(TreeNode<T> node)
{
if (node == null)
return new List<T>();
var result = new List<T>();
result.AddRange(InorderRecursive(node.Left));
result.Add(node.Value);
result.AddRange(InorderRecursive(node.Right));
return result;
}
// Iterative using explicit stack
public static List<T> InorderIterative<T>(TreeNode<T> root)
{
var stack = new Stack<TreeNode<T>>();
var result = new List<T>();
var current = root;
while (stack.Count > 0 || current != null)
{
// Go to leftmost node
while (current != null)
{
stack.Push(current);
current = current.Left;
}
// Process current node
current = stack.Pop();
result.Add(current.Value);
// Move to right subtree
current = current.Right;
}
return result;
}
Common Recursion Patterns
1. Linear Recursion
Each function calls itself at most once.
public static int SumArray(int[] arr, int index = 0)
{
if (index >= arr.Length)
return 0;
return arr[index] + SumArray(arr, index + 1);
}
2. Tree Recursion
Function calls itself multiple times.
public static int Fibonacci(int n)
{
if (n <= 1)
return n;
return Fibonacci(n - 1) + Fibonacci(n - 2); // Two recursive calls
}
3. Tail Recursion
Recursive call is the last operation.
public static int GCD(int a, int b)
{
if (b == 0)
return a;
return GCD(b, a % b); // Last operation is recursive call
}
4. Mutual Recursion
Functions call each other.
public static bool IsEven(int n)
{
if (n == 0)
return true;
return IsOdd(n - 1);
}
public static bool IsOdd(int n)
{
if (n == 0)
return false;
return IsEven(n - 1);
}
Recursion Debugging Tips
1. Print Trace
public static int FactorialDebug(int n, int depth = 0)
{
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent}factorial({n})");
if (n <= 1)
{
Console.WriteLine($"{indent}-> returning 1");
return 1;
}
int result = n * FactorialDebug(n - 1, depth + 1);
Console.WriteLine($"{indent}-> returning {result}");
return result;
}
FactorialDebug(4);
2. Visualize Call Stack
public static int FibonacciTrace(int n, List<string> callStack = null)
{
if (callStack == null)
callStack = new List<string>();
callStack.Add($"fib({n})");
Console.WriteLine(string.Join(" -> ", callStack));
int result;
if (n <= 1)
{
result = n;
}
else
{
int left = FibonacciTrace(n - 1, new List<string>(callStack));
int right = FibonacciTrace(n - 2, new List<string>(callStack));
result = left + right;
}
callStack.RemoveAt(callStack.Count - 1);
return result;
}
3. Count Function Calls
public class CallCounter
{
public int Calls { get; set; } = 0;
}
public static int FibonacciWithCounter(int n, CallCounter counter)
{
counter.Calls++;
if (n <= 1)
return n;
return FibonacciWithCounter(n - 1, counter) +
FibonacciWithCounter(n - 2, counter);
}
// Usage
var counter = new CallCounter();
int result = FibonacciWithCounter(10, counter);
Console.WriteLine($"Result: {result}, Function calls: {counter.Calls}");
Interview Problems
1. Power Calculation
// Calculate base^exp using recursion
public static long Power(int baseNum, int exp)
{
if (exp == 0)
return 1;
if (exp == 1)
return baseNum;
// Optimize by using exp/2
long halfPower = Power(baseNum, exp / 2);
if (exp % 2 == 0)
return halfPower * halfPower;
else
return baseNum * halfPower * halfPower;
}
Console.WriteLine(Power(2, 10)); // 1024
2. Reverse String
// Reverse string using recursion
public static string ReverseString(string s)
{
if (s.Length <= 1)
return s;
return ReverseString(s.Substring(1)) + s[0];
}
Console.WriteLine(ReverseString("hello")); // "olleh"
3. Palindrome Check
// Check if string is palindrome using recursion
public static bool IsPalindrome(string s, int left = 0, int right = -1)
{
if (right == -1)
right = s.Length - 1;
if (left >= right)
return true;
if (s[left] != s[right])
return false;
return IsPalindrome(s, left + 1, right - 1);
}
Console.WriteLine(IsPalindrome("racecar")); // True
Console.WriteLine(IsPalindrome("hello")); // False
Performance Considerations
Quick Reference
Recursion Checklist
✓ Base case - When to stop ✓ Progress - Move toward base case ✓ Recursive call - Solve smaller problem ✓ Combine - Use subproblem results
Time Complexity Patterns
| Pattern | Complexity | Example | |———|————|———| | Single recursive call | O(n) | Factorial, sum | | Two calls (binary) | O(2ⁿ) | Fibonacci (naive) | | Divide & conquer | O(n log n) | Merge sort | | With memoization | O(n) often | Fibonacci (cached) |
Space Complexity
- Recursion depth: O(d) stack space
- Each call: Variables stored on stack
- Risk: Stack overflow with deep recursion
When to Use Recursion
✓ Tree/graph traversals ✓ Divide and conquer ✓ Backtracking ✓ Mathematical sequences
When to Avoid
✗ Simple loops suffice ✗ Deep recursion (use iteration) ✗ Performance critical (unless tail-optimized)
Found this guide helpful? Share it with your team:
Share on LinkedIn