Recursion is the idea of self-invocation until a base case is reached. Search algorithms like DFS fall out of it almost naturally, and when examined mathematically, it is just induction in disguise.
Uses
- Recursive data structures (linked lists, trees, graphs)
- Dynamic programming (memoised recursion over a state space)
Recursion as Mathematical Induction
The idea of induction is as follows. Suppose I have some statement that I believe to be true over some domain, e.g. . The goal is to assume holds for some and demonstrate that follows. The standard cookbook:
- Prove the base case holds
- Assume for that is true (or for strong induction, all )
- Use that to demonstrate also holds
One useful approach (taught in graph theory) is to start from and reduce it to , rather than building up. As an example:
Claim: A tree with vertices has edges.
Proof by induction. A tree is a connected, acyclic graph. In the base case , a single vertex has no edges, so holds.
Assume : a tree with vertices has edges. Now suppose is a tree with vertices. By a corollary (proved in the appendix below), every tree has at least two leaves; pick one, . Deleting along with its single edge yields a tree on vertices; no cycles form because we removed a leaf, and connectivity is preserved because a leaf is not a cut vertex. By , has edges, so , which is what we needed to show.
The key pattern: we relied on the truth of a smaller case to construct the present one. Recursion works the same way. When approaching a recursive problem, ask:
- Base case: what does the smallest valid input look like?
- Sub-problem: what result from a smaller input do I need?
- Construction: how do I assemble my answer from that result?
The challenge in recursion is usually the construction step. In DP, the harder question is choosing the right state representation. Both are fundamentally induction.
Basic Examples
Linked list sum. Applying the three questions:
- Base case: empty list → 0
- Sub-problem: sum of the tail
- Construct: add the head value to the tail sum
ocaml(* OCaml: pattern matching makes the structure explicit *)let rec sum = function| [] -> 0| head :: tail -> head + sum tail
Coin change. Minimise the number of coins needed to make amount from denominations :
- Base case: → 0 coins needed; → impossible
- Sub-problem: the minimum coins for for each
- Construct: take 1 plus the minimum of those sub-problems
pythondef coin_change(denoms, n):if n == 0:return 0return min((1 + coin_change(denoms, n - c) for c in denoms if c <= n),default=float('inf'))
These examples track a single piece of state. Harder problems track multiple values, have more edge cases, or require careful thought about what the recursive call should return.
Leetcode: Twin Sum
Leetcode 2130: Maximum Twin Sum of a LinkedList: pair the first and last nodes, second and second-last, and so on, then return the maximum pair sum.
A naive approach (collect the first half into an array, then walk the second half) works but is inelegant. Three cleaner approaches:
- Reverse the first half in place and iterate together
- Hare-and-tortoise to find the midpoint, then reverse
- A zipper: descend recursively to the end, then walk the front pointer forward as the call stack unwinds
The zipper approach fell out most naturally to me. The key insight: on the way back up the call stack, we’re naturally iterating the back pointer from last to first, while the front pointer steps forward.
The recursive function carries the current front node and returns the updated front pointer together with the running maximum:
pythondef recurse(front, back, max_val):if back.next is None:# base: at the last node, form the first pairreturn max(max_val, back.val + front.val), front.nextbest_val, next_front = recurse(front, back.next, max_val)# unwinding: back is now moving backwards; front advances via next_frontreturn max(best_val, next_front.val + back.val), next_front.next
At every frame on the way up, back is one step closer to the front while next_front steps forward; they meet in the middle.
Practice Problems
Linked lists
- 234. Palindrome Linked List
- 206. Reverse Linked List
- 24. Swap Nodes in Pairs
- 25. Reverse Nodes in k-Group
Trees
- 104. Maximum Depth of Binary Tree
- 110. Balanced Binary Tree
- 124. Binary Tree Maximum Path Sum
- 687. Longest Univalue Path
Worked: Reverse Nodes in k-Group
The trick is to isolate the recursive leap of faith. Assume the suffix starting from position is already correctly reversed. Then all you need to do is reverse the current -group and attach it to that sorted suffix: iterate curr → prev while threading curr → next.
Worked: Binary Tree Maximum Path Sum
This one tripped me up because you track two different things:
- Global best: the best path seen anywhere in the tree (updated at each node)
- Local best: the best continuous path rooted at this node that a parent can extend
Applying the template:
- Base case: null node contributes 0; a leaf is just its value
- Sub-problem: left and right subtrees each give a global best and a local best (one-sided path)
- Construct: try stitching
L + node + Rto update the global best; the local best isnode + max(L, R, 0)
Keep your variable names precise: confusing l_best (global best from left subtree) with l_path (best path rooted at left child) is an easy way to get wrong answers.
Longest Univalue Path follows the same pattern, but the “stitching” condition becomes value equality rather than unconstrained addition. Watch your sentinel values: if node values can be negative, don’t use -1 as “impossible”.
How Rusty Was I?
As a form of self-humiliation, a log of the mistakes that actually happened:
- Used
|instead of&in a boolean expression - Missing brackets around an inequality comparison
- Forgot to
returnthe recursive call (classic) - Returned the whole tuple where only one element was needed
- In Swap Nodes in Pairs:
head.next, head = head, head.nextandhead, head.next = head.next, headare not the same (Python evaluates the right-hand side first as a tuple) - Struggled to see why you need a temporary when swapping
- The bracket placement in
balanced = (abs(l_height - r_height) <= 1) & l_bal & r_bal - Mixed up argument order in a recursive call
- Used the global best from the left subtree to compute the best continuous sum (these are different things)
- Used a sentinel of for Longest Univalue Path when node values lie in
Despite all that, a few problems was enough to shake the rust off. The main lesson: for trees, you often want to compute a global property while threading local subtree state upwards. Linked list problems often involve a second pointer tracking a different part of the list.
Appendix: Every Tree Has at Least Two Leaves
For a tree (connected, acyclic graph) with , we claim it has at least one leaf (a vertex of degree 1). Let be a maximal path in , i.e. one that cannot be extended at either end.
Because is maximal, no neighbour of lies outside (otherwise could be extended). And no neighbour of lies inside either: if some were adjacent to an interior vertex , we could form a cycle through , contradicting acyclicity. Both cases are illustrated below.
Therefore , i.e. is a leaf. The same argument applies to , giving at least two leaves.
Conclusion
Recursion, dynamic programming, DFS, and most tree/linked-list problems are all induction over a state space. Internalising that framing makes it much easier to construct solutions systematically: identify the base case, decide what the recursive call should return, and build the answer from there.