E ExamMaster

Data Structures & Algorithms · Data Structures & Algorithms

Searching and Sorting

Comparison sorts like merge and quick sort plus linear and non comparison sorts.

Eight concepts on comparison and non-comparison sorting — merge and quick sort, the \Omega(n \log n) lower bound, heap sort, counting and radix, and why stability decides multi-key sorts.

  • Data Structures & Algorithms
  • Medium level
  • 8 concepts
  • 5 practice questions

1Merge sort: divide, sort, merge

Merge sort splits the array in half, recursively sorts each half, then merges the two sorted runs into one. Every merge of two runs of total length m costs O(m) comparisons and writes, and the recursion tree has \Theta(\log n) levels, so the whole sort is O(n \log n) in every case. The price is an O(n) auxiliary buffer for the merge. Equal keys keep their relative order when the merge prefers the left run on ties — merge sort is stable.

Figure. Last merge of the worked example: left run [2, 5] with right run [1, 3, 4] yields [1, 2, 3, 4, 5].

How it works

  1. SplitCut at mid until every piece has one element.
  2. MergeWalk two sorted runs with two pointers; always take the smaller head (left on ties).
  3. Cost\log n levels × O(n) work per level ⇒ O(n \log n); need an O(n) buffer.

Top-down merge sort

def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left = merge_sort(a[:mid])
    right = merge_sort(a[mid:])
    return merge(left, right)

def merge(left, right):
    out = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i]); i += 1
        else:
            out.append(right[j]); j += 1
    out.extend(left[i:])
    out.extend(right[j:])
    return out

Merge-sort [5, 2, 4, 1, 3]

Sort [5, 2, 4, 1, 3] with top-down merge sort (mid = len//2). List the merges.

  • split → [5, 2] | [4, 1, 3]recurse both halves
  • merge [5] and [2][2, 5]
  • merge [1] and [3], then with [4][1, 3, 4]
  • merge [2, 5] and [1, 3, 4][1, 2, 3, 4, 5]

Pro tip. Need guaranteed O(n \log n) and stability — merge sort is the default, including on linked lists where random access is expensive.

Coding lab. Merge-sort [5, 2, 4, 1, 3] runs in the app, with checks on your output.

Merge sort's worst-case time and extra space are
  1. O(n \log n) time, O(1) extra space
  2. O(n \log n) time, O(n) extra space
  3. O(n^2) time, O(n) extra space

Every input pays \Theta(n \log n) comparisons across the levels, and the merge buffer is linear. In-place O(1) extra space is heap sort's trade-off, not merge sort's.

2Quick sort: partition around a pivot

Quick sort picks a pivot, partitions so every left element is \leq pivot and every right element is \geq pivot, then recurses on the two sides. Average time is O(n \log n) when partitions are reasonably balanced; the partition itself is in-place, and recursion uses O(\log n) stack on average. Unlike merge sort, the classic in-place form is not stable.

Figure. After partitioning [3, 1, 4, 2] with pivot 2: array is [1, 2, 4, 3]. Pivot sits at index 1.

Lomuto partition sketch

  1. PivotChoose an element (here: last) and treat it as the split value.
  2. ScanGrow a left region of values ≤ pivot; swap each such value into place.
  3. Place pivotSwap the pivot into the boundary; recurse on left and right sides.

Quick sort with Lomuto partition

def quick_sort(a, lo=0, hi=None):
    if hi is None:
        hi = len(a) - 1
    if lo >= hi:
        return
    p = partition(a, lo, hi)
    quick_sort(a, lo, p - 1)
    quick_sort(a, p + 1, hi)

def partition(a, lo, hi):
    pivot = a[hi]
    i = lo - 1
    for j in range(lo, hi):
        if a[j] <= pivot:
            i += 1
            a[i], a[j] = a[j], a[i]
    a[i + 1], a[hi] = a[hi], a[i + 1]
    return i + 1

One Lomuto partition

Partition [3, 1, 4, 2] with Lomuto, pivot = last element 2. Report the array after partition and the pivot index.

  • pivot = 2; scan j over 3, 1, 4only 1 ≤ 2
  • swap 3 with 1 (i becomes 0)[1, 3, 4, 2]
  • swap pivot into i+1[1, 2, 4, 3], pivot index 1

Pro tip. Prefer quick sort when average speed and in-place behaviour matter more than stability or a hard worst-case guarantee.

After a correct partition with pivot value p, which statement must hold?
  1. The whole array is fully sorted
  2. Every element left of p's final index is \leq p, and every element right is \geq p
  3. The recursion depth is always \lfloor \log_2 n \rfloor

Partition only places the pivot and separates lesser/greater sides; the sides are still unsorted. Depth is logarithmic only on balanced splits.

3Quick sort's O(n^2) trap

If every pivot is an extreme (smallest or largest remaining value), each partition peels off only one element and the recurrence becomes T(n) = T(n-1) + O(n). That sums to \Theta(n^2). Already-sorted input with a naive first/last pivot is the classic trigger. Randomizing the pivot (or median-of-three) makes that adversarial pattern vanishingly unlikely without changing the average O(n \log n) story.

Figure. A sorted input with a fixed end pivot yields n+(n-1)+\cdots partitions — randomise or median-of-three.

Why it squares

  1. Bad pivotPartition sizes become 0 and n-1 every time.
  2. Cost sumWork n + (n-1) + \cdots + 1 = n(n+1)/2.
  3. MitigationShuffle, pick a random pivot, or use median-of-three before partitioning.

Degenerate cost for n = 5

In the fully unbalanced case, how many element-touching partition steps accumulate for n = 5?

  • sizes peeled5 + 4 + 3 + 2 + 1
  • 5(5+1)/215

Pro tip. Name the O(n^2) case — sorted input with a fixed end pivot — then say how you would randomize the pivot.

Quick sort with always-last pivot on an already-sorted array is typically
  1. O(n \log n) worst case because the array is sorted
  2. O(n^2) because each pivot splits off one element
  3. O(n) because no swaps are needed

Sorted order plus a fixed end pivot makes every partition maximally unbalanced. Swaps may be few, but the recursion depth and scanned prefixes still sum to quadratic work.

4Comparison sorts need \Omega(n \log n)

Any correct comparison-based sort must distinguish among n! possible input permutations. In the decision-tree model each comparison has two outcomes, so a tree of height h has at most 2^h leaves. Need 2^h \ge n!, hence h \ge \log_2(n!). Stirling's approximation gives \log_2(n!) = \Omega(n \log n), so every comparison sort has worst-case \Omega(n \log n) comparisons. Merge, heap and (balanced) quick sort all meet this bound up to constants; counting and radix escape it by not comparing keys.

Figure. Any comparison sort needs \Omega(n\log n) in the worst case; linear sorts cheat with integer structure.

Decision-tree argument

  1. LeavesNeed at least one leaf per permutation ⇒ \ge n! leaves.
  2. Binary treeHeight h\le 2^h leaves ⇒ h \ge \log_2(n!).
  3. Stirling\log_2(n!) = \Omega(n \log n) — the information-theoretic floor.

Lower bound at n = 5

How many comparisons are necessary in the worst case to sort 5 distinct keys by comparisons alone?

  • 5!120 permutations
  • \log_2 120\approx 6.91
  • ceil of that heightat least 7 comparisons

Pro tip. If a claimed O(n) comparison sort appears in a multiple-choice option, the lower bound is the refutation — unless the keys are integers with extra structure.

Why can counting sort beat \Omega(n \log n)?
  1. It uses fewer than n! leaves in its decision tree
  2. It is not comparison-based — it indexes by key value
  3. It only works when n is a power of two

The \Omega(n \log n) bound applies to algorithms that learn order only by comparing keys. Counting sort reads keys as indices into a tally array.

5Heap sort: O(n \log n) in-place

Heap sort builds a max-heap in O(n) time, then repeatedly extracts the maximum into the end of the array. Each extract is O(\log n), so n extracts cost O(n \log n). Extra memory beyond the input array is O(1) (ignoring the call stack of an in-place sift). The sort is not stable: sifting moves equal keys past each other. Choose it when you need worst-case O(n \log n) without merge sort's linear buffer and can tolerate instability.

Figure. Heap sort keeps worst-case O(n \log n) with constant extra memory. Merge needs a linear buffer; quick is leaner but not worst-case safe. Heap sort is not stable.

Two phases

  1. Buildheapify the array — O(n), not O(n \log n).
  2. ExtractSwap root with the last unsorted slot; sift-down; shrink the heap.
  3. Trade-offWorst-case O(n \log n), O(1) extra space, not stable.
Compared with merge sort, heap sort's distinctive trade-off is
  1. Faster asymptotic time: O(n) vs O(n \log n)
  2. O(1) extra space and not stable, versus merge's O(n) buffer and stability
  3. Average O(n^2) like naive quick sort

Both are \Theta(n \log n) comparison sorts in the worst case. Heap sort drops the auxiliary array and loses stability; merge sort keeps stability and pays linear extra space.

6Counting sort for bounded integer keys

When keys are integers in a known range \{0, 1, \ldots, k-1\}, counting sort tallies how many times each key appears, then writes the output from the tallies (or from prefix sums for a stable variant). Time and space are O(n + k). If k = O(n), that is linear — beating the comparison lower bound because keys are used as indices, not compared. Unusable when k is huge (e.g. arbitrary 64-bit keys with no digit structure).

Figure. Input [2, 0, 2, 1, 0] tallies to count[0]=2, count[1]=1, count[2]=2 before emit.

Tally then emit

  1. CountAllocate size-k array; increment count[key] for each input.
  2. EmitWalk values 0..k-1; append each value count[v] times (stable forms use prefix sums and a reverse pass).
  3. WhenOnly when k is modest relative to n — otherwise memory and the O(k) term dominate.

Counting sort (simple emit)

def counting_sort(a, k):
    count = [0] * k
    for x in a:
        count[x] += 1
    out = []
    for v in range(k):
        out.extend([v] * count[v])
    return out

Small array, then the 10M case

Sort [2, 0, 2, 1, 0] with keys in \{0,1,2\}. Then compare O(n+k) to O(n \log n) for n = 10^7 keys in 0..999.

  • counts for 0, 1, 2[2, 1, 2]
  • emit from counts[0, 0, 1, 2, 2]
  • n=10^7, k=1000n+k1.0001 \times 10^7
  • n \log_2 n for same n\approx 2.33 \times 10^8 (~23× larger)

Pro tip. Sorting millions of small-range integers? Reach for counting (or radix) before merge or quick.

Counting sort on n keys from 0..k-1 runs in
  1. O(n \log n) always
  2. O(n + k)
  3. O(k \log n)

One pass over n keys plus work proportional to the k buckets. No \log n factor from comparisons.

7Radix sort: digit-by-digit linear passes

Radix sort sorts integers (or fixed-length strings) one digit at a time. Least-significant-digit (LSD) radix runs a stable counting sort on digit 0, then digit 1, and so on through d digits in base b. Each pass is O(n + b); d passes give O(d(n + b)). When d is constant (or O(1) relative to word size) and b is modest, total time is linear in nagain outside the comparison model.

Each digit pass reuses the tally strip from Counting sort for bounded integer keys — stable counts on the current digit, LSD to MSD, until every place is ordered.

LSD outline

  1. Stable passCounting-sort on the current digit; stability preserves earlier digit order.
  2. Next digitMove to the next more-significant digit; repeat.
  3. Costd passes ⇒ O(d(n+b)). Pick base b to balance pass count vs bucket work.

Two LSD passes on three keys

Sort [23, 15, 21] with LSD radix in base 10 (ones, then tens).

  • ones digits (3, 5, 1)order after ones: [21, 23, 15]
  • tens digits (2, 2, 1)order after tens: [15, 21, 23]

Pro tip. Radix needs a stable digit sort. Unstable counting on a digit wrecks the order established by previous passes.

LSD radix sort requires the per-digit sort to be
  1. In-place and unstable is fine
  2. Stable, so earlier digit order survives
  3. Comparison-based so the lower bound applies

After sorting on the ones place, equal tens must keep the ones-order among ties. That is exactly stability.

8Stability and choosing a sort

A sort is stable when equal keys keep their input order. That matters when you sort by a secondary key first and a primary key second — stability on the second pass preserves the secondary order among ties. Merge sort and properly implemented counting/radix are stable; typical in-place quick sort and heap sort are not. Pick merge for guaranteed O(n \log n) plus stability; quick for fast in-place average case when stability is irrelevant; counting/radix when keys are short integers; heap when you need worst-case O(n \log n) with O(1) extra space.

Figure. Stability keeps equal keys in their original relative order — required when you sort by a second key later.

Decision cues

  1. Stable + guaranteedMerge sort (also natural on linked lists).
  2. In-place average-fastQuick sort — randomize the pivot; drop stability.
  3. Small integer rangeCounting or radix for linear O(n+k) / O(d(n+b)).
Stable or not
AlgorithmStable?
Merge sortYes
Quick sort (typical in-place)No
Heap sortNo
Counting / radix (stable digit sort)Yes
You must sort records by name, then by age, keeping name order among equal ages. Which property do you need on the age pass?
  1. In-place partition
  2. Stability
  3. O(1) auxiliary memory

After sorting by name, the age pass must not reorder equal ages past each other, or the name order among those ties is lost. That is stability.

Notes

  • Merge Sort: A stable divide-and-conquer sort that splits, recursively sorts, and merges, guaranteeing O(n \log n) but needing O(n) extra space.
  • Quick Sort: Partitions around a pivot; average O(n \log n) and in-place, but worst case O(n^2) on bad pivots (mitigated by randomized/median-of-three pivots).
  • Comparison Sort Lower Bound: Any comparison-based sort needs at least \Omega(n \log n) comparisons in the worst case.
  • Non-Comparison Sorts: Counting sort and radix sort achieve linear time when keys are bounded integers, bypassing the comparison lower bound.
  • Stability: A stable sort preserves the relative order of equal keys, which matters when sorting by multiple criteria.

Formulas

  • Merge sort: time O(n \log n) (all cases), space O(n).
  • Quick sort: average O(n \log n), worst O(n^2), space O(\log n) for recursion.
  • Heap sort: time O(n \log n), space O(1), not stable.
  • Counting sort: O(n + k) time for key range k; radix sort: O(d(n+b)) for d digits, base b.
  • Comparison-sort lower bound: \Omega(n \log n).

Exam traps & shortcuts

  • Need guaranteed O(n \log n) and stability => merge sort; need in-place average-fast and stability is irrelevant => quick sort.
  • Sorting integers in a small known range => counting/radix sort for linear time.
  • Quick sort degrades to O(n^2) on already-sorted input with a naive pivot; randomize the pivot to avoid it.
  • If asked to sort a linked list, merge sort is preferred because it needs no random access and is O(n \log n).

Reference tables

Standard comparison and integer sorts. Quick sort's worst case assumes adversarial pivots; randomized pivot restores expected O(n \log n).

Complexity sheet
AlgorithmBest / avg / worst timeExtra spaceStable
Merge sortO(n \log n) / O(n \log n) / O(n \log n)O(n)Yes
Quick sortO(n \log n) / O(n \log n) / O(n^2)O(\log n) avg stack; O(n) worstNo*
Heap sortO(n \log n) / O(n \log n) / O(n \log n)O(1)No
Counting sortO(n+k)O(n+k)Yes†
Radix sortO(d(n+b))O(n+b)Yes†

Cues for choosing a sort. *Typical in-place quick sort. †With a stable digit/counting pass.

When to reach for which
NeedFirst tool
Guaranteed O(n \log n) + stableMerge sort
In-place, average-fast, stability irrelevantQuick sort (random pivot)
Worst-case O(n \log n), O(1) extra spaceHeap sort
Integers in a small known rangeCounting sort
Fixed-width integers / strings by digitRadix sort
Sort a linked listMerge sort

Recap

Comparison sorts sit on an \Omega(n \log n) floor; integer sorts escape it. Stability and space decide among merge, quick and heap.

Merge
Always O(n \log n), O(n) buffer, stable — linked lists too.
Quick
Average O(n \log n) in-place; naive pivot on sorted input ⇒ O(n^2).
Lower bound
\log_2(n!)=\Omega(n \log n) comparisons — no comparison sort beats it.
Heap sort
Worst-case O(n \log n), O(1) extra space, not stable.
Counting / radix
O(n+k) or O(d(n+b)) when keys are short integers.
Stability
Equal keys keep order — required for multi-key sort passes.

Practise Searching and Sorting

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 5 exam-style questions on this topic, with explanations
  • A 6-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.