Space complexity
Space complexity counts the memory an algorithm uses. This lesson separates input space from auxiliary space, covers the recursion stack, in-place algorithms, the memory-time trade-off, peak memory and Java memory leaks.
Learning goals
- Measure auxiliary space excluding the input
- Include the recursion stack in the analysis
- Spot leaks in garbage-collected code
Prerequisites
Auxiliary space
Auxiliary space is the extra memory an algorithm allocates beyond its input: temporary arrays, hash maps, recursion frames. Reversing an array with two indexes uses O(1) auxiliary space; copying it into a new array uses O(n). Space complexity is usually reported as auxiliary space.
Count every allocation that grows with n, including those inside helper calls.
1static void reverse(int[] a) { // O(1) auxiliary2for (int i = 0, j = a.length - 1; i < j; i++, j--) { int t = a[i]; a[i] = a[j]; a[j] = t; }3}4static int[] reversed(int[] a) { // O(n) auxiliary5int[] r = new int[a.length];6for (int i = 0; i < a.length; i++) r[i] = a[a.length - 1 - i];7return r;8}
Input space
Input space is the memory occupied by the input itself, such as the array passed in. Total space = input space + auxiliary space. Algorithms are compared on auxiliary space because the input is fixed by the problem.
State your convention. Saying “O(n) space” without it is ambiguous when the input itself is of size n.
Recursion stack
Each active call stores a frame (arguments, locals, return address). A recursion of depth d uses O(d) stack space, even if it allocates nothing else. Factorial uses O(n); binary search written recursively uses O(log n); merge sort uses O(log n) stack plus the O(n) merge buffer.
The JVM's default stack allows roughly 10,000 to 20,000 frames. Deeper recursion throws StackOverflowError; convert to a loop or raise -Xss.
Factorial: calls, base case and return path
Interactive · edit the inputRecursion tree · 1 call
Call factorial(4). It is frame number 1 on the call stack.
1static long factorial(int n) {2if (n == 0) return 1; // base case3return n * factorial(n - 1); // recursive case4}
Live state
n
4
depth
1
Call stack (top first)
- factorial(4)
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=4 depth=1 | Call factorial(4). It is frame number 1 on the call stack. |
| 2 | L2 | n=4 | n is not 0, so this call cannot answer yet. It needs factorial(3). |
| 3 | L3 | n=4 | Pause factorial(4) and call factorial(3). The pending multiplication by 4 waits on the stack. |
| 4 | L1 | n=3 depth=2 | Call factorial(3). It is frame number 2 on the call stack. |
| 5 | L2 | n=3 | n is not 0, so this call cannot answer yet. It needs factorial(2). |
| 6 | L3 | n=3 | Pause factorial(3) and call factorial(2). The pending multiplication by 3 waits on the stack. |
| 7 | L1 | n=2 depth=3 | Call factorial(2). It is frame number 3 on the call stack. |
| 8 | L2 | n=2 | n is not 0, so this call cannot answer yet. It needs factorial(1). |
| 9 | L3 | n=2 | Pause factorial(2) and call factorial(1). The pending multiplication by 2 waits on the stack. |
| 10 | L1 | n=1 depth=4 | Call factorial(1). It is frame number 4 on the call stack. |
| 11 | L2 | n=1 | n is not 0, so this call cannot answer yet. It needs factorial(0). |
| 12 | L3 | n=1 | Pause factorial(1) and call factorial(0). The pending multiplication by 1 waits on the stack. |
| 13 | L1 | n=0 depth=5 | Call factorial(0). It is frame number 5 on the call stack. |
| 14 | L2 | n=0 | Base case reached: no more recursion. Everything below this point is the return path. |
| 15 | L2 | n=0 return=1 | return 1. |
| 16 | L3 | n=1 return=1 | factorial(1) resumes: 1 × 1 = 1. Return it to the caller. |
| 17 | L3 | n=2 return=2 | factorial(2) resumes: 2 × 1 = 2. Return it to the caller. |
| 18 | L3 | n=3 return=6 | factorial(3) resumes: 3 × 2 = 6. Return it to the caller. |
| 19 | L3 | n=4 return=24 | factorial(4) resumes: 4 × 6 = 24. Return it to the caller. |
| 20 | L3 | return=24 | factorial(4) = 24, using 5 calls and a maximum stack depth of 5. |
In-place algorithms
An in-place algorithm transforms its input using O(1) extra space (some definitions allow O(log n) for the recursion). Heap sort, insertion sort, the two-pointer partition in quick sort and the reverse above are in place. Merge sort on arrays is not.
In place saves memory but loses the original data and can complicate the code.
Memory-time trade-off
You can often buy speed with memory. A hash set makes membership O(1) at O(n) space; memoisation turns exponential Fibonacci into linear time using an O(n) table; precomputed prefix sums make range queries O(1) with an extra array. The reverse trade, less memory for more time, is typical for streaming and low-memory devices.
Choose by constraints: a tight memory limit may force a slower approach.
| Technique | Extra space | Time gained |
|---|---|---|
| Hash set for lookups | O(n) | O(n) → O(1) per query |
| Memoisation | O(n) | exponential → O(n) |
| Prefix sums | O(n) | O(n) → O(1) per range query |
| Sorting then binary search | O(1) to O(n) | O(n) → O(log n) per query |
Peak memory
Peak memory is the largest amount in use at any moment, which is what a memory limit actually checks. An algorithm that allocates and frees many temporary arrays can have a small peak even if its total allocation is large. Conversely, holding two copies of a big structure at once doubles the peak.
Measure with a profiler or Runtime.getRuntime().totalMemory() - freeMemory() before and after a step, and run System.gc() first only for experiments.
1Runtime rt = Runtime.getRuntime();2long before = rt.totalMemory() - rt.freeMemory();3int[] big = new int[10_000_000]; // about 40 MB4long after = rt.totalMemory() - rt.freeMemory();5System.out.println((after - before) / 1_000_000 + " MB");
Memory leaks
Java has a garbage collector, but it only frees objects that are unreachable. A leak is an object you no longer need that remains reachable: a growing static List, an unbounded cache, listeners that were never removed, or an array-based stack that does not null out popped slots.
Fix by removing references when done, bounding caches (for example with an LRU policy), and using weak references where appropriate.
1class ArrayStack<T> {2private Object[] data = new Object[16]; private int size;3void push(T v) { if (size == data.length) data = Arrays.copyOf(data, size * 2); data[size++] = v; }4@SuppressWarnings("unchecked")5T pop() {6T v = (T) data[--size];7data[size] = null; // without this line the popped object stays reachable: a leak8return v;9}10}
Practice ladder
Now try it yourself
Easy
- State the auxiliary space of reversing an array in place and by copying
- How much memory does int[1_000_000] use?
Medium
- Give the space complexity of recursive binary search and iterative binary search
- Identify the leak in a stack that never nulls popped slots
Advanced
- Design an algorithm that finds the median of n values using O(1) extra space and explain its time cost
Key takeaways
- Space complexity usually means auxiliary space, and it includes the recursion stack.
- Memory can be traded for time and the reverse; constraints decide.
- Garbage collection frees unreachable objects only; stale references are leaks.