Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Sorting at Scale  ›  Lesson

Sorting Data That Doesn't Fit

External Sorting 24 minSplit into memory-sized sorted runs, then merge all of them in one pass with a min-heap
You're building a piece ofQuarry — the ingest sorter
This piece — external_sort(): Sorts a batch bigger than memory: generate runs, then merge them in one pass.
Scenario Quarry's nightly compactor has more records to sort than fit in memory at once, and re-reading the same data twice is the cost it most wants to avoid.
Your task
Build external_sort(data, memory_limit). Return data fully sorted ascending: split it into chunks of at most memory_limit items, sort each chunk ("run generation"), then merge every run in one pass with a min-heap ("k-way merge"). Example: external_sort([5, 3, 8, 1, 9, 2, 7, 4, 6], 3) -> [1, 2, 3, 4, 5, 6, 7, 8, 9].

Sorting Data That Doesn't Fit

In short: every sort so far assumed the whole array lives in memory at once. When the data is bigger than RAM, that assumption breaks, and disk reads become the real cost — not comparisons. External sorting splits the data into chunks small enough to sort in memory (runs), writes each run back to disk, then merges all the runs in one pass using a min-heap that never holds more than one item per run at a time. The heap from the heap sort lesson comes back here for a different job: picking the smallest of k candidates, cheaply, over and over.

Three horizontal lanes labelled disk read, in-memory sort and disk write, with staggered blocks: chunk 1 moves into the sort lane while chunk 2 is still being read, and into the write lane while chunk 2 sorts and chunk 3 reads

Watch the merge phase pop the smallest item across three runs, one pass, no re-reading:

step through it
1import heapq
2 
3runs = [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
4heap = [(run[0], r, 0) for r, run in enumerate(runs)]
5heapq.heapify(heap)
6out = []
7while heap:
8 value, r, idx = heapq.heappop(heap)
9 out.append(value)
10 print(value, "from run", r)
11 if idx + 1 < len(runs[r]):
12 heapq.heappush(heap, (runs[r][idx + 1], r, idx + 1))
13print(out)

Each heappush/heappop costs O(log k) for k runs, and the merge visits every item exactly once — O(n log k) total, a single sequential pass over the data instead of n passes of "find the overall minimum".

Sort a batch bigger than memory

Quarry's compactor ingests more records overnight than fit in memory. Split, sort each piece, then merge — the runs never need to be loaded all at once during merging, only their current front item:

python

With memory_limit = 3 on 9 items, that's 3 runs of 3 — each small enough to sort with any in-memory sort, then merged with a heap of size 3, never size 9.

When there are too many runs to merge at once

If there are more runs than the heap can hold open file handles for, merge happens in passes: merge k runs at a time into fewer, bigger runs, repeat, until one remains. The number of passes is 1 + ⌈log_k(runs)⌉ — the 1 is run generation, and each merge pass divides the run count by k:

What does this print?
import math

def merge_pass_count(num_runs, fan_in):
    if num_runs <= 1:
        return 1
    return 1 + math.ceil(math.log(num_runs, fan_in))

print(merge_pass_count(64, 4), merge_pass_count(64, 8))

A wider merge means fewer total passes, which is why real external sorts (and database engines) use as many open runs per merge as memory allows, not a fixed small number.

Quick practice

What does this print?
import heapq

runs = [[10, 40], [20, 30, 50]]
heap = [(run[0], r, 0) for r, run in enumerate(runs)]
heapq.heapify(heap)
value, r, idx = heapq.heappop(heap)
print(value, r)
broken — fix itIndexError

This crashes instead of printing the sorted list. Fix the off-by-one in the guard before pushing a run's next item.


🎯 Your turn

Build external_sort(data, memory_limit): return data fully sorted ascending, by splitting it into chunks of at most memory_limit items (sorted individually — "run generation"), then merging every run in one pass with a min-heap ("k-way merge").

  • external_sort([5, 3, 8, 1, 9, 2, 7, 4, 6], 3) → [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • external_sort([], 3) → []

Hint: push (run[0], run_index, 0) for every non-empty run onto a heap. Pop the smallest, append its value to the output, and if that run has a next item, push (run[idx + 1], run_index, idx + 1).

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upSplit into chunks

Write split_into_runs(data, size), splitting data into consecutive chunks of length size (the last chunk may be shorter), without sorting. split_into_runs([1, 2, 3, 4, 5, 6, 7], 3) -> [[1, 2, 3], [4, 5, 6], [7]].

arrays
DrillSort each run in memory

Write sort_runs(chunks), returning a new list where each chunk is individually sorted ascending. sort_runs([[3, 1, 2], [9, 5]]) -> [[1, 2, 3], [5, 9]].

sorting
BuildMerge two sorted runs

Write merge_two_runs(a, b), merging two already-sorted lists into one sorted list, without calling sorted(). merge_two_runs([1, 3, 5], [2, 4, 6]) -> [1, 2, 3, 4, 5, 6].

sorting
BossMerge k sorted runs at once

Write k_way_merge(runs), merging any number of sorted lists into one sorted list using a min-heap that holds at most one item per run at a time. k_way_merge([[1, 4, 7], [2, 5, 8], [3, 6, 9]]) -> [1, 2, 3, 4, 5, 6, 7, 8, 9].

heapsk-way-merge
CapstoneHow many disk passes?

Write merge_pass_count(num_runs, fan_in): the total number of disk passes external sort needs — 1 for run generation, plus ceil(log_fan_in(num_runs)) merge passes. num_runs <= 1 needs just the 1 generation pass. merge_pass_count(64, 4) -> 4.

external-sortcomplexity
external_sort.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
external_sort(data, memory_limit) → listdata sorted ascending, simulating memory-sized run generation followed by one k-way merge.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.