CrackedRuby CrackedRuby

Counting Sort and Radix Sort

Overview

Counting Sort and Radix Sort represent a class of sorting algorithms that break the O(n log n) lower bound of comparison-based sorting by making assumptions about the input data. These algorithms operate on integer keys or data that can be represented as integers, achieving linear time complexity under specific conditions.

Counting Sort works by counting the number of occurrences of each distinct element, then using arithmetic to determine the position of each element in the sorted output. The algorithm requires knowledge of the range of input values and allocates auxiliary space proportional to this range.

Radix Sort processes integers digit by digit, using a stable sorting algorithm (typically Counting Sort) as a subroutine. By sorting from the least significant digit to the most significant digit, or vice versa, Radix Sort achieves linear time complexity relative to the number of digits.

Both algorithms emerged from the need to sort large datasets efficiently when data characteristics are known. Counting Sort traces back to Harold Seward's work in the 1950s on sorting punch cards. Radix Sort has even earlier origins in mechanical sorting machines used for tabulating census data.

The primary advantage of these algorithms is performance on suitable datasets. Counting Sort runs in O(n + k) time where k is the range of input values. Radix Sort runs in O(d * (n + k)) where d is the number of digits. When k and d are small relative to n, these algorithms significantly outperform comparison-based alternatives.

# Counting Sort example with small range
data = [4, 2, 2, 8, 3, 3, 1]
max_val = 8

# Count occurrences
counts = Array.new(max_val + 1, 0)
data.each { |num| counts[num] += 1 }

# Build sorted array
sorted = []
counts.each_with_index do |count, num|
  count.times { sorted << num }
end

sorted
# => [1, 2, 2, 3, 3, 4, 8]

Key Principles

Counting Sort operates on three fundamental steps: counting, accumulation, and placement. The counting phase creates a histogram of the input data, tallying how many times each value appears. The accumulation phase transforms these counts into positions by computing prefix sums. The placement phase iterates through the input in reverse order, using the accumulated counts to place each element in its correct position.

The algorithm requires that input values fall within a known range [min, max]. The auxiliary space requirements scale with the range k = max - min + 1, not with the number of elements n. This creates a critical trade-off: Counting Sort excels when k = O(n) but becomes impractical when k >> n.

Stability is a defining characteristic of Counting Sort implementations. A sorting algorithm is stable if it preserves the relative order of elements with equal keys. Stability matters when sorting composite objects by one field while maintaining the order established by previous sorts. The reverse iteration during placement ensures stability by placing equal elements in their original relative order.

# Demonstrating stability with composite objects
Person = Struct.new(:name, :age)
people = [
  Person.new("Alice", 30),
  Person.new("Bob", 25),
  Person.new("Charlie", 30)
]

# Stable sort by age preserves Alice before Charlie (both age 30)
def counting_sort_by_age(people)
  min_age = people.map(&:age).min
  max_age = people.map(&:age).max
  range = max_age - min_age + 1
  
  counts = Array.new(range, 0)
  people.each { |p| counts[p.age - min_age] += 1 }
  
  # Prefix sum
  (1...range).each { |i| counts[i] += counts[i - 1] }
  
  output = Array.new(people.length)
  (people.length - 1).downto(0) do |i|
    age_index = people[i].age - min_age
    output[counts[age_index] - 1] = people[i]
    counts[age_index] -= 1
  end
  
  output
end

Radix Sort builds upon Counting Sort by processing multi-digit numbers one digit at a time. The algorithm partitions the sorting problem into d independent sorts, where d is the number of digits. Each digit-level sort must be stable to ensure correctness.

Two variants exist: Least Significant Digit (LSD) and Most Significant Digit (MSD). LSD Radix Sort processes digits from right to left, sorting by the ones place, then tens place, then hundreds place, and so on. This approach works because stable sorting by digit d preserves the order established by sorting digits 0 through d-1.

MSD Radix Sort processes digits from left to right, creating a recursive partitioning similar to quicksort. After sorting by the most significant digit, the algorithm recursively sorts each bucket by remaining digits. MSD can terminate early for buckets containing identical prefixes, providing performance advantages on certain datasets.

The choice of radix (base) affects performance. Binary (base 2) requires d = log₂(max) passes, while base 256 requires d = log₂₅₆(max) passes. Larger radices reduce the number of passes but increase the auxiliary space and cache misses per pass. Base 10 provides intuitive debugging, while base 256 aligns with byte boundaries in binary data.

# LSD Radix Sort processing digits right to left
def radix_sort_lsd(array)
  return array if array.empty?
  
  max_val = array.max
  exp = 1
  
  while max_val / exp > 0
    array = counting_sort_by_digit(array, exp)
    exp *= 10
  end
  
  array
end

def counting_sort_by_digit(array, exp)
  output = Array.new(array.length)
  counts = Array.new(10, 0)
  
  # Count digit occurrences
  array.each do |num|
    digit = (num / exp) % 10
    counts[digit] += 1
  end
  
  # Prefix sum
  (1...10).each { |i| counts[i] += counts[i - 1] }
  
  # Build output (reverse for stability)
  (array.length - 1).downto(0) do |i|
    digit = (array[i] / exp) % 10
    output[counts[digit] - 1] = array[i]
    counts[digit] -= 1
  end
  
  output
end

Ruby Implementation

Ruby provides the necessary array manipulation and arithmetic operations to implement both algorithms efficiently. The standard library does not include Counting Sort or Radix Sort, as the built-in sort method uses a hybrid algorithm (Timsort) optimized for general-purpose sorting.

A complete Counting Sort implementation must handle negative numbers by shifting the range. The algorithm determines the minimum and maximum values, creates a count array sized to the range, and offsets all indices by the minimum value.

def counting_sort(array)
  return [] if array.empty?
  
  min_val = array.min
  max_val = array.max
  range = max_val - min_val + 1
  
  # Count occurrences
  counts = Array.new(range, 0)
  array.each { |num| counts[num - min_val] += 1 }
  
  # Transform counts to positions (prefix sum)
  (1...range).each { |i| counts[i] += counts[i - 1] }
  
  # Place elements (reverse iteration for stability)
  output = Array.new(array.length)
  (array.length - 1).downto(0) do |i|
    index = array[i] - min_val
    output[counts[index] - 1] = array[i]
    counts[index] -= 1
  end
  
  output
end

# Example with negative numbers
data = [5, -1, 3, -1, 0, 2]
counting_sort(data)
# => [-1, -1, 0, 2, 3, 5]

For sorting objects by a numeric key, the implementation extracts keys, tracks original objects, and rebuilds the sorted array. This pattern appears frequently when sorting database records or structured data.

def counting_sort_by(array, &key_extractor)
  return [] if array.empty?
  
  keys = array.map(&key_extractor)
  min_key = keys.min
  max_key = keys.max
  range = max_key - min_key + 1
  
  counts = Array.new(range, 0)
  keys.each { |key| counts[key - min_key] += 1 }
  
  (1...range).each { |i| counts[i] += counts[i - 1] }
  
  output = Array.new(array.length)
  (array.length - 1).downto(0) do |i|
    key = keys[i]
    index = key - min_key
    output[counts[index] - 1] = array[i]
    counts[index] -= 1
  end
  
  output
end

# Sort strings by length
words = ["cat", "be", "apple", "dog", "at"]
counting_sort_by(words, &:length)
# => ["be", "at", "cat", "dog", "apple"]

Radix Sort in Ruby requires careful digit extraction and base selection. The implementation uses integer division and modulo operations to isolate individual digits. For base 10, digit extraction uses (num / 10^position) % 10.

def radix_sort(array, base: 10)
  return array if array.empty?
  
  max_val = array.max
  exp = 1
  
  while max_val / exp > 0
    array = counting_sort_by_digit(array, exp, base)
    exp *= base
  end
  
  array
end

def counting_sort_by_digit(array, exp, base)
  output = Array.new(array.length)
  counts = Array.new(base, 0)
  
  array.each do |num|
    digit = (num / exp) % base
    counts[digit] += 1
  end
  
  (1...base).each { |i| counts[i] += counts[i - 1] }
  
  (array.length - 1).downto(0) do |i|
    digit = (array[i] / exp) % base
    output[counts[digit] - 1] = array[i]
    counts[digit] -= 1
  end
  
  output
end

# Example with different bases
data = [170, 45, 75, 90, 802, 24, 2, 66]
radix_sort(data, base: 10)
# => [2, 24, 45, 66, 75, 90, 170, 802]

radix_sort(data, base: 16)  # Hexadecimal
# => [2, 24, 45, 66, 75, 90, 170, 802]

Handling negative numbers in Radix Sort requires splitting the array into negative and positive partitions, sorting each separately, and concatenating the results. The negative partition sorts by absolute value in descending order.

def radix_sort_with_negatives(array)
  return array if array.empty?
  
  negatives = array.select { |n| n < 0 }.map(&:-@)
  positives = array.select { |n| n >= 0 }
  
  sorted_negatives = radix_sort(negatives).reverse.map(&:-@)
  sorted_positives = radix_sort(positives)
  
  sorted_negatives + sorted_positives
end

data = [170, -45, 75, -90, 24, -2, 66]
radix_sort_with_negatives(data)
# => [-90, -45, -2, 24, 66, 75, 170]

MSD Radix Sort implementation in Ruby uses recursion to process buckets independently. The algorithm partitions by the current digit, then recursively sorts each non-empty bucket by remaining digits.

def radix_sort_msd(array, base: 10)
  return array if array.length <= 1
  
  max_val = array.max
  max_digits = Math.log(max_val, base).floor + 1
  
  msd_sort_recursive(array, 0, array.length, max_digits - 1, base)
end

def msd_sort_recursive(array, start, length, digit, base)
  return array if length <= 1 || digit < 0
  
  exp = base ** digit
  counts = Array.new(base + 1, 0)
  
  # Count digit occurrences
  (start...start + length).each do |i|
    d = (array[i] / exp) % base
    counts[d + 1] += 1
  end
  
  # Prefix sum
  (0...base).each { |i| counts[i + 1] += counts[i] }
  
  # Rearrange elements
  temp = Array.new(length)
  (start...start + length).each do |i|
    d = (array[i] / exp) % base
    temp[counts[d]] = array[i]
    counts[d] += 1
  end
  
  # Copy back
  temp.each_with_index do |val, i|
    array[start + i] = val
  end
  
  # Recursively sort buckets
  (0...base).each do |d|
    bucket_start = start + counts[d]
    bucket_length = counts[d + 1] - counts[d]
    msd_sort_recursive(array, bucket_start, bucket_length, digit - 1, base)
  end
  
  array
end

Practical Examples

Counting Sort excels when sorting ages in a demographic database. Ages typically range from 0 to 120, providing a small constant range regardless of dataset size.

class Demographics
  def self.sort_by_age(people)
    return [] if people.empty?
    
    # Ages range from 0 to 120
    MAX_AGE = 120
    counts = Array.new(MAX_AGE + 1, 0)
    
    people.each { |person| counts[person[:age]] += 1 }
    (1..MAX_AGE).each { |i| counts[i] += counts[i - 1] }
    
    output = Array.new(people.length)
    (people.length - 1).downto(0) do |i|
      age = people[i][:age]
      output[counts[age] - 1] = people[i]
      counts[age] -= 1
    end
    
    output
  end
end

people = [
  { name: "Alice", age: 34 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 34 },
  { name: "Diana", age: 25 }
]

Demographics.sort_by_age(people)
# => [
#   { name: "Bob", age: 25 },
#   { name: "Diana", age: 25 },
#   { name: "Alice", age: 34 },
#   { name: "Charlie", age: 34 }
# ]

Radix Sort handles sorting IP addresses represented as 32-bit integers. Each octet (8 bits) forms a digit in base 256, requiring exactly four passes.

class IPSorter
  def self.ip_to_int(ip)
    octets = ip.split('.').map(&:to_i)
    (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]
  end
  
  def self.int_to_ip(num)
    [
      (num >> 24) & 0xFF,
      (num >> 16) & 0xFF,
      (num >> 8) & 0xFF,
      num & 0xFF
    ].join('.')
  end
  
  def self.sort(ip_addresses)
    integers = ip_addresses.map { |ip| ip_to_int(ip) }
    
    # Radix sort with base 256 (one byte at a time)
    4.times do |pass|
      shift = pass * 8
      integers = counting_sort_byte(integers, shift)
    end
    
    integers.map { |num| int_to_ip(num) }
  end
  
  def self.counting_sort_byte(array, shift)
    counts = Array.new(256, 0)
    
    array.each do |num|
      byte = (num >> shift) & 0xFF
      counts[byte] += 1
    end
    
    (1...256).each { |i| counts[i] += counts[i - 1] }
    
    output = Array.new(array.length)
    (array.length - 1).downto(0) do |i|
      byte = (array[i] >> shift) & 0xFF
      output[counts[byte] - 1] = array[i]
      counts[byte] -= 1
    end
    
    output
  end
end

ips = ["192.168.1.10", "10.0.0.1", "192.168.1.5", "172.16.0.1"]
IPSorter.sort(ips)
# => ["10.0.0.1", "172.16.0.1", "192.168.1.5", "192.168.1.10"]

Bucket sort combined with Radix Sort handles floating-point numbers by separating them into buckets based on integer parts, then using Radix Sort within each bucket.

def bucket_radix_sort(floats, num_buckets: 10)
  return floats if floats.empty?
  
  min_val = floats.min
  max_val = floats.max
  range = max_val - min_val
  bucket_size = range / num_buckets
  
  buckets = Array.new(num_buckets) { [] }
  
  # Distribute into buckets
  floats.each do |num|
    index = ((num - min_val) / bucket_size).floor
    index = num_buckets - 1 if index >= num_buckets
    buckets[index] << num
  end
  
  # Sort each bucket and concatenate
  buckets.flat_map do |bucket|
    next [] if bucket.empty?
    
    # Convert to integers, sort, convert back
    scale = 1000
    integers = bucket.map { |f| (f * scale).round }
    sorted_ints = radix_sort(integers)
    sorted_ints.map { |i| i / scale.to_f }
  end
end

data = [0.897, 0.565, 0.656, 0.123, 0.665, 0.343]
bucket_radix_sort(data)
# => [0.123, 0.343, 0.565, 0.656, 0.665, 0.897]

Sorting dates efficiently uses Counting Sort on a composite key formed from year, month, and day components.

require 'date'

class DateSorter
  def self.sort_dates(date_strings)
    dates = date_strings.map { |ds| Date.parse(ds) }
    
    # Convert to sortable integer: YYYYMMDD
    date_keys = dates.map do |d|
      d.year * 10000 + d.month * 100 + d.day
    end
    
    sorted_keys = radix_sort(date_keys)
    
    # Convert back to dates
    sorted_keys.map do |key|
      year = key / 10000
      month = (key / 100) % 100
      day = key % 100
      Date.new(year, month, day).to_s
    end
  end
end

dates = ["2024-03-15", "2023-12-01", "2024-01-20", "2023-12-15"]
DateSorter.sort_dates(dates)
# => ["2023-12-01", "2023-12-15", "2024-01-20", "2024-03-15"]

Performance Considerations

Counting Sort achieves O(n + k) time complexity where n is the number of elements and k is the range of values. The algorithm makes exactly one pass to count elements, one pass to compute prefix sums, and one pass to place elements. The space complexity is O(n + k) for the output array and count array.

The performance degrades when k >> n. Sorting one million random integers between 0 and one billion requires a count array with one billion entries, consuming gigabytes of memory. This makes Counting Sort impractical despite its theoretical linear time.

The constant factors in Counting Sort are small. The algorithm performs simple arithmetic operations (incrementing, array access) without comparisons or swaps. Cache performance suffers when the count array exceeds cache size, as the counting phase exhibits poor spatial locality.

require 'benchmark'

def compare_sorting_methods(size, range)
  data = Array.new(size) { rand(range) }
  
  Benchmark.bm(20) do |x|
    x.report("counting_sort") { counting_sort(data.dup) }
    x.report("radix_sort") { radix_sort(data.dup) }
    x.report("Array#sort") { data.dup.sort }
  end
end

# Small range - Counting Sort wins
compare_sorting_methods(100_000, 1000)
# counting_sort:  0.015s
# radix_sort:     0.025s
# Array#sort:     0.045s

# Large range - Counting Sort loses
compare_sorting_methods(100_000, 1_000_000)
# counting_sort:  0.850s (high memory)
# radix_sort:     0.035s
# Array#sort:     0.048s

Radix Sort achieves O(d * (n + b)) time complexity where d is the number of digits, n is the number of elements, and b is the base. For integers up to maximum value m, d = log_b(m). The space complexity is O(n + b) for the output array and count array used in each pass.

The choice of base creates a time-space trade-off. Base 2 minimizes space (count array of size 2) but maximizes passes (d = log₂(m)). Base 256 reduces passes (d = log₂₅₆(m)) but requires a count array of size 256. Base 10 provides a middle ground with intuitive debugging.

For 32-bit integers (m = 2³²), base 2 requires 32 passes, base 10 requires 10 passes, base 16 requires 8 passes, and base 256 requires 4 passes. Higher bases reduce the number of passes but increase the cost per pass due to larger count arrays and worse cache performance.

def analyze_radix_bases(data)
  bases = [2, 10, 16, 256]
  max_val = data.max
  
  bases.each do |base|
    digits = Math.log(max_val, base).ceil
    passes = digits
    count_array_size = base
    
    puts "Base #{base}: #{passes} passes, count array size #{count_array_size}"
  end
end

data = Array.new(100_000) { rand(2**32) }
analyze_radix_bases(data)
# Base 2: 32 passes, count array size 2
# Base 10: 10 passes, count array size 10
# Base 16: 8 passes, count array size 16
# Base 256: 4 passes, count array size 256

LSD Radix Sort must complete all d passes regardless of data distribution. MSD Radix Sort can terminate early when buckets contain uniform values, providing better average-case performance on certain datasets. However, MSD requires more complex bookkeeping and performs poorly on random data.

The stability overhead in Radix Sort comes from processing elements in reverse order during each counting sort pass. This adds complexity but ensures correctness. Non-stable radix sort implementations exist but lose the ability to sort composite objects correctly.

Memory access patterns significantly affect real-world performance. Counting Sort and Radix Sort perform sequential writes to the output array, providing good spatial locality. However, reading from the count array exhibits poor temporal locality when k or b is large, causing cache misses.

# Demonstrating cache effects with different ranges
def measure_cache_impact
  size = 1_000_000
  
  # Small range - fits in cache
  small_range = Array.new(size) { rand(100) }
  t1 = Time.now
  counting_sort(small_range)
  small_time = Time.now - t1
  
  # Large range - exceeds cache
  large_range = Array.new(size) { rand(100_000) }
  t2 = Time.now
  counting_sort(large_range)
  large_time = Time.now - t2
  
  puts "Small range (100): #{small_time}s"
  puts "Large range (100k): #{large_time}s"
  puts "Slowdown factor: #{(large_time / small_time).round(2)}x"
end

Common Pitfalls

Forgetting to handle negative numbers breaks Counting Sort. The algorithm assumes all indices are non-negative, causing errors when negative values appear in the input. Always determine the minimum value and shift the range accordingly.

# INCORRECT - fails with negative numbers
def counting_sort_broken(array)
  max_val = array.max
  counts = Array.new(max_val + 1, 0)
  array.each { |num| counts[num] += 1 }  # Error if num < 0
  # ...
end

# CORRECT - handles negative numbers
def counting_sort_correct(array)
  min_val = array.min
  max_val = array.max
  range = max_val - min_val + 1
  counts = Array.new(range, 0)
  array.each { |num| counts[num - min_val] += 1 }
  # ...
end

data = [5, -2, 3, -2, 0]
counting_sort_correct(data)
# => [-2, -2, 0, 3, 5]

Using forward iteration during the placement phase destroys stability. Elements with equal keys must maintain their relative order, which requires processing the input array in reverse.

# UNSTABLE - forward iteration
def counting_sort_unstable(array)
  # ... counting and prefix sum ...
  
  output = Array.new(array.length)
  array.each_with_index do |num, i|  # Forward iteration
    index = num - min_val
    output[counts[index] - 1] = num
    counts[index] -= 1
  end
  output
end

# STABLE - reverse iteration
def counting_sort_stable(array)
  # ... counting and prefix sum ...
  
  output = Array.new(array.length)
  (array.length - 1).downto(0) do |i|  # Reverse iteration
    index = array[i] - min_val
    output[counts[index] - 1] = array[i]
    counts[index] -= 1
  end
  output
end

Allocating excessive memory for the count array wastes resources. When sorting sparse data with a large range, most count array entries remain zero. Alternative data structures like hash tables can store only non-zero counts.

# Memory-efficient for sparse data
def counting_sort_sparse(array)
  return [] if array.empty?
  
  min_val = array.min
  counts = Hash.new(0)
  
  array.each { |num| counts[num] += 1 }
  
  sorted = []
  (min_val..array.max).each do |num|
    counts[num].times { sorted << num }
  end
  
  sorted
end

# Works efficiently even with large range
sparse_data = [1, 1000000, 2, 999999]
counting_sort_sparse(sparse_data)
# => [1, 2, 999999, 1000000]

Integer overflow occurs in Radix Sort when computing positions or extracting digits. Ruby handles arbitrary precision integers automatically, but conversion to floats or improper scaling can introduce errors.

# Potential overflow with float conversion
def radix_sort_risky(array)
  max_val = array.max.to_f  # Float loses precision for large integers
  # ...
end

# Safe with integer arithmetic
def radix_sort_safe(array)
  max_val = array.max  # Stays as arbitrary precision integer
  # ...
end

Choosing an inappropriate base for Radix Sort affects performance. Base 2 works for all integers but requires many passes. Very large bases (like 2¹⁶) reduce passes but create large count arrays that exceed cache size.

Off-by-one errors in digit extraction frequently cause incorrect results. The expression (num / exp) % base extracts a specific digit, but incorrect exponent calculation leads to wrong digits.

# Testing digit extraction
def test_digit_extraction(num, base)
  digits = []
  exp = 1
  while num / exp > 0
    digit = (num / exp) % base
    digits << digit
    exp *= base
  end
  digits
end

test_digit_extraction(12345, 10)
# => [5, 4, 3, 2, 1]  # Correct: least to most significant

Failing to preserve object references during sorting breaks when sorting arrays of mutable objects. The algorithm must move the actual objects, not copy their values.

# Demonstrates reference preservation
Person = Struct.new(:name, :score)
people = [
  Person.new("Alice", 85),
  Person.new("Bob", 92),
  Person.new("Charlie", 85)
]

sorted = counting_sort_by(people) { |p| p.score }

# Original objects preserved
sorted[0].name = "Modified"
puts people.find { |p| p.name == "Modified" }.score
# => 85 (same object reference)

Reference

Counting Sort Characteristics

Property Value Notes
Time Complexity O(n + k) k is the range of values
Space Complexity O(n + k) Auxiliary arrays needed
Stability Yes When implemented with reverse iteration
In-place No Requires output array
Comparison-based No Uses arithmetic operations
Best Case O(n + k) Same for all cases
Average Case O(n + k) Same for all cases
Worst Case O(n + k) Same for all cases
Optimal When k = O(n) Range proportional to size

Radix Sort Characteristics

Property Value Notes
Time Complexity O(d * (n + b)) d digits, base b
Space Complexity O(n + b) Per-pass auxiliary space
Stability Yes Depends on stable subroutine
In-place No Requires output array
Comparison-based No Processes digits arithmetically
Best Case O(d * (n + b)) MSD can terminate early
Average Case O(d * (n + b)) LSD processes all digits
Worst Case O(d * (n + b)) Same for LSD
Optimal When d and b are small Constant compared to n

Algorithm Selection Guide

Scenario Algorithm Rationale
Small integer range Counting Sort Direct O(n + k) with small k
Large integers Radix Sort O(d * n) with small d
Floating-point numbers Bucket + Radix Partition then sort buckets
Negative numbers Modified Counting Shift range by minimum
Stability required Either Both stable when implemented correctly
Memory constrained Quicksort/Heapsort O(n log n) in-place
Unknown distribution Timsort/Mergesort Adaptive O(n log n)

Base Selection for Radix Sort

Base Passes for 32-bit Count Array Size Use Case
2 32 2 Minimal space
8 11 8 Moderate balance
10 10 10 Decimal debugging
16 8 16 Hexadecimal data
256 4 256 Byte-aligned processing

Common Operations

Operation Counting Sort Radix Sort
Find min/max O(n) scan O(n) scan for max
Count elements O(n) single pass Not needed
Compute positions O(k) prefix sum O(b) per pass
Place elements O(n) reverse iteration O(n) per pass
Total passes 1 d passes

Space Requirements

Component Counting Sort Radix Sort LSD Radix Sort MSD
Input array n n n
Output array n n n
Count array k b b
Recursion stack 0 0 O(d)
Total O(n + k) O(n + b) O(n + b + d)

Performance Thresholds

Condition Counting Sort Radix Sort Comparison Sort
k < n Excellent Good Slower
k = n Good Good Competitive
k = n log n Competitive Good Competitive
k > n log n Poor Depends on d Better
Sparse data Hash-based Standard Better