Classifying algorithms
Algorithms can also be classified by how they are written and how they run: iterative or recursive, deterministic or randomised, exact or approximate, online or offline, sequential or parallel, in place or not. These labels describe trade-offs you will meet in every chapter.
Learning goals
- Place an algorithm on each classification axis
- Convert between iterative and recursive forms
- Explain when randomisation and approximation are the right choice
- Tell online from offline algorithms
Prerequisites
Iterative vs recursive
An iterative algorithm repeats with a loop; a recursive algorithm solves a problem by calling itself on a smaller one. Anything recursive can be written iteratively (using your own stack if needed) and vice versa.
Recursion matches self-similar problems (trees, divide and conquer) and costs a stack frame per call. Loops are lighter. Step through the factorial animation to watch the call stack grow and unwind.
1static long factIterative(int n) { long r = 1; for (int i = 2; i <= n; i++) r *= i; return r; }2static long factRecursive(int n) { return n <= 1 ? 1 : n * factRecursive(n - 1); }
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. |
Deterministic vs randomized
A deterministic algorithm follows the same steps for the same input. A randomised algorithm uses random choices, so its running time (Las Vegas: always correct) or even its answer (Monte Carlo: correct with high probability) varies.
Randomised quick sort picks a random pivot so no input is consistently bad. A Bloom filter is Monte Carlo: it may report false positives but never false negatives.
| Kind | Answer | Time | Example |
|---|---|---|---|
| Deterministic | always the same | fixed for an input | merge sort |
| Las Vegas | always correct | random | randomised quick sort |
| Monte Carlo | correct with high probability | fixed | Miller-Rabin primality test |
Exact vs approximation
An exact algorithm returns the optimal answer. An approximation returns an answer within a provable factor of optimal when exact solutions are too expensive (many NP-hard problems).
Example: the greedy 2-approximation for vertex cover picks both ends of any uncovered edge and is guaranteed to use at most twice the optimal number of vertices. Chapter 61 covers approximation formally.
Online vs offline
An online algorithm must answer as each input item arrives, without seeing the future. An offline algorithm sees all of the input first and can reorder or preprocess it.
The running median is online: after every new number it must report the median so far. Sorting all numbers first is offline. Watch the two-heap solution update after every arrival.
Running median with two heaps
Interactive · edit the inputstream
low (max-heap)
high (min-heap)
medians so far
Split the numbers seen so far into a lower half (max-heap, top = largest of the lower half) and an upper half (min-heap, top = smallest of the upper half). The median sits at the two tops. Heaps are drawn with their top first.
1class MedianFinder {2PriorityQueue<Integer> low = new PriorityQueue<>(Comparator.reverseOrder()); // max-heap3PriorityQueue<Integer> high = new PriorityQueue<>(); // min-heap45void add(int x) {6low.add(x);7high.add(low.poll()); // every low value <= every high value8if (high.size() > low.size()) {9low.add(high.poll()); // keep low the same size or one larger10}11}1213double median() {14return low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2.0;15}16}
Live state
low.size
0
high.size
0
Execution trace (dry run table)Show 37 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | low.size=0 high.size=0 | Split the numbers seen so far into a lower half (max-heap, top = largest of the lower half) and an upper half (min-heap, top = smallest of the upper half). The median sits at the two tops. Heaps are drawn with their top first. |
| 2 | L6 | x=5 low.size=1 high.size=0 | low.add(5). The new value always enters the lower half first. |
| 3 | L7 | x=5 moved=5 low.size=0 high.size=1 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 4 | L8 | low.size=0 high.size=1 | The upper half is bigger than the lower half, so rebalance. |
| 5 | L9 | moved=5 low.size=1 high.size=0 | Move 5 back to the lower half. |
| 6 | L14 | median=5 low.size=1 high.size=0 | Odd count: the median is the top of low, 5. |
| 7 | L6 | x=15 low.size=2 high.size=0 | low.add(15). The new value always enters the lower half first. |
| 8 | L7 | x=15 moved=15 low.size=1 high.size=1 | high.add(low.poll()): the largest value of the lower half (15) moves up. This guarantees every low value is <= every high value. |
| 9 | L8 | low.size=1 high.size=1 | Sizes are balanced: low is equal or one larger. |
| 10 | L14 | median=10 low.size=1 high.size=1 | Even count: the median is the average of the two tops, (5 + 15) / 2 = 10. |
| 11 | L6 | x=1 low.size=2 high.size=1 | low.add(1). The new value always enters the lower half first. |
| 12 | L7 | x=1 moved=5 low.size=1 high.size=2 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 13 | L8 | low.size=1 high.size=2 | The upper half is bigger than the lower half, so rebalance. |
| 14 | L9 | moved=5 low.size=2 high.size=1 | Move 5 back to the lower half. |
| 15 | L14 | median=5 low.size=2 high.size=1 | Odd count: the median is the top of low, 5. |
| 16 | L6 | x=3 low.size=3 high.size=1 | low.add(3). The new value always enters the lower half first. |
| 17 | L7 | x=3 moved=5 low.size=2 high.size=2 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 18 | L8 | low.size=2 high.size=2 | Sizes are balanced: low is equal or one larger. |
| 19 | L14 | median=4 low.size=2 high.size=2 | Even count: the median is the average of the two tops, (3 + 5) / 2 = 4. |
| 20 | L6 | x=8 low.size=3 high.size=2 | low.add(8). The new value always enters the lower half first. |
| 21 | L7 | x=8 moved=8 low.size=2 high.size=3 | high.add(low.poll()): the largest value of the lower half (8) moves up. This guarantees every low value is <= every high value. |
| 22 | L8 | low.size=2 high.size=3 | The upper half is bigger than the lower half, so rebalance. |
| 23 | L9 | moved=5 low.size=3 high.size=2 | Move 5 back to the lower half. |
| 24 | L14 | median=5 low.size=3 high.size=2 | Odd count: the median is the top of low, 5. |
| 25 | L6 | x=7 low.size=4 high.size=2 | low.add(7). The new value always enters the lower half first. |
| 26 | L7 | x=7 moved=7 low.size=3 high.size=3 | high.add(low.poll()): the largest value of the lower half (7) moves up. This guarantees every low value is <= every high value. |
| 27 | L8 | low.size=3 high.size=3 | Sizes are balanced: low is equal or one larger. |
| 28 | L14 | median=6 low.size=3 high.size=3 | Even count: the median is the average of the two tops, (5 + 7) / 2 = 6. |
| 29 | L6 | x=9 low.size=4 high.size=3 | low.add(9). The new value always enters the lower half first. |
| 30 | L7 | x=9 moved=9 low.size=3 high.size=4 | high.add(low.poll()): the largest value of the lower half (9) moves up. This guarantees every low value is <= every high value. |
| 31 | L8 | low.size=3 high.size=4 | The upper half is bigger than the lower half, so rebalance. |
| 32 | L9 | moved=7 low.size=4 high.size=3 | Move 7 back to the lower half. |
| 33 | L14 | median=7 low.size=4 high.size=3 | Odd count: the median is the top of low, 7. |
| 34 | L6 | x=10 low.size=5 high.size=3 | low.add(10). The new value always enters the lower half first. |
| 35 | L7 | x=10 moved=10 low.size=4 high.size=4 | high.add(low.poll()): the largest value of the lower half (10) moves up. This guarantees every low value is <= every high value. |
| 36 | L8 | low.size=4 high.size=4 | Sizes are balanced: low is equal or one larger. |
| 37 | L14 | median=7.5 low.size=4 high.size=4 | Even count: the median is the average of the two tops, (7 + 8) / 2 = 7.5. |
Sequential vs parallel
A sequential algorithm does one step at a time. A parallel algorithm splits work across threads or cores. Divide and conquer algorithms such as merge sort parallelise naturally because the two halves are independent.
Parallelism helps only when the work is large enough to pay for the splitting and joining. In Java, Arrays.parallelSort and parallel streams do this for you; measure before trusting them.
1int[] big = new Random(1).ints(5_000_000).toArray();2Arrays.parallelSort(big); // splits, sorts in parallel, merges3long evens = Arrays.stream(big).parallel().filter(x -> x % 2 == 0).count();
In-place vs out-of-place
An in-place algorithm uses O(1) extra memory beyond its input (or O(log n) for a recursion stack) and rearranges the input itself: insertion sort, heap sort, quick sort. An out-of-place one needs memory that grows with the input: merge sort's buffer, counting sort's output array.
In-place saves memory but destroys the original order, which matters if the caller still needs the input.
| Algorithm | Extra space | In place? |
|---|---|---|
| Insertion sort | O(1) | yes |
| Heap sort | O(1) | yes |
| Quick sort | O(log n) stack | yes (usually counted as in place) |
| Merge sort (arrays) | O(n) | no |
| Counting sort | O(n + k) | no |
Practice ladder
Now try it yourself
Easy
- Write factorial iteratively and recursively
- Is binary search iterative or recursive? Can it be both?
Medium
- Explain why randomised quick sort is Las Vegas
- Is “find the median of a stream” online or offline?
Advanced
- Convert a recursive depth-first traversal into an iterative one with an explicit stack
Key takeaways
- Recursive and iterative are two forms of the same computation with different stack costs.
- Randomisation removes bad inputs; approximation trades exactness for tractability.
- Online algorithms answer as data arrives; in-place algorithms save memory.