Radix Sort

Problem Solved

Radix Sort solves the Sorting problem.

Design and Strategy

Radix Sort is a non-comparison-based sorting algorithm that exploits a space–time tradeoff to efficiently sort integers. It processes keys digit by digit, from the least significant digit (LSD) to the most significant. At each digit position, it uses a stable sort—usually Counting Sort—to reorder elements by that digit while preserving the order of elements with equal digits. Each pass is essentially a call to Counting Sort, applied not to the whole key, but to a single digit of each number. If you are not familiar with Counting Sort, you should read that section before continuing.

Let \( n \) be the number of items to sort, \( d \) the number of digits in the largest key (in base \( k \)), and \( k \) the radix (e.g., 10 for decimal). The algorithm performs \( d \) passes—one for each digit position—by looping through \( p = 0 \) to \( d - 1 \).

Each pass uses three arrays:

The algorithm works as follows:

  1. For each digit position \( p = 0 \) to \( d - 1 \), do the following:
    1. Count Digit Occurrences:
      • Initialize \( C[0 \ldots k-1] = 0 \).
      • For each index \( i \) from \( 0 \) to \( n - 1 \):
        • Compute the digit at position \( p \):
          \( d_i = \left\lfloor \frac{A[i]}{k^p} \right\rfloor \bmod k \)
        • Increment the corresponding count: \( C[d_i] = C[d_i] + 1 \)
    2. Prefix Sum:
      For \( j = 1 \) to \( k-1 \), set \( C[j] = C[j] + C[j-1] \) to determine final positions.
    3. Stable Placement:
      Traverse \( A \) from right to left. For each \( A[i] \), compute \( d_i \), decrement \( C[d_i] \), and set \( B[C[d_i]] = A[i] \).
    4. Prepare for Next Pass:
      Copy \( B \) back to \( A \).

As previously mentioned, steps a through c are just a modified version of Counting Sort.

Example: Sort the array \( [329,\, 457,\, 657,\, 839,\, 436,\, 720,\, 355] \) in base 10.
Pass Digit Position Keys (sorted by current digit)
1 Units (digit 0) \([720,\, 355,\, 436,\, 457,\, 657,\, 329,\, 839]\)
2 Tens (digit 1) \([720,\, 329,\, 436,\, 839,\, 355,\, 457,\, 657]\)
3 Hundreds (digit 2) \([329,\, 355,\, 436,\, 457,\, 657,\, 720,\, 839]\)

After three passes, the array is fully sorted. Because each digit is sorted stably, the effects of previous digit orders are preserved.

Implementation in Java, C++, Python

Here are implementations using base \(k=10\) (base 10).
public void radixSort(int[] A) {
    int n = A.length;

    // Find maximum value
    int max = A[0];
    for (int i = 1; i < n; i++) {
        if (A[i] > max) max = A[i];
    }

    int[] B = new int[n];

    // Process each digit using exp = 1, 10, 100, ...
    for (int exp = 1; max / exp > 0; exp *= 10) {
        int[] C = new int[10];

        // Phase 1: Count digit occurrences
        for (int i = 0; i < n; i++) {
            int digit = (A[i] / exp) % 10;
            C[digit]++;
        }

        // Phase 2: Compute prefix sums
        for (int i = 1; i < 10; i++) {
            C[i] += C[i - 1];
        }

        // Phase 3: Stable placement into B
        for (int i = n - 1; i >= 0; i--) {
            int digit = (A[i] / exp) % 10;
            B[--C[digit]] = A[i];
        }

        // Copy back for next pass
        System.arraycopy(B, 0, A, 0, n);
    }
}
void radixSort(int A[], int n) {
    // Find maximum value
    int maxv = A[0];
    for (int i = 1; i < n; i++) {
        if (A[i] > maxv) maxv = A[i];
    }

    int* B = new int[n];

    // Process each digit using exp = 1, 10, 100, ...
    for (int exp = 1; maxv / exp > 0; exp *= 10) {
        int C[10] = {0};

        // Phase 1: Count digit occurrences
        for (int i = 0; i < n; i++) {
            int digit = (A[i] / exp) % 10;
            C[digit]++;
        }

        // Phase 2: Compute prefix sums
        for (int i = 1; i < 10; i++) {
            C[i] += C[i - 1];
        }

        // Phase 3: Stable placement into B
        for (int i = n - 1; i >= 0; i--) {
            int digit = (A[i] / exp) % 10;
            B[--C[digit]] = A[i];
        }

        // Copy back for next pass
        for (int i = 0; i < n; i++) {
            A[i] = B[i];
        }
    }

    delete[] B;
}
def radix_sort(A):
    if not A:
        return A

    n = len(A)

    # Find maximum value
    maxv = A[0]
    for x in A:
        if x > maxv:
            maxv = x

    B = [0] * n
    exp = 1

    # Process each digit using exp = 1, 10, 100, ...
    while maxv // exp > 0:
        C = [0] * 10

        # Phase 1: Count digit occurrences
        for x in A:
            digit = (x // exp) % 10
            C[digit] += 1

        # Phase 2: Compute prefix sums
        for i in range(1, 10):
            C[i] += C[i - 1]

        # Phase 3: Stable placement into B
        for i in range(n - 1, -1, -1):
            digit = (A[i] // exp) % 10
            C[digit] -= 1
            B[C[digit]] = A[i]

        # Copy back for next pass
        A = B[:]
        exp *= 10

    return A

Time and Space Analysis

Before analyzing Radix Sort’s performance, we introduce three key parameters: Let \(n\) be the number of keys to sort, \(k\) the radix (number of buckets per digit, e.g. 10 for decimal or 256 for byte-values), and \(d\) the number of digit positions in the largest key (for a maximum key of value \(M\), roughly \(\lfloor \log_{k} M\rfloor + 1\)).

Time Complexity: Each pass of Radix Sort processes one digit position. It consists of four phases, each touching either the input array \(A\) or the count array \(C\):

  1. Counting Phase: Scan \(A[0\ldots n-1]\). For each key, extract the current digit in \(O(1)\) and increment the corresponding bucket in \(C\).
    Cost: \(\Theta(n)\).
  2. Prefix-Sum Phase: Transform \(C[0\ldots k-1]\) into cumulative counts so each entry indicates its endpoint in the output array.
    Cost: \(\Theta(k)\).
  3. Placement Phase: Traverse \(A\) backwards, recompute each digit, decrement its count, and write the key into the output buffer \(B\), preserving stability.
    Cost: \(\Theta(n)\).
  4. Copy-Back Phase: Copy \(B[0\ldots n-1]\) back into \(A\) to prepare for the next digit pass.
    Cost: \(\Theta(n)\).

Summing these four phases gives \(\Theta(3n + k)\) work per pass, which simplifies to \(\Theta(n + k)\).

Radix Sort makes one such digit pass for each of the \(d\) digits in the largest key. Therefore, the total running time is
\(\Theta\bigl(d \times (n + k)\bigr)\).

Space Complexity: Aside from the input array, Radix Sort requires:

So the total auxiliary space needed is \(\Theta(n + k)\).

Variations/Improvements

Links to Resources

Reading Comprehension Questions

  1. Stability: Why must the intermediate sort in Radix Sort be stable, and what could go wrong if it isn’t?
  2. Non-Comparison Basis: Explain in your own words why Radix Sort is non-comparison-based and how it leverages Counting Sort for each digit.
  3. Complexity Derivation: Show how one pass costs \(\Theta(n + k)\) and derive the overall time bound \(\Theta(d\,(n + k))\).
  4. Radix Choice: If you double the radix \(k\), how does that affect (a) the number of passes \(d\), and (b) the cost per pass?
  5. Space vs Time: Describe the space complexity \(\Theta(n + k)\) and discuss the trade-off between extra memory and per-pass speed.
  6. LSD vs MSD: Contrast the Least Significant Digit (LSD) and Most Significant Digit (MSD) variants in terms of recursion, handling of variable-length keys, and average work.
  7. Bit-Grouping: For a 32-bit key, compare the effects of using radix \(k=2^8\) (byte-by-byte) versus \(k=2^{16}\) (half-word).
  8. Input Distribution: How do “already sorted” or “reverse sorted” inputs affect the behavior of each pass’s count array?

In-Class Activities

  1. Card-and-Bucket Simulation: Use index cards labeled with numbers and buckets labeled 0–9 to perform two full passes of Radix Sort by hand.
  2. Radix Comparison: In small groups, run the demo with base-2, base-10, and base-256 on the same data and record the number of operations and memory used.
  3. Stability Experiment: Replace the stable Counting Sort step with an unstable sort (e.g. swapping on ties) and observe how the final result changes.
  4. Code Workshop: Pair up to implement the digit-extraction and count-array phases in your language of choice; test on various ranges of keys.
  5. Pseudocode Review: Write pseudocode for the MSD variant and discuss how early termination on singleton buckets can save work.
  6. Memory Trade-Off Debate: Discuss when it’s worth using a larger radix (more memory) versus more passes (more time)—cite cache behavior and real hardware.
  7. In-Place Variant Design: Sketch on the whiteboard an in-place cycle-leader version, including how you’d detect and close each cycle.
  8. Logging & Analysis: Augment the demo code to log the contents of \(C\) and \(B\) after each phase; swap logs with another group and explain their run.
  9. Parameterization Exercise: Modify the provided code so that the radix \(k\) is passed as a function parameter rather than hard-coded.
  10. Extension Challenge: Propose how you might parallelize the Counting and Placement phases on a multi-core processor, and outline synchronization points.
  11. Negative Keys: How would you adapt the algorithm to sort an array containing both positive and negative integers?

Problems

  1. Negative and Positive Integers: Implement Radix Sort to handle both negative and positive values in the same pass, and analyze its complexity.
  2. In-Place Cycle-Leader Sort: Code an in-place variant using cycle-leader permutation. Prove its correctness and compare its memory use.
  3. Empirical Radix Study: For \(n=10^5\), benchmark radix bases \(k=2\), \(10\), \(256\), and \(2^{16}\). Plot running time versus \(k\) and explain the results.
  4. Hybrid Design: Design a hybrid Radix-QuickSort algorithm that switches to Quick Sort when subarrays become small. Argue where the crossover point should be.
  5. MSD Recursion Depth: Analyze the worst-case recursion depth of the MSD variant on random strings of length \(d\) drawn from an alphabet of size \(k\).
  6. Floating-Point Keys: Extend Radix Sort to IEEE-754 floats by handling sign, exponent, and mantissa correctly. Include both pseudocode and proof of total order preservation.
  7. Parallel Radix Sort: Implement a multi-threaded version that divides Counting and Placement work across threads. Measure speedup on a multi-core machine.
  8. Stability Proof: Formally prove that using a stable Counting Sort at each digit pass guarantees a correct overall sort.
  9. Adaptive Radix Selection: Given input size \(n\) and maximum key \(M\), propose a heuristic to choose the optimal radix \(k\) that balances \(d\) and \(k\). Justify your heuristic experimentally.
  10. Leveraging Bit Operations: Give the code/pseudocode for a version that uses \(k=2^a\) for some choice of \(a\). Your implementation should take advantage of the fact that the base being a powers of two allows a more efficient implementation of some of the steps.