Skip to content
Home DSA Counting Sort & Radix Sort — Negative Number Pitfalls

Counting Sort & Radix Sort — Negative Number Pitfalls

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Sorting → Topic 7 of 8
Java's modulus returns negative remainders, crashing Radix Sort on negative inputs.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Java's modulus returns negative remainders, crashing Radix Sort on negative inputs.
  • 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).
  • 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.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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.
Production Incident

Radix Sort Breaks on Negative Numbers in Production Log Analyzer

A log analyzer processing server timestamps hit ArrayIndexOutOfBoundsException after a schema change introduced negative offsets.
SymptomArrayIndexOutOfBoundsException at digit extraction during Radix Sort. The exception only appeared when negative timestamps entered the pipeline.
AssumptionRadix Sort handles all integers — the code was written for POSIX timestamps which are non-negative.
Root causeJava'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.
FixShift 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 Guide

Diagnose when a non-comparison sort underperforms or fails in production

Counting Sort runs slowly or throws OutOfMemoryErrorCheck 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.
Radix Sort produces nearly sorted output but with ordering errorsVerify that the digit-level sort (Counting Sort) is STABLE. Replace with a stable implementation using prefix-sum and right-to-left traversal.
Radix Sort crashes with ArrayIndexOutOfBoundsExceptionCheck for negative numbers in input. Use the shift transform or offset by abs(min) before sorting.
Sorting 50 million IDs takes 5x longer than expectedMeasure 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.

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.

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.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
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) {
        int[] frequency = new int[maxValue + 1]; // bucket for every possible value

        // Pass 1: count how many times each value appears
        for (int value : data) {
            frequency[value]++;
        }

        // 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.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
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) {
            this.name  = name;
            this.grade = grade;
        }

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

    static Student[] stableCountingSort(Student[] students, int maxGrade) {
        int n = students.length;
        int[] frequency = new int[maxGrade + 1];

        // Pass 1: count how many students have each grade
        for (Student student : students) {
            frequency[student.grade]++;
        }

        // 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.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
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) {
        int n         = numbers.length;
        int BASE      = 10;           // decimal digits: 0-9
        int[] count   = new int[BASE];
        int[] output  = new int[n];

        // Count occurrences of each digit at this position
        for (int number : numbers) {
            int digit = getDigit(number, digitPosition);
            count[digit]++;
        }

        // 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++) {
            System.out.println("  Pass " + (digitPosition + 1)
                + " (digit position " + digitPosition + "): " + Arrays.toString(current));
            current = countingSortByDigit(current, digitPosition);
        }

        return current;
    }

    public static void main(String[] args) {
        // Simulating 7-digit subscriber IDs (like the last 7 digits of a phone number)
        int[] subscriberIds = {
            3706142,
            1234567,
            9999001,
            3706140,  // nearly identical to first entry — tests fine-grained ordering
            5550199,
            1000000,
            2468135
        };

        System.out.println("Unsorted: " + Arrays.toString(subscriberIds));
        System.out.println();
        System.out.println("Radix Sort passes:");

        int[] sortedIds = radixSort(subscriberIds);

        System.out.println();
        System.out.println("Sorted:   " + Arrays.toString(sortedIds));

        // Verify correctness against a comparison sort
        int[] expected = Arrays.copyOf(subscriberIds, subscriberIds.length);
        Arrays.sort(expected);
        System.out.println("Correct:  " + Arrays.equals(sortedIds, expected));
    }
}
▶ Output
Unsorted: [3706142, 1234567, 9999001, 3706140, 5550199, 1000000, 2468135]

Radix Sort passes:
Pass 1 (digit position 0): [3706142, 1234567, 9999001, 3706140, 5550199, 1000000, 2468135]
Pass 2 (digit position 1): [3706140, 3706142, 9999001, 1000000, 2468135, 1234567, 5550199]
Pass 3 (digit position 2): [9999001, 1000000, 3706140, 3706142, 5550199, 1234567, 2468135]
Pass 4 (digit position 3): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]
Pass 5 (digit position 4): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]
Pass 6 (digit position 5): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]
Pass 7 (digit position 6): [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]

Sorted: [1000000, 1234567, 2468135, 3706140, 3706142, 5550199, 9999001]
Correct: true
⚠ Watch Out:
Radix Sort only works correctly on non-negative integers out of the box. If your data has negative numbers, you need to either offset all values by the minimum (shift to make everything non-negative), or handle the sign separately. Forgetting this is one of the most common Radix Sort bugs in interviews.
📊 Production Insight
A streaming log processor used Radix Sort on timestamps that included negative offsets. The ArrayIndexOutOfBoundsException crashed the pipeline every 6 hours.
Rule: always shift negative values by abs(min) before Radix Sort, or use a sign-aware wrapper.
🎯 Key Takeaway
Radix Sort uses stable subsorts per digit (LSD).
Digit count determines passes — base-256 reduces passes dramatically.
Negative numbers require explicit handling — never assume non-negative input.

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.java · JAVA
1234567891011121314
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
Mental Model: The Range Budget
Think of your memory budget as k * 4 bytes per slot. If k exceeds n by a factor of 10, you're burning 40x more memory than necessary.
  • 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.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
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) {
        int n      = addresses.length;
        int BASE   = 256; // one byte at a time: 0-255
        int[] count  = new int[BASE];
        long[] output = new long[n];

        for (long address : addresses) {
            int byteValue = (int)((address >> (bytePosition * 8)) & 0xFF);
            count[byteValue]++;
        }

        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
10.0.0.2
172.16.254.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.
🗂 Sorting Algorithm Comparison
Counting Sort vs Radix Sort vs Comparison Sorts
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

  • 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).
  • 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.
  • 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.
  • 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.
  • 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

    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 Questions on This Topic

  • QWhy 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.SeniorReveal
    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.
  • QIf I asked you to sort 50 million user IDs where each ID is a 9-digit number, would you use Counting Sort, Radix Sort, or a comparison sort? Justify your choice with time and space complexity analysis.SeniorReveal
    Radix Sort is the best choice. Counting Sort would need a count array of size 10^9 (since IDs are 9-digit, up to 999,999,999) — 4 GB for the count array alone, impractical. Radix Sort with base 256 processes the 9-digit (30-bit) numbers in 4 passes, each pass O(n + 256) time and O(n + 256) space. That's about 200 MB per pass (50M 4 bytes for output) and a tiny count array. Comparison sort (QuickSort) would be O(n log n) ~ 50M 26 ≈ 1.3B comparisons, slower. Radix Sort gives O(4 * 50M) ~ 200M operations, significantly faster with acceptable memory.
  • QRadix Sort processes digits from least significant to most significant. What breaks if you reverse that order and go most significant to least significant? Can MSD (Most Significant Digit) Radix Sort be made to work, and how does it differ from LSD?SeniorReveal
    If you go most significant first without recursion, the entire sort collapses because later passes (less significant digits) cannot rearrange groups already separated. MSD Radix Sort works by recursively sorting each bucket: after sorting by the most significant digit, you recursively apply MSD to each bucket independently. This requires more complex recursion or stack management, and it is not stable across groups unless implemented carefully. LSD is simpler and requires exactly d passes without recursion, making it the preferred method for fixed-width keys. MSD is useful for variable-length strings with early termination.
  • QHow would you implement Radix Sort for a set of IPv4 addresses stored as strings in dotted-decimal format? What base would you use and how many passes?Mid-levelReveal
    Convert each IP to a 32-bit unsigned integer (or a long to avoid sign). Then use Radix Sort with base 256, sorting by byte position from least significant byte (LSB) to most significant (MSB). 4 passes total. The count array size is 256 per pass. The digit extraction uses bit shifting: (value >> (bytePos * 8)) & 0xFF. After sorting, convert back to dotted-decimal. This is the most efficient approach — 4 passes, each O(n + 256).

Frequently Asked Questions

Is Counting Sort always faster than QuickSort?

No. Counting Sort is faster when the value range k is comparable to or smaller than n (the number of elements). If you're sorting 1000 numbers that range from 0 to 1,000,000,000, Counting Sort allocates a billion-entry array and becomes both slower and memory-exhausting. QuickSort's O(n log n) wins there. The crossover point is roughly when k exceeds n by a large factor.

Why does Radix Sort sort from the least significant digit first?

Because each pass is stable, sorting least-significant-first means earlier passes are preserved within the groupings of later passes. If you sort most-significant-first you'd need to recursively apply the sort within each group (MSD Radix Sort), which is significantly more complex to implement. LSD is simpler, always iterative, and produces a fully sorted array after exactly d passes.

Can Radix Sort be used on strings?

Yes, if the strings are all the same length. Treat each character as a digit and sort from the last character to the first, using Counting Sort with base 128 (ASCII) or base 26 (letters only). For variable-length strings you either pad them to the same length or use MSD Radix Sort with early termination, which is how many suffix array algorithms work in bioinformatics and search engine indexing.

What is the space complexity of an in-place Radix Sort?

Standard Radix Sort is not in-place. It requires O(n) extra space for the output array per pass (though you can reuse two arrays). In-place Radix Sort exists but is significantly slower. The typical space complexity is O(n + base) per pass, where base is typically 256. If you reuse the same two arrays, total extra space is O(n) plus O(base).

How do you handle duplicates in Counting Sort?

The stable version with prefix sums handles duplicates naturally. Each duplicate is placed in a separate slot determined by its original order (right-to-left traversal ensures stability). The count array records frequency, and the prefix sum gives each value a contiguous block of output positions. Duplicates fill this block in reverse input order, preserving relative order.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousHeap SortNext →Sorting Algorithm Comparison
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged