Chapter 18: Efficient and Specialized Sorting
18.08Beginner / Intermediate5 topics14 min1 interactive animation

External sorting

When the data is larger than memory, the cost of sorting is dominated by disk reads and writes, not comparisons. External merge sort sorts memory-sized chunks into runs and then merges many runs at once.

Learning goals

  • Explain the two phases of external merge sort
  • Merge k sorted runs with a min-heap
  • Compute the number of merge passes
  • Reason about disk I/O instead of CPU comparisons

Prerequisites

Merge sortHeaps and priority queuesFiles and buffered I/O

Sorting a 100 GB file on a machine with 8 GB of memory is impossible to do in one array. You sort pieces that fit, save each sorted piece, and then merge the pieces by reading them sequentially.

18.08.1

External merge sort

Phase 1 (run generation): read as much data as fits in memory, sort it with any internal sort, and write the sorted chunk to disk as a run. Phase 2 (merging): merge the runs, k at a time, into larger runs until a single sorted run remains.

Merging is sequential: each run is read front to back and the output is written front to back. Sequential access is what disks and SSDs do best, which is why merge sort and not quick sort is the external sort of choice.

PhaseMemory useDisk access
Run generationWhole memory holds one chunkSequential read, sequential write
Merge passOne input buffer per run plus one output bufferSequential read of every run, sequential write
18.08.2

Runs

A run is a sorted sequence stored on disk. With N records and memory for M records, simple run generation produces ⌈N / M⌉ runs of length M.

Replacement selection generates longer runs. It keeps a heap in memory; the smallest value greater than the last value written goes out next, and a new value that is too small is held for the next run. On random input the average run length is about 2M, which halves the number of runs.

18.08.3

K-way merge

To merge k runs, keep the current head of each run. Repeatedly output the smallest head and read the next element from that run. The animation finds the smallest head by scanning the k heads, which costs O(k) per element; the heap version below needs only O(log k).

With a min-heap of at most k heads, merging n records in total costs O(n log k). Choose k as large as the memory for one buffer per run allows, to reduce the number of passes.

Java
1record Head(int value, int run) {}
2
3static void kWayMerge(List<Iterator<Integer>> runs, Consumer<Integer> out) {
4 PriorityQueue<Head> heap = new PriorityQueue<>(Comparator.comparingInt(Head::value));
5 for (int r = 0; r < runs.size(); r++) {
6 if (runs.get(r).hasNext()) heap.add(new Head(runs.get(r).next(), r));
7 }
8 while (!heap.isEmpty()) {
9 Head h = heap.poll(); // smallest of the k heads: O(log k)
10 out.accept(h.value());
11 Iterator<Integer> run = runs.get(h.run());
12 if (run.hasNext()) heap.add(new Head(run.next(), h.run()));
13 }
14}

Worst

O(n log k)

Space

O(k)

K-way merge of sorted runs

Interactive · edit the input

Your input

Try

run 0

7pos[0]
19
34

run 1

2pos[1]
11
28

run 2

5pos[2]
23
40

out[]

empty
Setup

3 sorted runs to merge. In an external sort these would be chunks that each fit in memory and were sorted separately, then written to disk.

Step 1 / 38
Java
1public static int[] kWayMerge(int[][] runs) {
2 int k = runs.length, total = 0;
3 for (int[] r : runs) total += r.length;
4 int[] pos = new int[k]; // next unread index in each run
5 int[] out = new int[total];
6 for (int o = 0; o < total; o++) {
7 int best = -1;
8 for (int r = 0; r < k; r++) {
9 if (pos[r] < runs[r].length &&
10 (best == -1 || runs[r][pos[r]] < runs[best][pos[best]])) {
11 best = r;
12 }
13 }
14 out[o] = runs[best][pos[best]++];
15 }
16 return out;
17}

Live state

0 comparisons

o

0

pos[0]

0

pos[1]

0

pos[2]

0

No condition evaluated on this step
Execution trace (dry run table)Show 38 steps
StepLineStateWhat happens
1L4o=0 pos[0]=0 pos[1]=0 pos[2]=03 sorted runs to merge. In an external sort these would be chunks that each fit in memory and were sorted separately, then written to disk.
2L11o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=0Run 0's head 7 is the first candidate.
3L10o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=1Run 1's head is smaller, so it becomes the new best.
4L10o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=1 r=2Run 1's head is still the smallest.
5L14o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=1out[0] = 2, taken from run 1.
6L11o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=0Run 0's head 7 is the first candidate.
7L10o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=1Run 0's head is still the smallest.
8L10o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=2Run 2's head is smaller, so it becomes the new best.
9L14o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=2out[1] = 5, taken from run 2.
10L11o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=0Run 0's head 7 is the first candidate.
11L10o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=1Run 0's head is still the smallest.
12L10o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=2Run 0's head is still the smallest.
13L14o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0out[2] = 7, taken from run 0.
14L11o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=0Run 0's head 19 is the first candidate.
15L10o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=1Run 1's head is smaller, so it becomes the new best.
16L10o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=1 r=2Run 1's head is still the smallest.
17L14o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=1out[3] = 11, taken from run 1.
18L11o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=0Run 0's head 19 is the first candidate.
19L10o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=1Run 0's head is still the smallest.
20L10o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=2Run 0's head is still the smallest.
21L14o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0out[4] = 19, taken from run 0.
22L11o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=0Run 0's head 34 is the first candidate.
23L10o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=1Run 1's head is smaller, so it becomes the new best.
24L10o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=1 r=2Run 2's head is smaller, so it becomes the new best.
25L14o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=2out[5] = 23, taken from run 2.
26L11o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=0Run 0's head 34 is the first candidate.
27L10o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=1Run 1's head is smaller, so it becomes the new best.
28L10o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=1 r=2Run 1's head is still the smallest.
29L14o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=1out[6] = 28, taken from run 1.
30L11o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=0Run 0's head 34 is the first candidate.
31L9o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=1Run 1 is exhausted, skip it.
32L10o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=2Run 0's head is still the smallest.
33L14o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=0out[7] = 34, taken from run 0.
34L9o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=0Run 0 is exhausted, skip it.
35L9o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=1Run 1 is exhausted, skip it.
36L11o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=2 r=2Run 2's head 40 is the first candidate.
37L14o=9 pos[0]=3 pos[1]=3 pos[2]=3 best=2out[8] = 40, taken from run 2.
38L16return=[2, 5, 7, 11, 19, 23, 28, 34, 40]Merged 9 values with 15 comparisons. Scanning k heads costs O(k) per value, so a min-heap of the heads makes it O(log k) and the total O(n log k).
18.08.4

Disk I/O

Reading a record from disk can be a million times slower than comparing two records in memory, so the cost model counts block transfers. Every pass reads the whole data set once and writes it once.

The number of passes is 1 (run generation) plus ⌈log_k(number of runs)⌉ merge passes. Example: a 100 GB file, 1 GB of memory, 100 runs. A 2-way merge needs ⌈log₂ 100⌉ = 7 merge passes. A 100-way merge, with 100 buffers of 10 MB each, needs a single merge pass.

Merge fan-in kMerge passes for 100 runsTotal passes over the data
278
1023
10012
18.08.5

Sorting beyond RAM

Practical systems rarely write this themselves. Databases use external merge sort for ORDER BY and sort-merge joins, and MapReduce-style frameworks sort partitions then merge them. In Java you can use a file-backed approach: read chunks, sort them with Arrays.sort, write each to a temporary file, and merge with the heap code above.

Alternatives when the data has structure: distribution sorts partition by key range so each partition fits in memory, and radix-based approaches reduce passes for integer keys.

Practice ladder

Now try it yourself

Easy

  • How many runs does external sort create for a 40 GB file with 4 GB of memory?
  • Merge three sorted lists by hand

Medium

  • Implement k-way merge with a PriorityQueue
  • Compute the pass count for N, M and fan-in k

Advanced

  • Implement replacement selection and measure the average run length
  • External sort of 1 GB of ints with a 64 MB heap in Java

Key takeaways

  • External sort: sort memory-sized chunks into runs, then merge runs with a k-way merge.
  • A min-heap makes each output element cost O(log k); passes = 1 + ⌈log_k runs⌉.
  • I/O volume, not comparisons, decides the running time.