Counting, Radix, and Bucket Sort
Every sort so far — quicksort, heap sort, merge sort — decides order by comparing two elements at a time. There’s a proven floor on how fast that can ever go:
is the best any comparison-based sort can guarantee, because sorting must distinguish between possible orderings, and each comparison only answers a yes/no question — you need of them in the worst case. Counting, radix, and bucket sort sidestep this entirely: they never compare two elements to each other. Instead they use the values themselves — as array indices — which unlocks -ish time, if the keys are the right shape (bounded integers, or a range you can bucket).
What you’ll learn
- Counting sort: count occurrences, prefix-sum them into positions, place elements directly — , and stable if done carefully.
- Radix sort (LSD): repeatedly counting-sort by one digit at a time, least-significant first, to sort integers of any size in linear time.
- Bucket sort: scatter into ranged buckets, sort each bucket small, then concatenate — great for uniformly distributed values.
- When each one is (and isn’t) the right tool.
Counting sort: count, then place
Counting sort works when every value is a small non-negative integer in a
known range 0..k0..k. The idea: count how many times each value appears,
turn those counts into prefix sums (so count[v]count[v] becomes “how many
elements are ”), and use that number as each element’s landing spot
in the output.
def counting_sort(arr):
if not arr:
return arr
k = max(arr) # values are 0..k
count = [0] * (k + 1)
for x in arr:
count[x] += 1
for i in range(1, k + 1):
count[i] += count[i - 1] # count[v] = number of elements <= v
output = [0] * len(arr)
for x in reversed(arr): # reversed = keeps equal elements stable
count[x] -= 1
output[count[x]] = x
return output
arr = [4, 2, 2, 8, 3, 3, 1]
print("input: ", arr)
print("sorted:", counting_sort(arr))def counting_sort(arr):
if not arr:
return arr
k = max(arr) # values are 0..k
count = [0] * (k + 1)
for x in arr:
count[x] += 1
for i in range(1, k + 1):
count[i] += count[i - 1] # count[v] = number of elements <= v
output = [0] * len(arr)
for x in reversed(arr): # reversed = keeps equal elements stable
count[x] -= 1
output[count[x]] = x
return output
arr = [4, 2, 2, 8, 3, 3, 1]
print("input: ", arr)
print("sorted:", counting_sort(arr))Three linear passes over the data (count, prefix-sum, place) — no comparisons anywhere. That’s the whole : for the array, for the count array covering the value range.
Watch counting sort place each element
Radix sort: counting sort, one digit at a time
Counting sort’s weakness: if the values span a huge range (say, up to a
billion), kk is huge and the count array becomes impractically large.
Radix sort fixes that by never counting-sorting on the whole value —
only on one digit at a time (base 10, so each digit only needs a
count array of size 10), starting from the least significant digit
(LSD) and working toward the most significant.
def counting_sort_by_digit(arr, exp):
"""Stable counting sort keyed on the digit at place value `exp`
(exp = 1 for ones, 10 for tens, 100 for hundreds, ...)."""
n = len(arr)
output = [0] * n
count = [0] * 10
for x in arr:
digit = (x // exp) % 10
count[digit] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for x in reversed(arr): # reversed again -- stability matters here too
digit = (x // exp) % 10
count[digit] -= 1
output[count[digit]] = x
return output
def radix_sort(arr):
if not arr:
return arr
max_val = max(arr)
exp = 1
while max_val // exp > 0:
arr = counting_sort_by_digit(arr, exp)
exp *= 10
return arr
nums = [170, 45, 75, 90, 802, 24, 2, 66]
print("sorted:", radix_sort(nums))def counting_sort_by_digit(arr, exp):
"""Stable counting sort keyed on the digit at place value `exp`
(exp = 1 for ones, 10 for tens, 100 for hundreds, ...)."""
n = len(arr)
output = [0] * n
count = [0] * 10
for x in arr:
digit = (x // exp) % 10
count[digit] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for x in reversed(arr): # reversed again -- stability matters here too
digit = (x // exp) % 10
count[digit] -= 1
output[count[digit]] = x
return output
def radix_sort(arr):
if not arr:
return arr
max_val = max(arr)
exp = 1
while max_val // exp > 0:
arr = counting_sort_by_digit(arr, exp)
exp *= 10
return arr
nums = [170, 45, 75, 90, 802, 24, 2, 66]
print("sorted:", radix_sort(nums))For digits and numbers, that’s passes of an counting sort each — total, which is linear in for any fixed number of digits (i.e., fixed-width integers, like 32-bit or 64-bit).
Bucket sort: scatter, sort small, concatenate
Bucket sort targets values uniformly spread across a known range (the
classic case: floats in [0, 1)[0, 1)). Scatter each value into one of kk
equal-width buckets by its magnitude, sort each (small) bucket with
whatever comparison sort you like, then concatenate the buckets in order.
def bucket_sort(arr, bucket_count=10):
if not arr:
return arr
buckets = [[] for _ in range(bucket_count)]
for x in arr:
idx = int(x * bucket_count)
idx = min(idx, bucket_count - 1) # guard x == 1.0 landing out of range
buckets[idx].append(x)
for b in buckets:
b.sort() # each bucket is small -- even O(n^2) insertion sort is fine here
result = []
for b in buckets:
result.extend(b)
return result
data = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
print("sorted:", bucket_sort(data))def bucket_sort(arr, bucket_count=10):
if not arr:
return arr
buckets = [[] for _ in range(bucket_count)]
for x in arr:
idx = int(x * bucket_count)
idx = min(idx, bucket_count - 1) # guard x == 1.0 landing out of range
buckets[idx].append(x)
for b in buckets:
b.sort() # each bucket is small -- even O(n^2) insertion sort is fine here
result = []
for b in buckets:
result.extend(b)
return result
data = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
print("sorted:", bucket_sort(data))If the input really is uniformly distributed, every bucket ends up with
roughly n / kn / k elements, so sorting all buckets costs total on
average — the (or worse) inner sorts are each working on a
tiny slice. If the data is instead heavily clustered into one bucket,
you’re back to sorting almost the whole array with one comparison sort,
and the worst case becomes .
When these are (and aren’t) the right tool
- Counting sort: values are small non-negative integers with a known,
small range
kk(e.g., ages, grades, byte values 0-255). Bad idea ifkkis huge relative tonn— you’d allocate a giant, mostly-empty array. - Radix sort: fixed-width integers (or strings treated digit-by-digit)
of any magnitude — the digit count
ddstays small (10 for a billion) even when the values themselves are huge. - Bucket sort: values you know are roughly uniformly distributed
over a range — floats in
[0, 1)[0, 1), timestamps spread across a day, etc. Clustered/skewed data defeats it. - None of these three handle arbitrary comparable objects (strings by
dictionary order, custom objects) as directly as
sorted()sorted()does — that’s what the next lesson, Timsort and custom sorting, covers.
Complexity at a glance
| Sort | Time | Space | Stable? | Needs |
|---|---|---|---|---|
| Counting sort | Yes (with reverse-scan placement) | small integer range kk | ||
| Radix sort (LSD) | Yes | fixed-width integers, dd digits, base bb | ||
| Bucket sort | avg, worst | Depends on the inner sort | roughly uniform distribution |
kk = value range (counting sort) or bucket count (bucket sort); dd =
number of digits; bb = digit base (10 for decimal digits).
Practice — real LeetCode problems
Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 1122 — Relative Sort Array · Easy
Problem. Sort arr1arr1 so that its elements follow the relative order given by
arr2arr2. Elements not present in arr2arr2 go at the end, in ascending order.
Constraints. 1 <= len(arr1), len(arr2) <= 10001 <= len(arr1), len(arr2) <= 1000, 0 <= values <= 10000 <= values <= 1000, arr2arr2
has distinct values, all of which appear in arr1arr1.
Examples. arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] gives
[2,2,2,1,4,3,3,9,6,7,19][2,2,2,1,4,3,3,9,6,7,19]
Editorial
Converting “this arbitrary order” into a numeric rank turns the problem into a standard sort. The tuple key then layers the fallback rule on top.
Time . Space .
rank.get(v, len(arr2))rank.get(v, len(arr2)) is doing two jobs: it supplies the rank for known values,
and it assigns unknown values a rank strictly larger than every known one, so they
sort last. The second tuple component then orders them ascending among themselves —
which is exactly what the problem asks.
Because values are bounded by 1000, a counting sort is the
alternative and the direct connection to this page: tally arr1arr1, then emit values in
arr2arr2’s order followed by the remaining values in ascending index order. That avoids
comparison sorting entirely.
[28,6,22,8,44,17][28,6,22,8,44,17] shows the fallback: 1717 and 4444 are unranked and appear at the
end in ascending order.
Follow-ups: “Do it with counting sort?” — the bounded-range version above; the
expected answer on this page. “What if arr2arr2 had values absent from arr1arr1?” — the
rank map is unaffected. “Descending fallback?” — negate the second key component.
LC 274 — H-Index · Medium
Problem. Given citation counts for a researcher’s papers, return their h-index:
the largest hh such that at least hh papers have at least hh citations each.
Constraints. 1 <= len(citations) <= 50001 <= len(citations) <= 5000, 0 <= citations[i] <= 10000 <= citations[i] <= 1000.
Examples. [3,0,6,1,5][3,0,6,1,5] gives 33 · [1,3,1][1,3,1] gives 11 · [100][100] gives 11
Editorial
The key observation is that the h-index cannot exceed the number of papers, so any
citation count above nn is indistinguishable from nn for this purpose. That caps the
value range at n + 1n + 1, which is exactly the condition counting sort needs.
Time . Space .
Walking hh downward and accumulating gives “how many papers have at least hh
citations” without re-scanning: each step adds the papers with exactly hh. The first
hh where the running total reaches hh is the answer, and because we come down from
the top it is the largest such hh.
[100][100] giving 11 is the capping case: one paper with 100 citations still yields an
h-index of only 1, since there is only one paper. [0][0] and [0,0][0,0] give 00.
The answer — sort descending and find the last index where
citations[i] >= i + 1citations[i] >= i + 1 — is simpler and worth stating first. The counting version is
the improvement this page exists to teach.
Follow-ups: “Input already sorted (LC 275)?” — binary search for the crossover in
. “Why cap at nn?” — the bound argument; this is the expected question.
“What is the g-index?” — a related metric using cumulative citations, solvable the
same way.
LC 164 — Maximum Gap · Medium
Problem. Return the maximum difference between two successive elements in the
array’s sorted form. If the array has fewer than two elements, return 00. Aim
for linear time and space.
Constraints. 1 <= len(nums) <= 10^51 <= len(nums) <= 10^5, 0 <= nums[i] <= 10^90 <= nums[i] <= 10^9.
Examples. [3,6,9,1][3,6,9,1] gives 33 · [10][10] gives 00
Editorial
Sorting and scanning adjacent pairs is and accepted. Start there.
Time as written. Space beyond the sort.
The problem asks for linear time, and the route is a pigeonhole / bucket argument worth understanding:
With nn values spanning max - minmax - min, the average gap is
(max - min) / (n - 1)(max - min) / (n - 1). The maximum gap must be at least the average, so if you
create buckets of exactly that width, no two values inside a single bucket can be the
maximum gap apart — meaning the answer always spans a boundary between buckets.
So you only need each bucket’s minimum and maximum, then take the largest difference between one bucket’s max and the next non-empty bucket’s min. That is time and space, with no sorting.
Radix sort is the other linear option: values are bounded by , so a handful of digit passes sorts them in .
[1,1,1][1,1,1] giving 00 (all equal) and [10][10] giving 00 (single element) are the
degenerate cases the guard exists for.
Follow-uups: “Achieve ?” — the bucket argument above, or radix sort; this
is the expected follow-up. “Why is the max gap at least the average?” — if every gap
were below average their total would be below max - minmax - min, a contradiction. “Negative
values?” — the constraints exclude them, but the argument is unchanged.
Recap
- Comparison sorts are stuck at — a proven lower bound. Counting, radix, and bucket sort escape it by using key values directly as positions, never comparing elements to each other.
- Counting sort (): count, prefix-sum into positions, place
in reverse for stability. Needs a small integer range
kk. - Radix sort (): repeated stable counting sort, one digit at a time, LSD first — correctness depends on every pass being stable.
- Bucket sort ( average): scatter into ranged buckets, sort each small bucket, concatenate — shines on uniformly distributed data, degrades to if the data clusters into one bucket.
Next: Timsort and Custom Sorting — how Python’s built-in sorted()sorted()
and .sort().sort() actually work, and how to sort arbitrary objects with key=key=
and custom comparators.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
