Converting recursion
Recursion is elegant but costs stack space and can overflow. Every recursive algorithm can be rewritten with a loop, using an explicit stack, an accumulator, or by computing results bottom-up, and sometimes caching results turns exponential recursion into linear time.
Learning goals
- Replace recursion with an explicit stack
- Write an iterative depth-first traversal
- Rewrite a recursion bottom-up
- Turn tail recursion into a loop and memoize overlapping subproblems
Prerequisites
The JVM call stack is just a stack data structure the runtime manages for you. If you manage a stack yourself, you control its size and are not limited by the thread's stack space.
Explicit stack
The call stack stores, for each pending call, its arguments and where to continue. You can store the same information in your own Deque: push the arguments, loop while the deque is not empty, and pop to process.
Take a recursive method with a single call at the end, push the work for the call instead of calling, and the loop replaces the recursion. The heap holds your stack, so depth is limited by memory, not by the thread stack.
1// recursive: depth = list length2static long sum(Node n) { return n == null ? 0 : n.val + sum(n.next); }34// iterative: no recursion, no depth limit5static long sumIter(Node head) {6long total = 0;7for (Node cur = head; cur != null; cur = cur.next) total += cur.val;8return total;9}
Iterative DFS
Depth-first search is the standard case: recursion visits a node, then its children. The iterative version keeps the nodes still to visit on a stack. Push the root, then repeatedly pop a node, process it, and push its children (right child first if you want left-to-right order).
This avoids StackOverflowError on deep trees and graphs, such as a path graph of a million nodes.
1// recursive pre-order2static void preorder(TreeNode n) {3if (n == null) return;4visit(n);5preorder(n.left);6preorder(n.right);7}89// iterative pre-order with an explicit stack10static void preorderIter(TreeNode root) {11Deque<TreeNode> stack = new ArrayDeque<>();12if (root != null) stack.push(root);13while (!stack.isEmpty()) {14TreeNode n = stack.pop();15visit(n);16if (n.right != null) stack.push(n.right); // pushed first, so popped last17if (n.left != null) stack.push(n.left);18}19}
Bottom-up processing
Top-down recursion starts from the big problem and works down to the base cases. Bottom-up starts from the base cases and builds up to the answer in a loop, in an order where every dependency is already computed.
For Fibonacci that means computing fib(2), fib(3), … in order. It uses no stack and, if only the last two values matter, only O(1) memory. It is the basis of dynamic programming.
1static long fibBottomUp(int n) {2if (n < 2) return n;3long prev = 0, cur = 1;4for (int i = 2; i <= n; i++) {5long next = prev + cur;6prev = cur;7cur = next;8}9return cur; // O(n) time, O(1) space, no recursion10}
Worst
O(n)
Space
O(1)
Tail-call transformation
A tail-recursive method can be rewritten as a loop by turning its parameters into variables and the call into an assignment followed by a jump back to the top. Java does not optimise tail calls, so doing this by hand removes the stack growth.
Pattern: introduce an accumulator for the work that was pending, then loop until the base case.
1// tail-recursive with an accumulator2static long factTail(int n, long acc) {3if (n == 0) return acc;4return factTail(n - 1, acc * n);5}67// the same logic as a loop8static long factLoop(int n) {9long acc = 1;10while (n != 0) {11acc *= n; // the work that was in the argument12n--; // the argument that was in the call13}14return acc;15}1617// Euclid's gcd, iterative18static int gcdLoop(int a, int b) {19while (b != 0) { int t = a % b; a = b; b = t; }20return a;21}
Memoization
Memoization keeps the recursive structure but stores each result the first time it is computed, so repeated subproblems are answered from a table. Naive Fibonacci goes from about 1.6ⁿ calls to 2n - 1, as you saw in the animation: cache hits are the dashed green boxes.
Use an array when the arguments are small integers and a HashMap for general keys. Pick a sentinel that cannot be a real answer (such as -1) to mean not computed yet; using 0 breaks when 0 is a valid answer.
1static long[] memo = new long[91];23static long fib(int n) {4if (n < 2) return n;5if (memo[n] != 0) return memo[n]; // fib(n) is never 0 for n >= 2, so 0 means "unknown"6return memo[n] = fib(n - 1) + fib(n - 2);7}89// general keys: use get / put, NOT computeIfAbsent with a recursive lambda10static Map<Integer, Long> cache = new HashMap<>();11static long fib2(int n) {12if (n < 2) return n;13Long hit = cache.get(n);14if (hit != null) return hit;15long value = fib2(n - 1) + fib2(n - 2);16cache.put(n, value);17return value;18}
Practice ladder
Now try it yourself
Easy
- Rewrite a recursive array sum as a loop
- Convert recursive gcd to a loop
Medium
- Iterative in-order traversal of a binary tree
- Climbing stairs bottom-up with two variables
- Memoize a recursive grid path counter
Advanced
- Iterative post-order traversal with a single stack
- Simulate the call stack of any recursive method with frames holding a program counter
- Convert recursive quick sort to an explicit stack that always pushes the larger side first
Key takeaways
- An explicit stack or accumulator removes recursion's depth limit.
- Bottom-up computes in dependency order and often needs O(1) extra space.
- Memoization keeps the recursive form and removes repeated work; use get/put, not recursive computeIfAbsent.