Non-Comparison Sorting Algorithms
Counting sort, radix sort, and bucket sort beat the O(n log n) comparison-sort floor by never comparing elements at all — they exploit known structure in the keys (small integer range, fixed digit count, or uniform distribution) to compute each element's final position directly through arithmetic and bucketing.
Recognize the pattern
- Keys are integers (or map cleanly to integers) with a range k that is not much larger than n.
- Problem says "sort ages 0-120", "sort grades", "sort by digit", or values are floats uniformly spread in [0,1).
- You need stable sorting as a subroutine (e.g., before radix sort, or sorting records by one field while preserving order on another).
- Interviewer hints at "can you beat O(n log n)?" — that phrase is the tell for a non-comparison sort.
Brute force → optimal
Brute force: comparison sort (merge/quick/heap sort) treats keys as opaque and compares pairs, paying Θ(n log n) comparisons no matter what the data looks like — this is provably optimal for comparison-based sorts (decision-tree lower bound) but wasteful when keys are small integers.
Optimal (counting sort): instead of comparing, count how many elements are ≤ each value, then place each element directly at its computed index. Cost is proportional to n + k, not n log n. When k = O(n), this is linear — strictly better than the comparison floor because it uses information (bounded integer range) that a generic comparison sort ignores.
Complexity from first principles
Counting sort
Three linear passes: (1) tally counts — n operations over the input; (2) prefix-sum the count array — k operations; (3) place elements by reading input right-to-left and writing to output using the count array — n operations. Total operations = n + k, so Time = O(n + k). Space: one count array of size k+1 plus one output array of size n → Space = O(n + k).
Radix sort
Run a stable counting sort (base b, so k=b) once per digit position. If the largest key has d digits, total work = d × O(n + b). With b=10 and d = O(log_b(max value)), Time = O(d·(n+b)); for fixed-width keys (e.g. 32-bit ints, d≈constant) this is effectively O(n). Space = O(n + b) (reused per pass).
Bucket sort
Distribute n elements into m buckets (an O(n) scan); if input is uniformly distributed, each bucket gets ~n/m elements. Sorting each bucket with insertion sort costs O((n/m)²) expected per bucket; summed over m buckets, expected Time = O(n + n²/m), which is O(n) when m = Θ(n). Worst case (all elements in one bucket): O(n²). Space = O(n + m).
Traced example — Counting sort on [4,2,2,8,3,3,1]
| Step | State |
|---|---|
| Max value | 8 → count array size 9 (indices 0..8) |
| Tally | count = [0,1,2,2,1,0,0,0,1] (index i = frequency of value i) |
| Prefix sum | count = [0,1,3,5,6,6,6,6,7] (count[i] = # elements ≤ i) |
| Place (right→left through input 4,2,2,8,3,3,1) | place 1→output[count[1]-1=0]=1, dec count[1]=0; place 3→output[4]=3, count[3]=4; place 3→output[3]=3, count[3]=3; place 8→output[6]=8, count[8]=6; place 2→output[2]=2, count[2]=2; place 2→output[1]=2, count[2]=1; place 4→output[5]=4, count[4]=5 |
| Result | [1,2,2,3,3,4,8] — sorted, and the two 3's kept their original relative order (stable) |
Reference implementation (Java)
static void countingSort(int[] arr) {
int max = Arrays.stream(arr).max().getAsInt();
int[] count = new int[max + 1];
for (int x : arr) count[x]++;
for (int i = 1; i <= max; i++) count[i] += count[i - 1];
int[] output = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
System.arraycopy(output, 0, arr, 0, arr.length);
}
static void radixSort(int[] arr) {
int max = Arrays.stream(arr).max().getAsInt();
for (int exp = 1; max / exp > 0; exp *= 10) {
int[] output = new int[arr.length];
int[] count = new int[10];
for (int x : arr) count[(x / exp) % 10]++;
for (int i = 1; i < 10; i++) count[i] += count[i - 1];
for (int i = arr.length - 1; i >= 0; i--) {
int digit = (arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
System.arraycopy(output, 0, arr, 0, arr.length);
}
}Pitfalls
- Forgetting stability in the placement loop (e.g., iterating left-to-right instead of right-to-left) silently breaks radix sort, which depends on each digit pass being stable to build on the previous pass's order.
- Counting sort on a huge range — if k ≫ n (e.g., sorting 100 numbers spread across 0..10^9), the count array dominates memory and time; counting sort degrades badly and a comparison sort wins.
- Radix sort on negative numbers — the naive digit extraction breaks; requires an offset/bias step or separate handling of sign.
- Bucket sort with skewed input — if the distribution isn't roughly uniform, one bucket absorbs most elements and you pay O(n²) via that bucket's insertion sort.
When to use / when NOT — vs comparison sorts
| Situation | Choice |
|---|---|
| Integer keys, range k = O(n) | Counting sort — O(n+k), beats O(n log n) |
| Fixed-width integers/strings, large range but bounded digits | Radix sort — O(d·n), independent of value magnitude |
| Uniformly distributed reals in a known interval | Bucket sort — expected O(n) |
| Arbitrary/unknown key distribution, need worst-case guarantee, or sorting by a general comparator | Merge/quick/heap sort — O(n log n), no assumptions needed |
| Very large k, or negative/floating keys without preprocessing | Avoid counting/radix — memory or correctness cost outweighs gains |
Trade-off in one line: comparison sorts trade a guaranteed log n factor for zero assumptions about the data; non-comparison sorts trade that generality for linear time when the key structure cooperates.
Takeaways
- Non-comparison sorts win by using arithmetic on keys instead of pairwise comparisons — this only works when keys are integers (or digit-decomposable) with bounded range.
- Counting sort's core trick is the prefix-summed count array: count[i] becomes "how many elements are ≤ i", which is exactly the final index.
- Radix sort is just counting sort applied once per digit — its stability requirement per pass is non-negotiable.
- Bucket sort's performance is a bet on distribution uniformity; it degrades to O(n²) when that bet fails.
Recall: Why must the inner counting-sort pass in radix sort be stable, and what breaks if it isn't?
Synthesized from CLRS (Counting Sort, Radix Sort, Bucket Sort chapters) and standard interview-prep sorting references.
🤖 Don't fully get this? Learn it with Claude
Stuck on Non-Comparison Sorting Algorithms? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Non-Comparison Sorting Algorithms** (DSA) and want to truly understand it. Explain Non-Comparison Sorting Algorithms from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Non-Comparison Sorting Algorithms** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Non-Comparison Sorting Algorithms** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Non-Comparison Sorting Algorithms** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.