Bucket & Counting Sort

Mediumsorting · counting · hashing · frequency · pigeonhole⏱ 6 min readUpdated

Beat O(n log n) sorting by using the value (or its frequency) as an array index - counting sort for tiny ranges, bucket sort for frequencies and evenly spread values.

PatternBucket & Counting Sort

Instinct Keys live in a small, known range (letters, frequencies 1..n, bounded values) - index an array by the key instead of comparing, and get O(n).

Memory hack Don't compare, just file it - every value walks straight to its own pigeonhole, then you read the shelves in order.

At a glance

When to use it

  • Keys come from a small known range such as lowercase letters or values 0..k
  • Items must be ordered or grouped by frequency, and a frequency can never exceed n
  • You need the top k most frequent items faster than O(n log n)
  • A sorting-style question demands linear time

Key idea

Use the key or its frequency as an array index so every item drops straight into its slot, then sweep the slots in order; with no comparisons the work is O(n + k), beating the O(n log n) floor of comparison sorts.

Typical complexity

O(n + k) time, O(n + k) space

How to spot it If the problem says the values are bounded (letters, 0..k, counts up to n) and asks for a sorted order, the most frequent items or a linear-time answer, think bucket or counting sort first.

The instinct

Comparison sorts can never beat n log n, but they also never look at what the keys are. If you know the keys are small integers - 26 letters, ages 0..150, or frequencies that cannot exceed n - you can skip comparisons entirely and use each key as an address. Drop every item into the slot its key points to, then sweep the slots in order. That single idea powers counting sort, bucket sort by frequency, and the pigeonhole trick behind Maximum Gap.

Recognise it

Signal in the problem Why it points here
"sort characters by frequency" A frequency is between 1 and n, so it can index an array of n + 1 buckets.
"top k frequent elements / words" Bucket by count, then read from the highest bucket down - O(n) instead of a heap.
"values are in the range 0..k with small k" / "only lowercase letters" A count[k] array replaces sorting or hashing.
"linear time and linear space" on a sorting-flavoured task Comparison sorts are ruled out; distribution sorts remain.
"maximum gap between successive elements in sorted form" Pigeonhole: with n - 1 gaps of average width, the answer spans buckets, never sits inside one.
"sort colours / 0s, 1s and 2s" Three-key counting sort (or Dutch flag partition).
"H-index", "anagram", "can the ransom note be built" Counting array of bounded size answers each in one pass.

Not this pattern if...

  • Keys are unbounded or sparse (values up to 10^9 with n = 10^5) and you need full order - use a regular sort or a hash map.
  • You need the k-th largest from a stream - a heap or quickselect fits better.
  • Order matters only relative to neighbours (next greater) - see monotonic stack.

Memory hack

Tip

"Don't compare, just file it." Think of a post office sorting wall: a clerk never compares two letters - each letter goes into the pigeonhole for its postcode, and at the end the clerk empties the holes left to right. Counting sort stores just the number of letters per hole; bucket sort stores the letters themselves.

For frequency problems, flip the map: instead of "value -> count", build "count -> list of values". The bucket index is the rank.

Say it out loud in the interview:

  1. "What is the key, and what is its maximum possible value?" (frequency <= n, letter < 26).
  2. "Allocate buckets indexed by that key and scatter each item into its bucket."
  3. "Gather by sweeping buckets in the needed direction, stopping once I have k items."

How it works

Counting sort has three phases: count occurrences of each key, optionally turn counts into prefix sums (giving each key's final start position, which keeps the sort stable), then place items.

Bucket sort by frequency (Sort Characters By Frequency, Top K Frequent Words): first a hash map gives value -> freq; then an array buckets[freq] holds lists of values; finally walk from n down to 1.

Pigeonhole buckets (Maximum Gap): with n numbers between min and max, the largest gap is at least ceil((max - min) / (n - 1)). Make buckets that wide; any gap inside a bucket is smaller, so you only need each bucket's min and max and compare consecutive non-empty buckets.

flowchart LR
    A[Input items] --> B[Count: key -> frequency]
    B --> C[Scatter: buckets indexed by frequency]
    C --> D[Gather: sweep from highest bucket]
    D --> E{Collected k items?}
    E -- no --> D
    E -- yes --> F[Answer]

Tiny trace for "tree": counts t=1, r=1, e=2; buckets [1] = {t, r}, [2] = {e}; sweep 2 then 1 -> "eetr".

Java template

java
import java.util.*;

class BucketCounting {
    // Template 1: stable counting sort for keys in [0, k).
    static int[] countingSort(int[] a, int k) {
        int[] count = new int[k + 1];
        for (int x : a) count[x + 1]++;                    // shift by one for prefix sums
        for (int i = 0; i < k; i++) count[i + 1] += count[i]; // count[v] = start index of v
        int[] out = new int[a.length];
        for (int x : a) out[count[x]++] = x;               // stable placement
        return out;
    }

    // Template 2: bucket by frequency, read highest buckets first.
    static List<Integer> topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> freq = new HashMap<>();
        for (int x : nums) freq.merge(x, 1, Integer::sum);

        List<List<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i <= nums.length; i++) buckets.add(new ArrayList<>());
        for (Map.Entry<Integer, Integer> e : freq.entrySet())
            buckets.get(e.getValue()).add(e.getKey());

        List<Integer> res = new ArrayList<>();
        for (int f = nums.length; f >= 1 && res.size() < k; f--) {
            for (int v : buckets.get(f)) {          // sort this bucket first if ties need an order
                if (res.size() == k) break;
                res.add(v);
            }
        }
        return res;
    }

    // Template 3: pigeonhole buckets that only track min and max (Maximum Gap).
    static int maximumGap(int[] a) {
        int n = a.length;
        if (n < 2) return 0;
        int lo = Integer.MAX_VALUE, hi = Integer.MIN_VALUE;
        for (int x : a) { lo = Math.min(lo, x); hi = Math.max(hi, x); }
        if (lo == hi) return 0;
        int width = Math.max(1, (hi - lo) / (n - 1));
        int cnt = (hi - lo) / width + 1;
        int[] bMin = new int[cnt], bMax = new int[cnt];
        Arrays.fill(bMin, Integer.MAX_VALUE);
        Arrays.fill(bMax, Integer.MIN_VALUE);
        for (int x : a) {
            int b = (x - lo) / width;
            bMin[b] = Math.min(bMin[b], x);
            bMax[b] = Math.max(bMax[b], x);
        }
        int best = 0, prevMax = lo;
        for (int b = 0; b < cnt; b++) {
            if (bMin[b] == Integer.MAX_VALUE) continue;   // empty bucket
            best = Math.max(best, bMin[b] - prevMax);
            prevMax = bMax[b];
        }
        return best;
    }
}

Complexity

Technique Time Space
Counting sort, key range k O(n + k) O(n + k)
Bucket by frequency (top k) O(n) average (hash map) O(n)
Bucket by frequency with tie-sorting (Top K Frequent Words) O(n + u log u) worst case, u = distinct words O(n)
Pigeonhole Maximum Gap O(n) O(n)

Variations

  • Pure counting array - fixed alphabet or tiny range: Valid Anagram, Sort Colors.
  • Frequency buckets - index by count, sweep downward: Sort Characters By Frequency, Top K Frequent Elements.
  • Frequency buckets with tie-break - sort words alphabetically inside each bucket: Top K Frequent Words.
  • Capped buckets - clamp values above n into bucket n: H-Index.
  • Range buckets / pigeonhole - only min and max per bucket matter: Maximum Gap, Contains Duplicate III.
  • Radix sort - repeated stable counting sort on each digit for large integer keys.

Common mistakes

Warning

Sizing the frequency bucket array as n instead of n + 1. If every element is the same, its frequency is n, and buckets[n] throws ArrayIndexOutOfBoundsException.

  • Forgetting to offset negative keys (count[x - min]) before using them as indices.
  • Allocating a counting array of size max value when values reach 10^9 - check the range before choosing this pattern.
  • Ignoring tie-break rules: Top K Frequent Words wants lexicographic order inside a frequency, so each bucket must be sorted.
  • In Maximum Gap, letting the bucket width become 0 when max - min < n - 1; clamp it to at least 1.
  • Iterating buckets in the wrong direction (ascending when the question wants most frequent first).

All practice problems

3 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.

My notes saved in this browser