← SciSim / Computer Science

🔢 Counting Sort

Non-Comparison Sort | Integer Keys | O(n+k) Time

Standard Undergraduate

§1 — Interactive Simulation

Speed:
Count
Phase
-
Current Index
0
Step
10
n
COUNTING-SORT(A, k): // k = max value
  C[0..k] ← all zeros // count array
  for i ← 0 to n-1: C[A[i]]++ // count
  for i ← 1 to k: C[i] += C[i-1] // prefix sum
  B[0..n-1] ← output array
  for i ← n-1 downto 0: // place (stable)
    B[C[A[i]]-1] ← A[i]
    C[A[i]]--
  return B
Show Count Array
Show Output Array

§2 — The Idea, Step by Step

Picture a teacher with a tall stack of graded exams, every score a whole number from 0 to 100. Most sorting methods compare two papers at a time. Counting sort refuses to compare anything at all. Instead it lays out a labelled tray for every possible score, drops each exam into the tray that matches its score, then sweeps the trays from low to high and stacks the papers back up. They come out in order, and no two papers were ever held side by side.

That tallying is the whole trick. Call the list length $n$, and let every value sit in the range $0$ to $k$. First build a count array $C$: walk the input once and, for each value $v$, do $C[v]\,{+}{+}$. For the list $[3,1,4,1,5]$ you finish with $C[1]=2$, $C[3]=1$, $C[4]=1$, $C[5]=1$ — a histogram of the data. One pass, $n$ steps.

Now turn those counts into positions. Replace each entry with a running total, $C[v]\mathrel{+}=C[v-1]$, so that $C[v]$ becomes the number of elements less than or equal to $v$ — which is exactly the slot where the last $v$ belongs. Then walk the input backward: for each value $A[i]$, place it at $B[\,C[A[i]]-1\,]$ and decrement $C[A[i]]$. Going backward keeps equal keys in their original order, which makes counting sort stable — the property that lets Radix Sort chain it digit by digit.

Add the passes and the cost is $O(n) + O(k) + O(n) = O(n+k)$. When the range is modest, $k = O(n)$, that is plain linear time — faster than the $\Omega(n\log n)$ floor that traps every comparison sort. The catch is memory: the trays need $O(k)$ space, so a huge value range ruins it. The sliders above set exactly these two knobs — Array Size is $n$, Max Value is $k$.

Try this in the sim above. Pick the Small k (0–3) preset and watch just a handful of trays do all the work — the count array stays tiny while the output fills cleanly. Switch to Many Duplicates and notice one count bar tower over the rest, yet the running time barely changes. Finally, step through the placement phase slowly and watch the cursor move right to left: that backward sweep is stability happening in front of you.

§3 — Complexity Analysis & Derivation

SymbolMeaning
nNumber of input elements
kRange of values [0..k]
C[0..k]Count/prefix-sum array
B[0..n-1]Output array
Step 1 — Phase 1: Counting. Loop i=0 to n−1: increment C[A[i]]. Exactly n operations → O(n).
Step 2 — Phase 2: Prefix Sum. Loop i=1 to k: C[i] += C[i−1]. After this, C[v] = number of elements ≤ v. Exactly k operations → O(k).
Step 3 — Phase 3: Output Placement. Traverse A backwards (for stability). For each A[i]: place at B[C[A[i]]−1], then C[A[i]]--. Exactly n operations → O(n).
Step 4 — Total Time. T(n,k) = O(n) + O(k) + O(n) = O(n+k). When k = O(n), this is O(n). This beats the Ω(n log n) comparison sort lower bound because it does NOT compare elements.
Step 5 — Space Complexity. C[] needs k+1 space, B[] needs n space. Total: O(n+k). If k ≫ n (e.g., k=10⁹, n=10), Counting Sort is worse than comparison sorts.
Step 6 — Stability. STABLE if placement loop runs right-to-left (i = n−1 downto 0). Running left-to-right would be unstable. Stability makes Counting Sort the basis of Radix Sort.
Step 7 — When to use. Optimal when k = O(n). Grade sorting (0–100), character sorting (0–255), age sorting (0–150). Never use when k can be large or values are not integers.

§4 — Frequently Asked Questions

Q1. Why traverse backwards in the placement phase?concept
Traversing A from right to left ensures stability: equal elements that appeared later in A are placed later in B. Traversing left to right would reverse their relative order. This matters in Radix Sort where Counting Sort is used as a stable subroutine on individual digits.
Q2. How does the prefix sum tell us where to place each element?concept
After prefix sum, C[v] = count of elements ≤ v. So the last occurrence of value v should go to position C[v]−1 (0-indexed). After placing it, we decrement C[v] so the next occurrence of v goes to position C[v]−2, and so on. This ensures all v's are placed in contiguous, correct positions.
Q3. Can Counting Sort handle negative numbers?applied
Not directly. Standard solution: find min value, shift all elements by −min so the range becomes [0, max−min]. Apply Counting Sort on shifted values. Shift back at the end. This works but adds O(n) preprocessing. k = max−min in this case.
Q4. What happens when k is very large (e.g., sorting 32-bit integers)?complex
k = 2³² ≈ 4 billion. C[] would need 4GB of memory. This is why Counting Sort alone can't sort general integers. Radix Sort solves this by sorting digit by digit, using Counting Sort with k=10 or k=256 repeatedly — each pass is cheap.
Q5. Does Counting Sort violate the Ω(n log n) lower bound?nonobv
No contradiction. The Ω(n log n) lower bound applies only to comparison-based sorts. Counting Sort uses the actual integer values as array indices — it's not comparing A[i] with A[j]. It exploits additional structure (integer keys in a bounded range) that comparison sorts cannot.
Q6. Counting Sort vs Bucket Sort vs Radix Sort — when to use which?deep
Counting Sort: integer keys, small k (k = O(n)). Bucket Sort: uniformly distributed real numbers in [0,1), O(n) expected. Radix Sort: large integers via multiple Counting Sort passes on digits, O(d·n) where d = number of digits. For sorting IP addresses or DNA sequences, Radix Sort is typically fastest. For sorting exam grades, Counting Sort wins.

§5 — Common Misconceptions

Conceptual Misconceptions

❌ "Counting Sort is always O(n), ignoring k."

✅ Time is O(n+k). If k = n², Counting Sort is O(n²), worse than comparison sorts. The k=O(n) condition is critical.

❌ "The count array directly gives sorted output."

✅ Count array only tells how many times each value appears. The prefix sum and backward placement phases are needed for the actual sorted array, especially for stability.

❌ "Counting Sort works on floating-point numbers."

✅ Counting Sort requires integer keys to use as array indices. Floats cannot be used as indices without conversion (which changes the algorithm fundamentally).

Implementation Misconceptions

❌ Initializing C[] with size n instead of k+1.

✅ C[] must have size k+1 (one slot for each possible value 0 through k). Using n causes index-out-of-bounds when A[i] > n−1.

❌ Running placement loop forward (i=0 to n−1) when stability is needed.

✅ Stable placement requires backward traversal (i=n−1 downto 0). Forward traversal produces a sorted output but is unstable — equal elements' relative order is reversed.

❌ Forgetting to decrement C[A[i]] after placement.

✅ C[A[i]]-- is essential. Without it, all duplicates would be placed at the same position, overwriting each other.