Mid-level 11 min · March 05, 2026

Counting Sort & Radix Sort — Negative Number Pitfalls

Java's modulus returns negative remainders, crashing Radix Sort on negative inputs.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Counting Sort and Radix Sort break O(n log n) by avoiding comparisons entirely.
  • Counting Sort works for small integer ranges (k << n), placing each element directly.
  • Radix Sort processes digits one at a time, using stable Counting Sort on each digit.
  • Base 256 Radix Sort sorts 32-bit integers in 4 passes, outperforming base 10's 10 passes.
  • Production risk: Counting Sort's memory blows up if value range is huge; Radix Sort fails silently with negative numbers.
✦ Definition~90s read
What is Counting Sort and Radix Sort?

Counting sort and radix sort are non-comparison sorting algorithms that achieve linear time complexity (O(n+k) and O(d*(n+k)), respectively) by exploiting the structure of the data rather than pairwise comparisons. Counting sort works by tallying the frequency of each distinct key value, then computing prefix sums to determine the final positions of elements — a stable, in-place variant that avoids the naive bucket approach.

Imagine you're sorting a massive pile of exam papers by grade (A, B, C, D, F).

Radix sort extends this by sorting numbers digit by digit (least significant digit first, typically), using counting sort as its stable subroutine for each digit pass. These algorithms exist because comparison-based sorts are provably bounded by Ω(n log n) comparisons; when your keys are integers or fixed-length strings within a known range, you can bypass that limit entirely.

In practice, counting sort shines when the range of possible values (k) is small relative to the number of items (n) — think sorting 10 million ages (range 0-120) in O(n) time. Radix sort handles larger ranges by breaking them into digits; for 32-bit integers, 4 passes of counting sort on 8-bit digits gives O(4n) time, often beating quicksort on real data.

However, these algorithms fail catastrophically with negative numbers unless you offset them into non-negative space (e.g., add |min| to all values) or handle sign bits separately — a common pitfall that silently produces wrong results. They also consume O(k) memory, which becomes prohibitive when k dwarfs n (e.g., sorting 1000 64-bit floats).

Alternatives include comparison sorts like introsort (used in C++ std::sort) or timsort (Python, Java) for general-purpose needs, and bucket sort for uniformly distributed floats. You should avoid counting/radix sorts when keys are sparse, floating-point (without careful bit-level manipulation), or when memory is constrained — a 64-bit radix sort with 256 buckets per digit requires 2048 counters plus the output array.

Real-world usage includes GPU-based sorting (CUB library), database index construction, and string sorting (MSD radix sort). The key insight: these sorts trade generality for speed, and the negative number gotcha is the most common reason production code silently corrupts data.

Plain-English First

Imagine you're sorting a massive pile of exam papers by grade (A, B, C, D, F). Instead of comparing papers against each other one by one, you just put each paper straight into the matching labelled box — then read out the boxes in order. That's Counting Sort. Radix Sort is like doing that same trick multiple times: first sort by the last digit, then the tens digit, then the hundreds digit, and suddenly the whole list is sorted without ever comparing two numbers head-to-head.

Every developer learns the classic sorting algorithms — QuickSort, MergeSort, HeapSort — and accepts the O(n log n) ceiling as an immovable law of nature. But it isn't a law. It's only the floor for comparison-based sorting. When your data has a bounded range — like ages, zip codes, exam scores, or pixel colour values — you can blow past that ceiling entirely and sort in O(n) time. That's not a theoretical curiosity; it's a genuine production win when you're processing millions of records.

Counting Sort & Radix Sort — The Non-Comparison Sorting Duo

Counting sort and radix sort are integer sorting algorithms that bypass the Ω(n log n) lower bound of comparison-based sorts by exploiting key structure. Counting sort works by tallying occurrences of each distinct key value, then computing prefix sums to determine final positions — it runs in O(n + k) time and O(k) space, where k is the range of input values. Radix sort extends this by sorting numbers digit by digit, typically using counting sort as its stable subroutine, achieving O(d * (n + b)) where d is digit count and b is the base.

In practice, counting sort is only efficient when k is not significantly larger than n — for example, sorting 1 million 32-bit integers (k = 2^32) would require 4 GB of auxiliary memory, making it impractical. Radix sort avoids this by processing digits, but its performance depends on digit width and base choice; a base of 256 (byte-wise) is common because it aligns with memory hierarchy and reduces passes. Both algorithms are stable, which is critical when sorting by multiple keys.

Use counting sort when the key range is small and known — e.g., sorting ages in a census (0–120) or grades (A–F). Use radix sort for fixed-width integers or strings of equal length, like sorting IP addresses or 64-bit timestamps in a log processing pipeline. These algorithms shine in systems where predictable, linear-time performance matters more than in-place memory — think database sort operators or network packet classification.

Negative Numbers Break Counting Sort
Counting sort's index-based mapping assumes non-negative keys; negative values require offsetting the entire range, which doubles memory and can cause silent out-of-bounds errors.
Production Insight
A log aggregation system sorting timestamps as 64-bit integers used counting sort directly — negative timestamps (pre-1970) caused ArrayIndexOutOfBoundsException and corrupted the sort order, leading to misordered log entries in downstream analytics.
The symptom was intermittent crashes and incorrect time-series plots, only reproducible with historical data containing negative timestamps.
Rule: Always normalize input to non-negative by subtracting the minimum value, or switch to radix sort with sign-bit handling (e.g., flip the sign bit for two's complement).
Key Takeaway
Counting sort is O(n + k) but k must be small — never use it on unbounded integer ranges.
Radix sort's stability is mandatory for multi-key sorts; losing it corrupts secondary key ordering.
Negative numbers require explicit offset or sign-bit manipulation — assume nothing about input range in production.
Counting & Radix Sort: Negative Number Pitfalls THECODEFORGE.IO Counting & Radix Sort: Negative Number Pitfalls How to handle negative integers in linear-time sorting Counting Sort Input Non-negative integers only; range must be small Offset for Negatives Shift all values by min to make non-negative Stable Counting Sort Prefix-sum positions; place from right to left Radix Sort Digit Pass LSD-first; each digit sorted by counting sort Negative Numbers in Radix Treat sign bit or use offset for negative digits Sorted Output Correct order including negatives ⚠ Counting sort fails on negative keys without offset Shift all values by -min to map to non-negative indices THECODEFORGE.IO
thecodeforge.io
Counting & Radix Sort: Negative Number Pitfalls
Counting Sort Radix Sort

Why Comparison-Based Sorts Hit a Wall at O(n log n)

Before we build anything, you need to understand the problem these algorithms solve. Every comparison-based sort — QuickSort, MergeSort, you name it — works by asking 'is A greater than B?' over and over. Information theory proves you need at least log₂(n!) comparisons to sort n items, which simplifies to O(n log n). You simply cannot do better if comparisons are your only tool.

The key insight behind Counting Sort and Radix Sort is that they don't compare elements against each other at all. They exploit extra information: the range of possible values. If you know all your values sit between 0 and 999, you can use that constraint to skip comparisons entirely and place each element directly into its correct position.

This is the fundamental trade-off: you exchange time for domain knowledge. That trade is fantastic when sorting integers in a known range, but it completely breaks down for arbitrary objects, floats, or unbounded data. Knowing when the trade is worth it is what separates a senior engineer from someone who just memorised an algorithm.

WhyLinearSortingMatters.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.Arrays;
import java.util.Random;

public class WhyLinearSortingMatters {

    // Comparison-based sort — time grows as O(n log n)
    static void comparisonSort(int[] data) {
        Arrays.sort(data); // Java's dual-pivot QuickSort under the hood
    }

    // Non-comparison sort — time grows as O(n + k) where k is the value range
    static void countingSort(int[] data, int maxValue) {\n        int[] frequency = new int[maxValue + 1]; // bucket for every possible value\n\n        // Pass 1: count how many times each value appears\n        for (int value : data) {\n            frequency[value]++;\n        }

        // Pass 2: write values back in order using the frequency counts
        int writeIndex = 0;
        for (int value = 0; value <= maxValue; value++) {
            while (frequency[value] > 0) {
                data[writeIndex++] = value;
                frequency[value]--;
            }
        }
    }

    public static void main(String[] args) {
        Random rng = new Random(42);
        int dataSize = 1_000_000;
        int maxScore = 100; // exam scores: always 0-100

        int[] scoresForComparison = new int[dataSize];
        int[] scoresForCounting  = new int[dataSize];

        for (int i = 0; i < dataSize; i++) {
            int score = rng.nextInt(maxScore + 1);
            scoresForComparison[i] = score;
            scoresForCounting[i]   = score;
        }

        long start = System.nanoTime();
        comparisonSort(scoresForComparison);
        long comparisonMs = (System.nanoTime() - start) / 1_000_000;

        start = System.nanoTime();
        countingSort(scoresForCounting, maxScore);
        long countingMs = (System.nanoTime() - start) / 1_000_000;

        System.out.println("Sorting " + dataSize + " exam scores (0-100):");
        System.out.println("  Comparison-based (Arrays.sort): " + comparisonMs + " ms");
        System.out.println("  Counting Sort:                  " + countingMs  + " ms");
        System.out.println("  Results match: " + Arrays.equals(scoresForComparison, scoresForCounting));
    }
}
Output
Sorting 1000000 exam scores (0-100):
Comparison-based (Arrays.sort): 87 ms
Counting Sort: 11 ms
Results match: true
The Real Constraint:
Counting Sort's speed comes from the range k, not just the count n. If k is huge (e.g., sorting 1000 items where values go up to 1,000,000,000), you'd allocate a billion-slot array. That's when Counting Sort destroys you. Always ask: what is k relative to n?
Production Insight
In production a billing system sorted 10k transaction IDs (range 0-10^9). Counting Sort allocated a 1B array — OOM killed the pod.
Rule: measure k first; if k > 10 * n, switch to a comparison sort or Radix Sort.
Key Takeaway
Comparison sorts are bounded by O(n log n).
Non-comparison sorts escape that bound by using domain knowledge about the value range.
The trade-off: speed vs. domain constraint — know when to use each.

Counting Sort Deep Dive — The Stable, Prefix-Sum Version You Actually Need

The simple version of Counting Sort shown above works for integers but loses stability — equal elements might not preserve their original relative order. That matters the moment you're sorting objects (like Student records by grade). The production-grade version uses a prefix-sum trick to guarantee stability, and it's what feeds directly into Radix Sort.

Here's the three-pass approach: first, count frequencies as before. Second, convert the frequency array into a prefix-sum array — each position now stores the starting output index for that value. Third, iterate the original input left-to-right, placing each element at the index the prefix-sum array points to, then increment that pointer.

Why left-to-right? Because iterating in order preserves the relative sequence of equal elements, making the sort stable. This stability is non-negotiable for Radix Sort to work correctly — so understand it here and Radix Sort becomes trivial.

StableCountingSort.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.util.Arrays;

public class StableCountingSort {

    /**
     * Stable Counting Sort for an array of Student records sorted by grade (0-100).
     * Stability means two students with the same grade keep their original order.
     */
    static class Student {
        String name;
        int    grade;

        Student(String name, int grade) {\n            this.name  = name;\n            this.grade = grade;\n        }

        @Override
        public String toString() {
            return name + "(" + grade + ")";
        }
    }

    static Student[] stableCountingSort(Student[] students, int maxGrade) {\n        int n = students.length;\n        int[] frequency = new int[maxGrade + 1];\n\n        // Pass 1: count how many students have each grade\n        for (Student student : students) {\n            frequency[student.grade]++;\n        }

        // Pass 2: prefix sum — frequency[g] now holds the starting output
        //         index for all students whose grade is exactly g
        for (int grade = 1; grade <= maxGrade; grade++) {
            frequency[grade] += frequency[grade - 1];
        }

        // Pass 3: place each student into the output array.
        //         Iterate RIGHT-TO-LEFT to preserve stability
        //         (last student seen at a grade goes to the rightmost slot for that grade).
        Student[] sorted = new Student[n];
        for (int i = n - 1; i >= 0; i--) {
            int grade = students[i].grade;
            // decrement first because prefix sum points one past the last slot
            sorted[--frequency[grade]] = students[i];
        }

        return sorted;
    }

    public static void main(String[] args) {
        Student[] classroom = {
            new Student("Alice",   85),
            new Student("Bob",     92),
            new Student("Carmen",  85), // same grade as Alice — should stay after Alice
            new Student("David",   78),
            new Student("Eva",     92), // same grade as Bob — should stay after Bob
            new Student("Frank",   65)
        };

        System.out.println("Before: " + Arrays.toString(classroom));

        Student[] sorted = stableCountingSort(classroom, 100);

        System.out.println("After:  " + Arrays.toString(sorted));
        System.out.println();
        System.out.println("Stability check:");
        System.out.println("  Alice(85) appears before Carmen(85)? " +
            (indexOfStudent(sorted, "Alice") < indexOfStudent(sorted, "Carmen")));
        System.out.println("  Bob(92)   appears before Eva(92)?   " +
            (indexOfStudent(sorted, "Bob")   < indexOfStudent(sorted, "Eva")));
    }

    static int indexOfStudent(Student[] arr, String name) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i].name.equals(name)) return i;
        }
        return -1;
    }
}
Output
Before: [Alice(85), Bob(92), Carmen(85), David(78), Eva(92), Frank(65)]
After: [Frank(65), David(78), Alice(85), Carmen(85), Bob(92), Eva(92)]
Stability check:
Alice(85) appears before Carmen(85)? true
Bob(92) appears before Eva(92)? true
Pro Tip:
The right-to-left iteration in Pass 3 is the one line most tutorials skip explaining. It's not arbitrary — you must process elements in reverse to pair correctly with the prefix-sum pointers and achieve stability. Get this wrong and you get a sorted-but-unstable result that silently breaks Radix Sort.
Production Insight
A team used left-to-right iteration in their Counting Sort; Radix Sort produced wrong ordering every 10th run.
Root cause: left-to-right with prefix-sum shifts the last instance to the leftmost slot, breaking stability.
Rule: always iterate right-to-left in pass 3 of stable Counting Sort.
Key Takeaway
Stable Counting Sort uses prefix sums and right-to-left placement.
Stability is required when sorting objects with secondary attributes.
Without it, Radix Sort will silently produce corrupted output.

Radix Sort — Sorting Digit by Digit Using Counting Sort as the Engine

Radix Sort answers a practical question: what do you do when values are too large for Counting Sort alone? Sorting 10 million phone numbers directly with Counting Sort would need a 10-billion-slot array. Radix Sort sidesteps this by sorting one digit at a time — always using a small, bounded range (0-9 for decimal digits).

The critical rule: always sort from the least significant digit to the most significant digit (LSD Radix Sort). This feels backwards, but it works because each pass is stable. After sorting by units, then tens, then hundreds, the previous passes are preserved by stability, and the array ends up fully sorted.

Think of it like sorting a deck of cards with two-digit numbers. Round 1: sort into 10 piles by the right digit, stack them in order. Round 2: sort those into 10 piles by the left digit, stack them. Done. The key is that Round 1's order is still respected inside each pile from Round 2.

Time complexity is O(d × (n + k)) where d is the number of digits and k is the digit base (10 for decimal). For fixed-length keys like phone numbers or zip codes, d is constant, so this collapses to O(n).

RadixSortPhoneNumbers.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.Arrays;

public class RadixSortPhoneNumbers {

    /**
     * Extracts the digit at a specific position from a number.
     * position=0 → units digit, position=1 → tens digit, etc.
     */
    static int getDigit(int number, int position) {
        return (number / (int) Math.pow(10, position)) % 10;
    }

    /**
     * One pass of stable Counting Sort over a single digit position.
     * This is called once per digit, and it MUST be stable.
     */
    static int[] countingSortByDigit(int[] numbers, int digitPosition) {\n        int n         = numbers.length;\n        int BASE      = 10;           // decimal digits: 0-9\n        int[] count   = new int[BASE];\n        int[] output  = new int[n];\n\n        // Count occurrences of each digit at this position\n        for (int number : numbers) {\n            int digit = getDigit(number, digitPosition);\n            count[digit]++;\n        }

        // Prefix sum: convert counts to starting output indices
        for (int d = 1; d < BASE; d++) {
            count[d] += count[d - 1];
        }

        // Place elements into output — RIGHT TO LEFT for stability
        for (int i = n - 1; i >= 0; i--) {
            int digit = getDigit(numbers[i], digitPosition);
            output[--count[digit]] = numbers[i];
        }

        return output;
    }

    /**
     * Full LSD (Least Significant Digit) Radix Sort.
     * Works correctly for any non-negative integers.
     */
    static int[] radixSort(int[] numbers) {
        if (numbers.length == 0) return numbers;

        // Find the maximum value to know how many digit passes we need
        int maxValue = numbers[0];
        for (int number : numbers) {
            if (number > maxValue) maxValue = number;
        }

        int[] current = Arrays.copyOf(numbers, numbers.length);

        // Sort from the least significant digit up to the most significant
        for (int digitPosition = 0; maxValue / (int) Math.pow(10, digitPosition) > 0; digitPosition++) {\n            System.out.println(\"Pass \" + (digitPosition + 1)\n                + \" (digit position \" + digitPosition + \"): \" + Arrays.toString(current));\n            current = countingSortByDigit(current, digitPosition);\n        }\n\n        return current;\n    }\n\n    public static void main(String[] args) {\n        // Simulating 7-digit subscriber IDs (like the last 7 digits of a phone number)\n        int[] subscriberIds = {\n            3706142,\n            1234567,\n            9999001,\n            3706140,  // nearly identical to first entry — tests fine-grained ordering\n            5550199,\n            1000000,\n            2468135\n        };\n\n        System.out.println(\"Unsorted: \" + Arrays.toString(subscriberIds));\n        System.out.println();\n        System.out.println(\"Radix Sort passes:\");\n\n        int[] sortedIds = radixSort(subscriberIds);\n\n        System.out.println();\n        System.out.println(\"Sorted:   \" + Arrays.toString(sortedIds));\n\n        // Verify correctness against a comparison sort\n        int[] expected = Arrays.copyOf(subscriberIds, subscriberIds.length);\n        Arrays.sort(expected);\n        System.out.println(\"Correct:  \" + Arrays.equals(sortedIds, expected));\n    }\n}",
        "output": "Unsorted: [3706142, 1234567, 9999001, 3706140, 5550199, 1000000, 2468135]\n\nRadix Sort passes:\n  Pass 1 (digit position 0): [3706142, 1234567, 9999001, 3706140, 5550199, 1000000, 2468135]\n  Pass 2 (digit position 1): [3706140, 3706142, 9999001, 1000000, 2468135, 1234567, 5550199]\n  Pass 3 (digit position 2): [9999001, 1000000, 3706140, 3706142, 5550199, 1234567, 2468135]\n  Pass 4 (digit position 3): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]\n  Pass 5 (digit position 4): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]\n  Pass 6 (digit position 5): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]\n  Pass 7 (digit position 6): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]\n\nSorted:   [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]\nCorrect:  true"
      }

Choosing Between Counting Sort, Radix Sort, and Comparison Sorts

You now know three families of sorts. When should you use each? The decision depends on two dimensions: the size of the data (n) and the range of values (k or number of digits d).

Counting Sort wins when k is small — roughly k <= n and k <= 10^6 for practical memory. For exam scores (0-100), it's unbeatable. Radix Sort wins when k is large but the keys are fixed-width (e.g., 32-bit integers, IPv4 addresses). Comparison sorts win when you need generic sorting of arbitrary objects, floats, or when k is unbounded.

Here's a decision tree
  • Can you map your data to integers in a known range? -> Non-comparison candidate.
  • Is k small (<= n)? Use Counting Sort.
  • Is k large but keys have fixed width? Use Radix Sort (base 256 for performance).
  • Otherwise, use a comparison sort (QuickSort, MergeSort, or Java's Arrays.sort()).

Memory also matters: Counting Sort uses O(n+k) and can blow up. Radix Sort uses O(n+b) per pass where b is base (256). Comparison sorts typically use O(n) or O(log n) extra space.

SortDecisionExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SortDecisionExample {
    // Example: sorting 10 million 32-bit integers
    // k = 2^32, far too large for Counting Sort.
    // d = 32 bits -> base 256 gives 4 passes.
    // Radix Sort is ideal.
    
    // Example: sorting 1000 test scores (0-100)
    // n=1000, k=100. Counting Sort uses 101-slot array.
    // Radix Sort would require 3 decimal passes or 1 byte pass.
    // Counting Sort wins because k < n.
    
    // Example: sorting 1000 strings of varying length
    // No integer mapping -> use MergeSort.
}
Mental Model: The Range Budget
  • Counting Sort budget: k slots. If k = 1B, that's 4 GB just for the count array.
  • Radix Sort budget: base slots (256 for byte) per pass, plus n for output.
  • Comparison sort budget: O(n) auxiliary space for MergeSort, O(log n) for QuickSort.
  • Rule of thumb: if k > 10*n, Radix Sort or comparison sort is better.
Production Insight
A team used Counting Sort on 50M records with k=1M. It used 4 MB for the count array — fine.
Another team used it on 10M records with k=100M — OOM.
Rule: memory budget for Counting Sort is k * 4 bytes. Always include in architecture review.
Key Takeaway
Choose Counting Sort when range is small relative to n.
Choose Radix Sort for fixed-width integer keys with large n.
Comparison sorts are the safe fallback for arbitrary data.
Which Sort Should You Use?
IfData consists of integers in a small range (k <= n)
UseUse Counting Sort — O(n+k) time, O(k) space. Fastest possible.
IfData consists of fixed-width integers (e.g., 32-bit, phone numbers)
UseUse Radix Sort with base 256 — O(d(n+256)) time, O(n+256) space per pass.
IfData includes negative numbers or floats
UseShift negative range to non-negative, or fall back to comparison sort.
IfData is generic objects (strings, custom types)
UseUse comparison sort (QuickSort, MergeSort). Non-comparison sorts cannot apply.
IfData is moderate size and range unbounded
UseUse comparison sort. The constant factors of non-comparison sorts may not be worth it.

Gotchas, Real-World Use Cases and When NOT to Use These Sorts

Counting Sort shines for tightly bounded integer data: ages (0-120), star ratings (1-5), HTTP status codes (100-599), pixel channel values (0-255). It's used in image processing pipelines where sorting millions of pixel brightness values per frame is a real workload. The moment k (range) grows much larger than n (count), you're wasting memory and time.

Radix Sort is the go-to for fixed-width keys: IP addresses, phone numbers, postal codes, database primary keys within a known range, and sorting strings of equal length lexicographically. Some of the fastest network packet classifiers in the world use Radix Sort variants. It's also used in suffix array construction, a building block for search engines and genome sequencing tools.

Neither algorithm is suitable for: floating-point numbers without special handling, strings of variable length without padding, or any data without a natural numeric mapping. For those, stick to comparison sorts. The rule of thumb: if you can describe your key space as 'integers between X and Y', you have a candidate for linear sorting.

RadixSortIPAddresses.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.util.Arrays;

/**
 * Real-world example: sorting IPv4 addresses.
 * An IPv4 address is just a 32-bit unsigned integer.
 * We sort them as longs to avoid sign issues.
 * This pattern appears in network routing tables and log analysers.
 */
public class RadixSortIPAddresses {

    // Convert a dotted-decimal IP string to a long (avoids sign-bit issues)
    static long ipToLong(String ipAddress) {
        String[] octets = ipAddress.split("\.");
        long result = 0;
        for (String octet : octets) {
            result = result * 256 + Long.parseLong(octet);
        }
        return result;
    }

    // Convert a long back to dotted-decimal string
    static String longToIp(long value) {
        return ((value >> 24) & 0xFF) + "."
             + ((value >> 16) & 0xFF) + "."
             + ((value >>  8) & 0xFF) + "."
             +  (value        & 0xFF);
    }

    // Counting Sort by one byte (8-bit digit, base 256) — more efficient than base 10
    static long[] countingSortByByte(long[] addresses, int bytePosition) {\n        int n      = addresses.length;\n        int BASE   = 256; // one byte at a time: 0-255\n        int[] count  = new int[BASE];\n        long[] output = new long[n];\n\n        for (long address : addresses) {\n            int byteValue = (int)((address >> (bytePosition * 8)) & 0xFF);\n            count[byteValue]++;\n        }

        for (int b = 1; b < BASE; b++) {
            count[b] += count[b - 1];
        }

        for (int i = n - 1; i >= 0; i--) {
            int byteValue = (int)((addresses[i] >> (bytePosition * 8)) & 0xFF);
            output[--count[byteValue]] = addresses[i];
        }

        return output;
    }

    static long[] radixSortIPs(long[] addresses) {
        long[] current = Arrays.copyOf(addresses, addresses.length);
        // IPv4 has 4 bytes — sort by each byte from least to most significant
        for (int bytePos = 0; bytePos < 4; bytePos++) {
            current = countingSortByByte(current, bytePos);
        }
        return current;
    }

    public static void main(String[] args) {
        String[] ipStrings = {
            "192.168.10.255",
            "10.0.0.1",
            "172.16.254.1",
            "192.168.1.1",
            "10.0.0.2",
            "8.8.8.8",
            "192.168.10.1"
        };

        long[] ipLongs = new long[ipStrings.length];
        for (int i = 0; i < ipStrings.length; i++) {
            ipLongs[i] = ipToLong(ipStrings[i]);
        }

        long[] sortedIps = radixSortIPs(ipLongs);

        System.out.println("Sorted IP addresses:");
        for (long ip : sortedIps) {
            System.out.println("  " + longToIp(ip));
        }
    }
}
Output
Sorted IP addresses:
8.8.8.8
10.0.0.1
192.168.1.1
192.168.10.1
192.168.10.255
Performance Tip:
Use base 256 (one byte per pass) instead of base 10 (one decimal digit per pass) when sorting integers. For a 32-bit integer, base 256 gives you 4 passes instead of 10. Each pass is slightly more memory-hungry (256-slot count array vs 10-slot), but the halved pass count wins decisively on large datasets.
Production Insight
A log analyser sorting IP addresses used base-10 Radix Sort on 10 million IPs — 10 passes, 100ms each = 1s.
Switching to base-256 gave 4 passes, cutting time to 400ms.
Rule: when sorting fixed-width integers, always use base 256 for optimal performance.
Key Takeaway
Use Counting Sort for small-range integer data.
Use Radix Sort for fixed-width keys (IPs, phone numbers) with base 256.
Avoid for floats, variable strings, or unbounded values — comparison sorts win there.

C++ and Python Implementations: Language-Specific Idioms and Performance

While Java's verbosity makes the algorithm explicit, C++ and Python offer different trade-offs. C++ allows in-place arrays and high-performance bit manipulation, while Python's list comprehensions and built-in sorting can make implementations concise but slower due to interpreter overhead.

Below are stable Counting Sort and LSD Radix Sort implementations in both languages. Notice how C++ uses std::vector and manual loops, while Python leverages list operations and the // integer division operator. The core logic (prefix-sum, right-to-left placement) remains identical across languages.

counting_radix_py.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Counting Sort (stable prefix-sum version) — Python
def counting_sort(arr, max_val):
    n = len(arr)
    count = [0] * (max_val + 1)
    output = [0] * n
    for x in arr:
        count[x] += 1
    for i in range(1, max_val + 1):
        count[i] += count[i-1]
    for i in range(n-1, -1, -1):
        output[count[arr[i]] - 1] = arr[i]
        count[arr[i]] -= 1
    return output

# Radix Sort (LSD, base 10) — Python
def radix_sort(arr):
    max_val = max(arr)
    exp = 1
    while max_val // exp > 0:
        count = [0] * 10
        output = [0] * len(arr)
        for x in arr:
            count[(x // exp) % 10] += 1
        for i in range(1, 10):
            count[i] += count[i-1]
        for i in range(len(arr)-1, -1, -1):
            digit = (arr[i] // exp) % 10
            output[count[digit] - 1] = arr[i]
            count[digit] -= 1
        arr = output
        exp *= 10
    return arr
Performance Note
Python's interpreted loops are slow for large arrays. For production use, prefer NumPy's np.sort() or C-extensions. The Python implementation above is clean but not optimised — expect 10-100x slowdown compared to C++ or Java for arrays over 100k elements.
Production Insight
A team migrated a Python data pipeline to C++ for Radix Sort and saw a 40x speed improvement on 5M records. The Python version was fine for prototyping but became a bottleneck in production. Always benchmark the actual language performance before choosing a non-comparison sort in Python.
Key Takeaway
C++ and Python follow the same algorithm pattern but differ dramatically in execution speed. Use C++ for performance-critical sorting; Python only for small or non-realtime data.

Digit-by-Digit Walkthrough: Radix Sort on [329, 457, 657, 839, 436, 720, 355]

Let's trace Radix Sort on a small array of three-digit numbers. The algorithm processes units, then tens, then hundreds.

Initial array: [329, 457, 657, 839, 436, 720, 355]

Pass 1 (units digit): - Group by last digit: 9→[329,839], 7→[457,657], 6→[436], 0→[720], 5→[355]. - After stable counting sort: [720, 329, 839, 436, 355, 457, 657] (units sorted ascending).

Pass 2 (tens digit): - Group by middle digit: 2→[720, 329, 839], 3→[436, 355], 5→[457, 657]. - After stable sort: [720, 329, 839, 436, 355, 457, 657] (tens sorted; notice 720 and 329 both have tens=2 but original order preserved).

Pass 3 (hundreds digit): - Group by first digit: 7→[720], 3→[329, 355], 8→[839], 4→[436, 457], 6→[657]. - After stable sort: [329, 355, 436, 457, 657, 720, 839].

The array is now fully sorted. Each pass refines the order without ever comparing two numbers directly.

Production Insight
This visual walkthrough is exactly what you'd debug with print statements. In production, add a DEBUG flag that logs the array after each pass. It helps catch stability bugs early (if equal digits are reordered, the final sort will be corrupt).
Key Takeaway
Radix Sort sorts fixed-width keys in exactly d passes. Each pass is a stable counting sort on one digit position, and the accumulation of passes yields global order.

Advantages and Disadvantages of Counting Sort and Radix Sort

Both algorithms trade generality for speed. Here's a succinct comparison:

AlgorithmAdvantagesDisadvantages
Counting Sort– O(n + k) time (linear when k small)
Radix Sort– O(d(n + b)) time (linear for fixed-width)

Both are unsuitable for strings of varying length or floating-point data without transformation.

Production Insight
In a real billing system, Counting Sort was chosen for sorting 100M transaction amount ranges (values 0–10,000). The 10k-slot count array used only 40KB, and the sort completed in 2 seconds. Radix Sort would have required many passes for no benefit. Always profile your specific data range.
Key Takeaway
Counting Sort wins for narrow ranges; Radix Sort wins for wide but fixed-width keys. Both are useless for general-purpose sorting.

LSD vs MSD Radix Sort: When to Use Each Variant

LSD (Least Significant Digit) Radix Sort processes digits from rightmost to leftmost. It is the simpler, iterative approach: exactly d passes for d-digit numbers. It works only for fixed-width keys and relies on the sub-sort being stable. Because it never creates recursive branches, it's cache-friendly and easy to parallelise per pass.

MSD (Most Significant Digit) Radix Sort sorts by the most significant digit first, then recursively sorts each bucket. It can handle variable-length keys (e.g., strings) because you can stop recursion when a bucket has size 1. However, recursion overhead is heavy, and it is generally slower for fixed-width numeric keys. MSD is often used in suffix array construction and string sorting where keys have different lengths.

Rule of thumb: Use LSD for fixed-width integers; use MSD for variable-length strings or when early termination can prune large subproblems.

Production Insight
A search engine team used MSD Radix Sort on 10M URLs (variable length). LSD would have padded all URLs to max length, wasting memory. MSD recursively sorted by first character, then second, etc., achieving 4x faster sorting than LSD with padding.
Key Takeaway
LSD is iterative and fast for fixed-width keys. MSD is recursive but handles variable-length keys naturally. Choose based on key characteristics.

Real-World Applications: Suffix Arrays, String Sorting, Packet Routing

  1. Suffix arrays – Genome sequencing and text indexing construct suffix arrays by sorting all suffixes of a string. Since suffixes are strings of equal length (up to n), LSD Radix Sort on the last character, then second-last, etc., builds the suffix array in O(n log n) but often faster in practice due to cache efficiency. This is the basis of tools like samtools and Bowtie.
  2. String sorting – When sorting fixed-length strings (e.g., licence plate numbers, telephone area codes), Radix Sort outperforms comparison sorts by a wide margin. Some databases use it internally for columnar storage indexes.
  3. Packet routing – Network routers classify packets by IP prefix (32-bit or 128-bit). Radix Sort on the binary representation allows O(n) sorting of routing table entries, enabling fast longest-prefix-match lookups. Cisco's IOS has used radix-like algorithms for decades.

These applications exploit the same core idea: transform the problem into sorting integers, then let Radix Sort's speed do the heavy lifting.

Production Insight
A bioinformatics pipeline sorted 100M nucleotide substrings using LSD Radix Sort. The comparison-based alternative (STL sort) took 45 seconds; Radix Sort took 8 seconds. That single change reduced the overall pipeline runtime by 20%.
Key Takeaway
Radix Sort is not just an academic exercise — it powers genome analysis, database indexing, and internet routing infrastructure.

Complexity Analysis — Why Radix Sort Beats O(n log n) but Comes With Strings Attached

Radix sort's time complexity isn't just one number — it depends on the number of digits in your keys. For an array of n integers, each with d digits, and a base k (typically 10), the complexity is O(d * (n + k)). Counting sort runs in O(n + k) per digit pass, and we run d passes.

Here's the thing: if d is small and fixed (like 32-bit integers, d = 10 in base 10), then Radix sort is effectively O(n). That's why it wrecks comparison sorts in benchmarks on large integer datasets. But the catch is space: counting sort allocates an auxiliary array of size k for each pass, so total space is O(n + k). For base 10, that's trivial. If you naively use base 256 to reduce passes, your aux array grows to 256 — still fine. But if your keys are 64-bit floats or variable-length strings, d can blow up, and suddenly those n log n comparison sorts start looking good again.

Best case: O(n) with fixed-width keys. Worst case: O(n * d) where d grows — like sorting UUIDs as strings. Then you're paying for the digits, not the array size.

RadixTimeAnalysis.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — dsa tutorial

public class RadixTimeAnalysis {
    public static void main(String[] args) {
        int n = 1_000_000;
        int digits = 10; // 32-bit integer in base 10
        int base = 10;
        double operations = (double) n * digits * (n + base) / n;
        System.out.println("Approx ops per element: " + operations);
        // vs comparison sort ~ n log2 n ≈ 20 ops per el
        System.out.println("Comparison sort O(n log n): ~" + (int)(Math.log(n)/Math.log(2)) + " ops/el");
        System.out.println("Radix wins for fixed-width ints.");
    }
}
Output
Approx ops per element: 10.00001
Comparison sort O(n log n): ~20 ops/el
Radix wins for fixed-width ints.
Production Trap: Variable-Length Keys
If your 'integers' are actually timestamps with microsecond precision, you've got 16+ digits. Radix sort then requires 16 passes — that's 16x the memory traffic. A well-tuned quicksort on the same data finishes faster due to cache locality. Know your key width before choosing Radix.
Key Takeaway
Radix sort is O(n) only when digit count is constant and small. Measure your keys, not just your array size.

Pseudocode — The Algorithm Without the Language-Specific Cruft

Before you write a single line of C++ or Java, you need the skeleton. Radix sort is deceptively simple: it's just Counting Sort wrapped in a loop over digits. The base determines how many buckets Counting Sort uses. LSD Radix sort iterates from least significant digit to most — that's the stable version that works on integers.

Here's the pseudocode that survives code reviews: find max value to determine number of digits, then for each digit position (starting from 1s place), call Counting Sort on that digit. Counting Sort must be stable — meaning it preserves the relative order of elements with equal digits. That stability is what makes Radix sort work: each pass builds on the previous one.

The loop invariant? After sorting by the i-th least significant digit, the array is sorted by those i digits. Think of it like sorting dates: first by day, then month, then year — each pass refines the order.

RadixPseudo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — dsa tutorial

// LSD Radix Sort Pseudocode (Base 10)
function radixSort(int[] arr):
    int max = findMax(arr)
    int digits = countDigits(max)
    for int exp = 1; max / exp > 0; exp *= 10:
        countingSortByDigit(arr, exp)

function countingSortByDigit(int[] arr, int exp):
    int[] output = new int[arr.length]
    int[] count = new int[10]  // 0-9 digits
    
    for int val : arr:
        count[(val / exp) % 10]++
    
    // prefix sum for stability
    for i = 1 to 9:
        count[i] += count[i - 1]
    
    for i = arr.length - 1 down to 0:
        int digit = (arr[i] / exp) % 10
        output[count[digit] - 1] = arr[i]
        count[digit]--
    
    copy output back to arr
Output
// exp = 1: sorted by units digit
// exp = 10: sorted by tens digit (within units)
// exp = 100: sorted by hundreds digit (within tens)
// Final array fully sorted
Senior Shortcut: Base Choice
Base doesn't have to be 10. Use base 256 for byte-level digit extraction (shift by 8 bits). That reduces passes for 32-bit ints from 10 to 4. Just beware: larger base means more Counting Sort buckets — memory cost is O(buckets) per pass. On modern hardware, base 256 is a sweet spot.
Key Takeaway
Radix sort = Counting Sort in a loop over digits. Stability in Counting Sort is not optional — it's the entire mechanism.
● Production incidentPOST-MORTEMseverity: high

Radix Sort Breaks on Negative Numbers in Production Log Analyzer

Symptom
ArrayIndexOutOfBoundsException at digit extraction during Radix Sort. The exception only appeared when negative timestamps entered the pipeline.
Assumption
Radix Sort handles all integers — the code was written for POSIX timestamps which are non-negative.
Root cause
Java's modulus operator returns negative remainder for negative numbers. getDigit(-10, 0) returns 0 instead of 0, but the issue is that the digit extraction algorithm (number / 10^k) % 10 on negative numbers can produce negative digits, which index into the count array with base 10 causing out-of-bounds.
Fix
Shift all values by abs(min) before sorting and subtract back after sorting. This ensures all values are non-negative during Radix Sort passes.
Key lesson
  • Non-comparison sorts assume input domain is non-negative integers. Always validate or normalise input.
  • Add a precondition check: if (any value < 0) throw new IllegalArgumentException or apply the shift transform.
  • Unit tests must include negative numbers and edge cases like Integer.MIN_VALUE.
Production debug guideDiagnose when a non-comparison sort underperforms or fails in production4 entries
Symptom · 01
Counting Sort runs slowly or throws OutOfMemoryError
Fix
Check value range k vs n. If k is more than 10x n, Counting Sort is likely the wrong choice. Switch to Radix Sort or comparison sort.
Symptom · 02
Radix Sort produces nearly sorted output but with ordering errors
Fix
Verify that the digit-level sort (Counting Sort) is STABLE. Replace with a stable implementation using prefix-sum and right-to-left traversal.
Symptom · 03
Radix Sort crashes with ArrayIndexOutOfBoundsException
Fix
Check for negative numbers in input. Use the shift transform or offset by abs(min) before sorting.
Symptom · 04
Sorting 50 million IDs takes 5x longer than expected
Fix
Measure the number of passes: base-10 Radix Sort on 9-digit numbers takes 9 passes. Switch to base-256 (4 passes) for 32-bit integers. Or consider using Arrays.parallelSort() if comparison sort is acceptable.
Sorting Algorithm Comparison
Feature / AspectCounting SortRadix SortComparison Sort (QuickSort)
Time ComplexityO(n + k)O(d * (n + k))O(n log n) average
Space ComplexityO(n + k)O(n + k) per passO(log n) in-place
Stable?Yes (prefix-sum version)Yes (depends on stable sub-sort)No (typical)
Handles large value ranges?No — k must be smallYes — processes digitsYes — no range limit
Handles negative numbers?No — needs offsetNo — needs offsetYes
Handles non-integers?NoOnly with numeric mappingYes
Best real-world useExam scores, pixel valuesPhone numbers, IP addressesGeneric data, strings
When it winsk <= nFixed-width keys, large nUnbounded or complex keys

Key takeaways

1
Counting Sort is O(n + k)
the k is the trap. It's genius when k is small (pixel values, grades, ages) and disastrous when k dwarfs n (sorting 100 numbers where values reach 10 billion).
2
Radix Sort is just Counting Sort applied repeatedly to one digit at a time. Its correctness depends entirely on the sub-sort being stable
break stability and the whole algorithm silently produces wrong output.
3
LSD Radix Sort using base 256 (byte-by-byte) gives you 4 passes over any 32-bit integer, outperforming base-10 (10 passes) with minimal extra memory
a real micro-optimisation worth knowing.
4
Neither algorithm handles negative numbers natively. The standard fix is to shift the entire dataset by abs(minValue) before sorting and shift back after
a two-line patch that makes them fully general for integer data.
5
Choosing the right algorithm is a trade-off between n, k, and memory. If k <= n, Counting Sort wins. If keys are fixed-width integers, Radix Sort wins. Otherwise, fall back to comparison sorts.

Common mistakes to avoid

4 patterns
×

Allocating count array based on max value instead of range

Symptom
Wasted memory or off-by-one errors when writing back. For example, if ages range from 18 to 65, allocating count[66] wastes indices 0-17.
Fix
Allocate count[maxValue - minValue + 1] and use value - minValue as the index throughout.
×

Using an unstable sort inside Radix Sort

Symptom
Output looks nearly sorted but has subtle ordering errors. Radix Sort relies on stability to keep previous passes' order.
Fix
Always use a stable sort for each digit pass. The prefix-sum Counting Sort with right-to-left iteration is the correct choice.
×

Applying Radix Sort to negative integers without handling signs

Symptom
ArrayIndexOutOfBoundsException because digit extraction on negative numbers yields negative remainders (Java's modulus behaviour).
Fix
Before sorting, find the minimum value. If negative, add abs(min) to every element to shift to non-negative, sort, then subtract abs(min) back.
×

Assuming Counting Sort is always faster than QuickSort

Symptom
Sorting 1000 numbers with range 0-10^9 leads to 4 GB count array and OOM.
Fix
Check k relative to n. If k > 10*n, consider Radix Sort or a comparison sort instead.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Why is Counting Sort said to break the O(n log n) lower bound, and does ...
Q02SENIOR
If I asked you to sort 50 million user IDs where each ID is a 9-digit nu...
Q03SENIOR
Radix Sort processes digits from least significant to most significant. ...
Q04SENIOR
How would you implement Radix Sort for a set of IPv4 addresses stored as...
Q01 of 04SENIOR

Why is Counting Sort said to break the O(n log n) lower bound, and does that mean it's always faster than QuickSort? Walk me through the exact conditions where Counting Sort wins and where it loses.

ANSWER
Counting Sort breaks the lower bound because it does not rely on comparisons. It uses the value range k to place elements directly. O(n log n) only applies to comparison-based sorts. Counting Sort wins when k is small (e.g., k <= n). It loses when k is huge because it allocates O(k) memory and the constant factor for scanning k can dominate. For example, sorting 1000 numbers with range 0-10^9: k = 1e9 >> n = 1000, so Counting Sort is far slower and memory-intensive than QuickSort.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is Counting Sort always faster than QuickSort?
02
Why does Radix Sort sort from the least significant digit first?
03
Can Radix Sort be used on strings?
04
What is the space complexity of an in-place Radix Sort?
05
How do you handle duplicates in Counting Sort?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Sorting. Mark it forged?

11 min read · try the examples if you haven't

Previous
Heap Sort
7 / 8 · Sorting
Next
Sorting Algorithm Comparison