Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Sorting at Scale  ›  Lesson

Counting, Bucket & Radix Sort

Non-Comparison Sorts 26 minSorting without comparisons: count occurrences, turn counts into positions, and place each item directly
You're building a piece ofQuarry — the ingest sorter
This piece — radix_sort(): Sorts a batch of non-negative record IDs without ever comparing two of them.
Scenario A batch of record IDs lands overnight and needs sorting before the morning read, without ever comparing two IDs directly.
Your task
Build radix_sort(nums). Return a NEW list with nums (non-negative integers) sorted ascending, using LSD radix sort: a stable counting-sort pass per digit, from the ones place up. No sorted(). Example: radix_sort([170, 45, 75, 90, 802, 24, 2, 66]) -> [2, 24, 45, 66, 75, 90, 170, 802].

Counting, Bucket & Radix Sort

In short: every sort in section 3 decided order by comparing two items. Counting sort never compares anything — it counts how many times each value occurs, turns those counts into positions with a running total, then places each item straight into its final slot. That makes it O(n + k), where k is the range of values, beating any comparison sort's O(n log n) floor. But it only works on a small range of non-negative integers. Radix sort lifts that limit by running a stable counting sort once per digit, from the ones place up, so it can sort numbers of any size in O(d·n), where d is the digit count. Both are stable — and they have to be, or radix sort's later digit-passes would undo the sorting the earlier passes did.

Three stacked rows — the input array, a counts row, and an output row — with one curved arrow looping from the last input box back to an early output slot, showing the backward scan that keeps stability

Sort record IDs without comparing them

Quarry ingests a batch of small non-negative record IDs overnight and needs them sorted before the morning read. Counting sort never asks "is this one bigger than that one" — it only counts:

python

The prefix-summed counts array turns "how many 2s are there" into "how many items belong at or before a 2" — exactly the final index each item needs, counting from the back so that the last equal item lands in the highest open slot, leaving earlier arrivals the lower ones. Watch the output array fill in, one placement at a time:

watch it move · array

Scanning the input backward and placing straight into the final slot is what keeps equal values in their original order.

The last input value is 2 (index 5). Its running count says "one belongs at position 3". 6 cells. At index 3.

1 / 6

Sort record IDs with six-digit numbers

Counting sort's counts array would need one slot per possible ID — useless once IDs run into the millions. Radix sort keeps counting sort's O(n) shape by running it on one digit at a time, least significant first, so the counts array only ever needs 10 slots:

python

Watch one digit pass distribute five two-digit IDs into 10 buckets by their ones digit, then read the buckets back top to bottom:

watch it move · grid

Each bucket is a row, keyed by the digit in focus. Reading the rows back in order 0 to 9 sorts by that digit — and keeps ties in the order they arrived, because a bucket is filled left to right.

29 ends in 9, so it goes into row 9, the first open slot. 10 by 2 table. At row 9, column 0 — value 29. 1 of 20 cells filled.

1 / 6

The tens pass runs next on [43, 34, 8, 29, 19], this time keying on the digit that was already the tiebreaker — and because both passes are stable, the ones-digit order survives inside every tens-digit group.

Why "stable" is not optional here

A sort being stable is a nice-to-have almost everywhere else. For radix sort it is the entire algorithm. Sort unstably on the ones digit, and the tens pass has no way to recover which of two same-tens-digit numbers came first:

What does this print?
def unstable_counting_sort_by_digit(nums, exp):
    buckets = [[] for _ in range(10)]
    for x in sorted(nums, reverse=True):     # sorting first "for speed" — breaks arrival order
        buckets[(x // exp) % 10].append(x)
    return [x for bucket in buckets for x in bucket]

step1 = unstable_counting_sort_by_digit([23, 21, 45], 1)   # sort by ones digit
print(step1)
What does this print?
def unstable_counting_sort_by_digit(nums, exp):
    buckets = [[] for _ in range(10)]
    for x in sorted(nums, reverse=True):
        buckets[(x // exp) % 10].append(x)
    return [x for bucket in buckets for x in bucket]

after_ones = unstable_counting_sort_by_digit([23, 21, 45, 25], 1)
after_tens = unstable_counting_sort_by_digit(after_ones, 10)
print(after_tens)

Quick practice

What does this print?
counts = [0, 0, 0, 0]
for x in [2, 3, 0, 2, 3, 2]:
    counts[x] += 1
for i in range(1, 4):
    counts[i] += counts[i - 1]
print(counts)
broken — fix it

This sorts every digit backward. Fix the one word that reverses the read-back order.


🎯 Your turn

Build radix_sort(nums): return a new list with nums sorted ascending. nums are non-negative integers. Use LSD radix sort — a stable counting-sort pass for each digit, from the ones place up — not sorted().

  • radix_sort([170, 45, 75, 90, 802, 24, 2, 66]) → [2, 24, 45, 66, 75, 90, 170, 802]
  • radix_sort([]) → []

Hint: exp starts at 1 and multiplies by 10 each pass, while max(nums) // exp > 0. (x // exp) % 10 reads the digit currently in focus. Scan out in its current order when filling the 10 buckets — that's what keeps the sort stable.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upCount each value

Write count_occurrences(arr, k), returning a list of length k + 1 where index v holds how many times v appears in arr. Every value in arr is between 0 and k inclusive. count_occurrences([1, 0, 5, 3, 3, 1, 3, 3, 4, 4], 5) -> [1, 2, 0, 4, 2, 1].

sorting
DrillCounting sort, start to finish

Write counting_sort(arr), returning a new list of non-negative integers sorted ascending, using the full three-row counting sort (counts, running total, backward placement). counting_sort([4, 2, 2, 6, 3, 3, 1, 6, 5, 2, 3]) -> [1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 6].

counting-sortprefix-sum
BuildBucket sort for floats

Write bucket_sort(values), sorting a list of floats in [0, 1) ascending. Spread them into len(values) buckets by int(x * len(values)), sort each bucket, then concatenate. bucket_sort([0.42, 0.11, 0.9, 0.3]) -> [0.11, 0.3, 0.42, 0.9].

bucket-sort
BossDid this sort stay stable?

Items are [key, label] pairs. Write is_stable_result(before, after), returning True if every group of same-key items in after appears in the same relative order it had in before. is_stable_result([[2,'A'],[5,'B'],[2,'C']], [[2,'A'],[2,'C'],[5,'B']]) -> True.

sortingstable-sort
CapstoneRadix sort with negative IDs

Write radix_sort_signed(nums), extending radix sort to a list that may include negative integers. Radix sort itself only handles non-negative values, so split the list, radix-sort the magnitudes of the negatives and the non-negatives separately, then stitch the three pieces together in order. radix_sort_signed([3, -1, -5, 2, 0, -2]) -> [-5, -2, -1, 0, 2, 3].

radix-sortsorting
radix_sort.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
radix_sort(nums) → listA new list with nums sorted ascending, using repeated stable counting-sort passes, one digit at a time.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.