Ten million exam scores all fall in the range 0–100. A comparison sort orders them in Θ(n log n) because comparing two keys is its only source of information, and distinguishing the n! possible orderings takes log₂(n!) ≈ n log n yes/no answers. Counting Sort throws comparison out: a score of 73 is never ranked against its neighbours — its value is an address. The algorithm tallies how many keys hold each value across [0, k], turns those tallies into end positions with a running sum, then writes each element straight into the slot its value names. Indexing by value sidesteps the Ω(n log n) lower bound — that bound governs only sorts whose sole move is comparing pairs — and drops the cost to Θ(n + k). The price is a hard domain assumption: keys must be integers, or map to them, over a range k small enough that an array of k counters is affordable.

A trace of Counting Sort would run over [2, 5, 3, 0, 2, 3, 0, 3] with k = 5.

Visualization pending

Planned StepTrace: a histogram/counts card showing the count array filling, the prefix-sum pass, then stable placement into the output. No matching renderer exists in engine.js yet.

Core condition: integer keys over a known range [0, k] → index by value instead of comparing → Θ(n + k) time and Θ(n + k) auxiliary space, stable.

Why the value is an address

Three linear passes, none of them a comparison:

  1. Tally. One scan fills count[0..k], where count[v] is the number of elements whose key equals v. For [2, 5, 3, 0, 2, 3, 0, 3] with k = 5 the tally is [2, 0, 2, 3, 0, 1].
  2. Prefix sum. Replacing count with its running total makes count[v] the number of keys ≤ v, which is exactly the index one past the last slot value v may occupy. The tally becomes [2, 2, 4, 7, 7, 8].
  3. Place. Walking the input from last element to first, each element decrements count[key] and is written at that index. The result is [0, 0, 2, 2, 3, 3, 3, 5].

The invariant the prefix sum establishes is that count[v] marks the end of the contiguous block reserved for value v. Decrementing before every write fills that block from its top slot downward.

Stability falls out of the placement direction. Equal keys share one block, and because the input is consumed tail-first, the element appearing last among equal keys lands in the block’s highest slot while earlier ones fill beneath it — original relative order survives. Reverse the loop and the same decrement scheme emits equal keys backwards. Stability is discretionary for a standalone sort but a correctness requirement when Counting Sort is the per-digit pass inside Radix Sort, which produces wrong output the moment a digit pass reorders equal keys.

No arrangement of the input alters this. The two Θ(n) scans and the Θ(k) prefix sum run identically whether the data arrives sorted, reversed, or random; the work is fixed by n and k alone. Whatever can go wrong is therefore a property of k, not of the data’s order.

Complexity

CaseTimeAuxiliary spaceCause
BestΘ(n + k)Θ(n + k)Two input scans plus one sweep of the k-cell counter, independent of order.
AverageΘ(n + k)Θ(n + k)The same three passes; no input distribution changes the pass count.
WorstΘ(n + k)Θ(n + k)No adversarial ordering exists — cost is set by n and k, never arrangement.

The bound is tight in every case, so Θ rather than O is the honest notation. Auxiliary space splits into the k-cell count array and the n-cell output buffer, so Counting Sort is not in-place: placement reads the original keys while writing a separate array. Done as above — prefix sum followed by tail-first placement — it is stable.

When indexing by value breaks

Every failure traces to the same assumption: the key can serve as an array index.

k ≫ n inverts the economics. Sorting eight 64-bit integers by their raw value asks for a 2^64-cell count array — value-as-address needs one counter per representable key, not one per element present. The + k term, invisible when k = O(n), becomes the entire cost, and the allocation fails long before the eight elements are placed. Radix Sort exists for exactly this case: it sorts wide keys through several Counting Sort passes over a fixed small digit base, holding each pass’s k down.

Non-integer or unbounded keys have no index. A floating-point value, a string, or an arbitrary comparable object cannot name a cell in count, because there is no finite integer range to allocate over. Such keys stay with a comparison sort, or — when they distribute smoothly across a range — with Bucket Sort.

Negative keys index before the array starts. A key of -3 addresses count[-3] and throws, or corrupts memory in a language that permits it. The fix is an offset: size count at max - min + 1 and index count[key - min], shifting the domain so its minimum maps to zero. Omitting the offset does not sort incorrectly — it faults on the first negative key.

Reference drawer

Questions

References