Algorithms

Linear Time Sorting

The previous article proved that comparison-based sorting cannot beat O(n log n). But what if we don't use comparisons? This article explores three algorithms that achieve O(n) time by exploiting properties of the data: Counting Sort, Radix Sort, and Bucket Sort.

These algorithms aren't always applicable, but when they are, they can be orders of magnitude faster than QuickSort or MergeSort - especially for large datasets with bounded integer keys.

πŸ“‹ At a Glance

AlgorithmTimeSpaceStableWhen to Use
Counting SortO(n + k)O(k)YesSmall range integers
Radix SortO(d Γ— (n + k))O(n + k)YesFixed-width integers/strings
Bucket SortO(n + k)O(n + k)YesUniformly distributed data

Where k = range of values, d = number of digits


🎯 What You'll Learn

After reading this article, you will be able to:

  • Implement Counting Sort for integers with known range
  • Apply Radix Sort to sort millions of integers faster than QuickSort
  • Use Bucket Sort for uniformly distributed floating-point numbers
  • Know when O(n) sorts beat O(n log n) in practice
  • Combine these algorithms with comparison sorts for hybrid approaches
  • Avoid common pitfalls like negative numbers and memory overhead

πŸ”₯ Production Story: 100 Million Log Entries

A monitoring system needed to sort 100 million log entries by timestamp for daily analysis. The timestamp was a Unix epoch in milliseconds, ranging from midnight to midnight (86,400,000 values).

JAVA(5 lines)
Code
Loading syntax highlighter...
Analysis revealed:
  • Timestamps were bounded (one day = 86.4M milliseconds)
  • Timestamps were dense (many entries per millisecond)
  • MergeSort was doing ~1.7 billion comparisons
The optimization:
JAVA(25 lines)
Code
Loading syntax highlighter...
Further optimization with Radix Sort:
JAVA(8 lines)
Code
Loading syntax highlighter...
The lesson: When data has bounded, integer keys, linear sorts can be dramatically faster than comparison sorts.

🧠 Mental Model: Trading Assumptions for Speed

Linear time sorts work by avoiding comparisons entirely. Instead, they use direct addressing or arithmetic:

TEXT(27 lines)
Code
Loading syntax highlighter...
When to use linear sorts:
TEXT(6 lines)
Code
Loading syntax highlighter...

πŸ”¬ Deep Dive

1. Counting Sort: The Foundation

Counting Sort works by counting occurrences of each value, then using those counts to place elements directly.

JAVA(21 lines)
Code
Loading syntax highlighter...
Stable Counting Sort (for objects or as Radix Sort subroutine):
JAVA(27 lines)
Code
Loading syntax highlighter...
Why traverse backwards for stability?
TEXT(10 lines)
Code
Loading syntax highlighter...
Handling negative numbers:
JAVA(21 lines)
Code
Loading syntax highlighter...

2. Radix Sort: Sorting by Digits

Radix Sort sorts numbers digit by digit, using a stable sort (usually Counting Sort) for each digit.

LSD (Least Significant Digit) Radix Sort:
JAVA(38 lines)
Code
Loading syntax highlighter...
Why LSD works:
TEXT(15 lines)
Code
Loading syntax highlighter...
Handling negative numbers with Radix Sort:
JAVA(27 lines)
Code
Loading syntax highlighter...
Base-256 (byte) Radix Sort for maximum efficiency:
JAVA(31 lines)
Code
Loading syntax highlighter...

3. Bucket Sort: For Uniform Distributions

Bucket Sort works by distributing elements into buckets, sorting each bucket, and concatenating.

JAVA(36 lines)
Code
Loading syntax highlighter...
Why uniform distribution matters:
TEXT(9 lines)
Code
Loading syntax highlighter...
Bucket Sort for integers with known range:
JAVA(29 lines)
Code
Loading syntax highlighter...

4. When to Use Which?

Decision framework:
JAVA(28 lines)
Code
Loading syntax highlighter...

⚠️ Common Mistakes

Mistake 1: Forgetting About Negative Numbers

JAVA(21 lines)
Code
Loading syntax highlighter...

Mistake 2: Using Radix Sort When Range Is Huge

JAVA(6 lines)
Code
Loading syntax highlighter...

Mistake 3: Unstable Counting Sort Breaking Radix Sort

JAVA(17 lines)
Code
Loading syntax highlighter...

πŸ› Debug This

The following Radix Sort produces wrong results. Find the bug:

JAVA(36 lines)
Code
Loading syntax highlighter...
Click to reveal the bug and solution
The Bug:

Two issues:

  1. Loop condition: exp <= max should be max / exp > 0
    • For max=170, exp=1000 is included but shouldn't be
  2. Not traversing backwards: The output loop goes forwards, breaking stability
Trace with forward traversal:
TEXT(5 lines)
Code
Loading syntax highlighter...
The Fix:
JAVA(31 lines)
Code
Loading syntax highlighter...

πŸ’» Exercises

Exercise 1: Sort Strings by Length ⭐

Use Counting Sort to sort strings by their length:

JAVA(4 lines)
Code
Loading syntax highlighter...
Solution
JAVA(28 lines)
Code
Loading syntax highlighter...

Exercise 2: MSD Radix Sort ⭐⭐

Implement Most Significant Digit (MSD) Radix Sort:

JAVA(4 lines)
Code
Loading syntax highlighter...
Solution
JAVA(39 lines)
Code
Loading syntax highlighter...

MSD is useful for variable-length strings (can stop early) but more complex than LSD.


Exercise 3: Sort IP Addresses ⭐⭐

Sort IP addresses (as strings) efficiently:

JAVA(5 lines)
Code
Loading syntax highlighter...
Solution
JAVA(55 lines)
Code
Loading syntax highlighter...

Exercise 4: Hybrid Sort ⭐⭐⭐

Implement a hybrid sort that chooses the best algorithm:

JAVA(3 lines)
Code
Loading syntax highlighter...
Solution
JAVA(56 lines)
Code
Loading syntax highlighter...

Exercise 5: External Radix Sort ⭐⭐⭐⭐

Implement Radix Sort for data too large to fit in memory:

JAVA(4 lines)
Code
Loading syntax highlighter...
Solution
JAVA(68 lines)
Code
Loading syntax highlighter...

🎀 Interview Questions

Question #1: When would you use Counting Sort over QuickSort?

What interviewers want to hear: Understanding of trade-offs and practical constraints.
Strong answer: Use Counting Sort when:
  1. Keys are integers in a known, small range [0, k]
  2. k ≀ n log n: If range k exceeds n log n, QuickSort is faster
    • Example: n=1M, k=20M β†’ Counting Sort
    • Example: n=1M, k=1B β†’ QuickSort
  3. Stability is needed: Counting Sort is naturally stable
  4. Memory is available: Needs O(k) extra space
Real example:
  • Sorting grades (0-100) for 10,000 students β†’ Counting Sort (k=100, n=10000)
  • Sorting salaries ($0-$10M) for 1000 employees β†’ QuickSort (k=10M >> n log n)

Question #2: Why does Radix Sort use LSD (Least Significant Digit) order?

What interviewers want to hear: Understanding of stability requirements.
Strong answer: LSD works because each pass uses a stable sort, preserving relative order from previous passes.
Why it works:
  • After sorting by ones digit: numbers ending in 0 come first, etc.
  • After sorting by tens digit: stability ensures numbers with same tens digit remain sorted by ones
Why MSD is different:
  • MSD must recursively sort each bucket separately
  • Can't rely on a global stable sort because higher digits are more significant
  • More complex but can short-circuit for variable-length strings
Example with LSD:
[170, 45, 75, 90]
After ones:  [170, 90, 45, 75]  (0s, then 5s)
After tens:  [45, 70, 75, 90]   (4,7,7,9 - stable keeps 70<75)
After hundreds: [45, 75, 90, 170]

Question #3: How would you sort 1 billion 32-bit integers with only 1GB RAM?

What interviewers want to hear: External sorting knowledge and practical problem-solving.
Strong answer: 1 billion integers = 4GB, exceeds 1GB RAM. Strategy:
Approach 1: External Merge Sort
  • Split into ~8 chunks of 125M integers each
  • Sort each chunk in memory with QuickSort
  • K-way merge the sorted chunks
Approach 2: External Radix Sort
  • 4 passes (one per byte)
  • Each pass: distribute to 256 buckets (files)
  • Concatenate buckets
Approach 3: Counting Sort variant (if range is small)
  • If range is <1B distinct values, use streaming counting
  • First pass: count occurrences (bitmap or count array)
  • Second pass: output values in order
Best choice: External Radix Sort
  • Predictable I/O pattern
  • Only 4 passes over the data
  • ~10 minutes for 1B integers on SSD

Question #4: Explain why Bucket Sort degrades to O(nΒ²) with non-uniform distribution.

What interviewers want to hear: Understanding of algorithm assumptions.
Strong answer: Bucket Sort distributes n elements into k buckets, then sorts each bucket.
Analysis:
  • If uniform: each bucket has O(n/k) elements
  • Sorting each bucket: O((n/k)Β²) with insertion sort
  • Total: k Γ— O((n/k)Β²) = O(nΒ²/k)
  • With k=n: O(n)
Non-uniform case:
  • All elements in one bucket: O(nΒ²)
  • Example: [0.001, 0.002, 0.003, ...] all go to bucket 0
Mitigations:
  1. Use comparison sort for buckets (O(n/k log n/k))
  2. Adaptive bucket sizing based on samples
  3. Hybrid: switch to QuickSort if bucket too large

Question #5: How would you sort strings alphabetically using Radix Sort?

What interviewers want to hear: Extension of Radix Sort concepts to strings.
Strong answer: Two approaches:
MSD Radix Sort (most common for strings):
1. Sort by first character (256-way counting sort)
2. Recursively sort each bucket by remaining characters
3. Stop when single character or empty

Optimization: Switch to insertion sort for small buckets
LSD with padding:
1. Pad all strings to max length
2. Sort from rightmost character to leftmost
3. Use stable counting sort for each position

Problem: Wastes space/time with variable lengths
Comparison:
AspectMSDLSD
Variable lengthNaturalNeeds padding
Early terminationYesNo
ImplementationComplex (recursive)Simple
Cache behaviorPoorGood
Real implementation: 3-way String QuickSort
  • Partition by first character
  • Recursively sort substrings
  • Combines QuickSort efficiency with string structure

πŸ“‹ Quick Reference

Algorithm Selection

ConditionBest Algorithm
k ≀ nCounting Sort
k ≀ n log nCounting Sort
k > n log n, bounded integersRadix Sort
Uniform distribution floatsBucket Sort
General/unboundedQuickSort/TimSort

Complexity Summary

AlgorithmTimeSpaceStableRequirements
CountingO(n+k)O(k)YesKeys in [0,k]
Radix (LSD)O(d(n+k))O(n+k)Yesd-digit keys
Radix (MSD)O(nΓ—len)O(n+k)YesVariable-length
BucketO(n+k)O(n+k)YesUniform dist

Code Templates

JAVA(13 lines)
Code
Loading syntax highlighter...

πŸ“ Summary & Key Takeaways

Core Principles

  1. Linear sorts bypass Ξ©(n log n) by not using comparisons
  2. Counting Sort: O(n+k) when k (range) is small
  3. Radix Sort: O(d(n+k)) for fixed-width integers
  4. Bucket Sort: O(n) for uniformly distributed data
  5. Trade-off: speed vs assumptions - linear sorts have requirements

What You Can Do Now

  • Implement all three linear sorting algorithms
  • Choose the right algorithm based on data characteristics
  • Handle edge cases (negatives, large ranges)
  • Combine linear sorts with comparison sorts for hybrid approaches
  • Analyze when O(n) sorts beat O(n log n) in practice

πŸ“… Review Schedule

DayTaskTime
Day 1Review when to use each algorithm5 min
Day 3Implement Counting Sort from memory10 min
Day 7Implement Radix Sort from memory15 min
Day 14Redo Debug This exercise10 min
Day 30Solve the hybrid sort exercise20 min

Next in series: [Part 6: Sorting in Practice - Stability, External Sorting & Java's Arrays.sort()]