Loading learning content...
With the limited range property established, we now turn to how counting sort exploits this constraint. The answer lies in a deceptively simple idea: instead of rearranging elements directly, we count how many elements have each value.
This count—stored in an auxiliary array—contains everything we need to reconstruct the sorted output. The counting phase is where the magic happens, transforming an unsortable mess into structured information from which order emerges.
If you know there are exactly 3 zeros, 5 ones, and 2 twos, you know the sorted output without computing it: [0, 0, 0, 1, 1, 1, 1, 1, 2, 2]. The counts encode the result. Counting sort is, at its core, this observation made algorithmic.
The count array (also called the histogram or frequency array) is an auxiliary data structure of size k, where k is the range of input values. Each index i in the count array holds the number of elements in the input that have value i.
Definition: Given input array A of n elements with values in [0, k-1], the count array C of size k satisfies:
C[i] = |{j : A[j] = i}|
In words: C[i] equals the count of elements in A that equal i.
1234567891011121314151617181920212223242526272829303132333435
/** * Build a count array from input values. * * Example: * Input: [2, 0, 2, 3, 1, 0, 2] * Range: 0 to 3 (k = 4) * * Count Array: * Index 0: count of 0s → 2 * Index 1: count of 1s → 1 * Index 2: count of 2s → 3 * Index 3: count of 3s → 1 * * C = [2, 1, 3, 1] */function buildCountArray(arr: number[], k: number): number[] { // Initialize count array with zeros const count = new Array(k).fill(0); // Single pass through input: O(n) for (const value of arr) { count[value]++; // Direct addressing: value IS the index } return count;} // Example executionconst input = [2, 0, 2, 3, 1, 0, 2];const k = 4; // Values range from 0 to 3const counts = buildCountArray(input, k);console.log(counts); // [2, 1, 3, 1] // Verify: sum of counts equals array lengthconsole.log(counts.reduce((a, b) => a + b)); // 7 (correct!)Notice count[value]++. The value itself becomes the index. This is direct addressing—no comparison, no search, just immediate access. This single line is why counting sort bypasses the comparison-based lower bound.
Let's trace through the counting process step by step to build intuition.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
Input Array: [2, 0, 2, 3, 1, 0, 2]Range k = 4 (values 0, 1, 2, 3) Initial Count Array: [0, 0, 0, 0] ↑ ↑ ↑ ↑ 0 1 2 3 Step 1: Process A[0] = 2 count[2]++ Count: [0, 0, 1, 0] ← One 2 seen Step 2: Process A[1] = 0 count[0]++ Count: [1, 0, 1, 0] ← One 0 seen Step 3: Process A[2] = 2 count[2]++ Count: [1, 0, 2, 0] ← Two 2s seen Step 4: Process A[3] = 3 count[3]++ Count: [1, 0, 2, 1] ← One 3 seen Step 5: Process A[4] = 1 count[1]++ Count: [1, 1, 2, 1] ← One 1 seen Step 6: Process A[5] = 0 count[0]++ Count: [2, 1, 2, 1] ← Two 0s seen Step 7: Process A[6] = 2 count[2]++ Count: [2, 1, 3, 1] ← Three 2s seen Final Count Array: [2, 1, 3, 1] Interpretation: • 2 elements have value 0 • 1 element has value 1 • 3 elements have value 2 • 1 element has value 3 Sorted output (by reading counts): [0, 0, 1, 2, 2, 2, 3]After a single O(n) pass, we know exactly how many of each value exist. This information is sufficient to produce sorted output—we've essentially 'sorted' the data by categorizing it.
The most straightforward counting sort directly reconstructs the sorted output from the count array. This version works perfectly for sorting bare integers but is not stable—it doesn't preserve the relative order of equal elements.
12345678910111213141516171819202122232425262728293031323334353637
/** * Simple Counting Sort: Sort integers using count-based reconstruction. * * Algorithm: * 1. Build count array: count[i] = number of elements equal to i * 2. Reconstruct: output count[i] copies of value i, for each i * * Time: O(n + k) * Space: O(k) for count array + O(n) for output = O(n + k) * Stability: NOT STABLE */function simpleCountingSort(arr: number[], k: number): number[] { if (arr.length === 0) return []; // Phase 1: Build count array - O(k) init + O(n) counting const count = new Array(k).fill(0); for (const value of arr) { count[value]++; } // Phase 2: Reconstruct sorted output - O(n + k) const output: number[] = []; for (let value = 0; value < k; value++) { // Add 'count[value]' copies of 'value' to output for (let i = 0; i < count[value]; i++) { output.push(value); } } return output;} // Example usageconst unsorted = [4, 2, 2, 8, 3, 3, 1];const k = 9; // Values range 0-8const sorted = simpleCountingSort(unsorted, k);console.log(sorted); // [1, 2, 2, 3, 3, 4, 8]This version generates new values based on counts—it doesn't move original elements. If the input contained objects like {value: 2, id: 'A'} and {value: 2, id: 'B'}, we couldn't reconstruct them from just knowing 'there are 2 twos'. Stability requires a different approach.
To achieve stability (and sort objects with associated data), we need to know not just how many elements have each value, but where elements of each value should be placed in the output. This requires transforming counts into cumulative counts (also called prefix sums).
Key Insight: If count[i] represents how many elements have value i, then the cumulative count tells us: 'After placing all elements with values < i, the next available position is...'
Formally, cumulative count C'[i] = sum of all C[j] for j < i.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Transform count array into cumulative count array. * * After transformation: * C[i] = number of elements with value < i * = starting position for elements with value i * * Example: * Count: [2, 1, 3, 1] (2 zeros, 1 one, 3 twos, 1 three) * * Cumulative: [0, 2, 3, 6] * ↑ ↑ ↑ ↑ * │ │ │ └─ 3s start at index 6 (after 2+1+3=6 elements) * │ │ └──── 2s start at index 3 (after 2+1=3 elements) * │ └─────── 1s start at index 2 (after 2 elements) * └────────── 0s start at index 0 (first) */function computeCumulativeCounts(count: number[]): number[] { const cumulative = new Array(count.length); cumulative[0] = 0; // First value starts at index 0 for (let i = 1; i < count.length; i++) { // Position for value i = position for value (i-1) + count of (i-1) cumulative[i] = cumulative[i - 1] + count[i - 1]; } return cumulative;} // Alternative: Cumulative sum that includes current value// C[i] = number of elements with value ≤ i// This is useful for right-to-left traversal in stable sortfunction computeCumulativeCountsInclusive(count: number[]): number[] { const cumulative = [...count]; // Copy original counts for (let i = 1; i < cumulative.length; i++) { cumulative[i] += cumulative[i - 1]; } return cumulative;} // Exampleconst count = [2, 1, 3, 1];console.log(computeCumulativeCounts(count)); // [0, 2, 3, 6]console.log(computeCumulativeCountsInclusive(count)); // [2, 3, 6, 7]| Array Type | C[i] Represents | Use Case |
|---|---|---|
| Raw Count | Number of elements equal to i | Simple non-stable sort |
| Cumulative (Exclusive) | Start position for elements with value i | Stable sort (left-to-right) |
| Cumulative (Inclusive) | End position after placing all elements ≤ i | Stable sort (right-to-left) |
The stable version of counting sort uses cumulative counts to place each original element in its correct position, preserving relative order among equal elements. This is the standard counting sort used as a subroutine in radix sort.
The algorithm processes the input right-to-left, and for each element:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
/** * Stable Counting Sort: Preserves relative order of equal elements. * * This is the standard counting sort algorithm, essential for: * - Radix sort (sorting by successive digits) * - Sorting objects by a key field * - Any situation where duplicate handling matters * * Time: O(n + k) * Space: O(n + k) * Stability: STABLE */function stableCountingSort(arr: number[], k: number): number[] { const n = arr.length; if (n === 0) return []; // Phase 1: Build count array - O(n) const count = new Array(k).fill(0); for (const value of arr) { count[value]++; } // Phase 2: Transform to cumulative counts (inclusive) - O(k) // After this, count[i] = number of elements ≤ i for (let i = 1; i < k; i++) { count[i] += count[i - 1]; } // Phase 3: Place elements in output array - O(n) // Process RIGHT-TO-LEFT for stability const output = new Array(n); for (let i = n - 1; i >= 0; i--) { const value = arr[i]; // count[value] gives position AFTER the last element with this value // Decrement first to get 0-indexed position count[value]--; // Place element at its correct sorted position output[count[value]] = value; } return output;} // Example demonstrating stabilityinterface Item { value: number; id: string; } function stableCountingSortObjects( arr: Item[], k: number): Item[] { const n = arr.length; if (n === 0) return []; const count = new Array(k).fill(0); for (const item of arr) { count[item.value]++; } for (let i = 1; i < k; i++) { count[i] += count[i - 1]; } const output = new Array(n); for (let i = n - 1; i >= 0; i--) { const item = arr[i]; count[item.value]--; output[count[item.value]] = item; } return output;} // Stability testconst items: Item[] = [ { value: 2, id: 'A' }, { value: 1, id: 'B' }, { value: 2, id: 'C' }, { value: 1, id: 'D' }]; const sortedItems = stableCountingSortObjects(items, 3);console.log(sortedItems);// [{ value: 1, id: 'B' }, { value: 1, id: 'D' },// { value: 2, id: 'A' }, { value: 2, id: 'C' }]// Note: B before D, A before C - original order preserved!Processing right-to-left with inclusive cumulative counts places equal elements in reverse order of encounter, which restores their original left-to-right order. Processing left-to-right would reverse it. This subtle detail is critical for stability.
Let's trace through the stable counting sort step by step to understand how cumulative counts and right-to-left processing achieve stability.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
Input: A = [2, 0, 2, 1, 0]Range k = 3 (values 0, 1, 2)n = 5 ════════════════════════════════════════════════════════════PHASE 1: Build Count Array════════════════════════════════════════════════════════════ count = [0, 0, 0] (initialized) Process each element: A[0] = 2: count[2]++ → count = [0, 0, 1] A[1] = 0: count[0]++ → count = [1, 0, 1] A[2] = 2: count[2]++ → count = [1, 0, 2] A[3] = 1: count[1]++ → count = [1, 1, 2] A[4] = 0: count[0]++ → count = [2, 1, 2] Final count: [2, 1, 2] → 2 zeros, 1 one, 2 twos ════════════════════════════════════════════════════════════PHASE 2: Cumulative Sum (Inclusive)════════════════════════════════════════════════════════════ cumulative[0] = count[0] = 2cumulative[1] = cumulative[0] + count[1] = 2 + 1 = 3cumulative[2] = cumulative[1] + count[2] = 3 + 2 = 5 Cumulative count: [2, 3, 5] Interpretation: count[0] = 2: There are 2 elements with value ≤ 0 So 0s fill positions 0-1 (indices 0 and 1) count[1] = 3: There are 3 elements with value ≤ 1 So 1s fill position 2 count[2] = 5: There are 5 elements with value ≤ 2 So 2s fill positions 3-4 ════════════════════════════════════════════════════════════PHASE 3: Place Elements (Right-to-Left)════════════════════════════════════════════════════════════ output = [_, _, _, _, _]count = [2, 3, 5] Step 1: i = 4, A[4] = 0 count[0]-- → count[0] = 1 output[1] = 0 output = [_, 0, _, _, _] count = [1, 3, 5] Step 2: i = 3, A[3] = 1 count[1]-- → count[1] = 2 output[2] = 1 output = [_, 0, 1, _, _] count = [1, 2, 5] Step 3: i = 2, A[2] = 2 count[2]-- → count[2] = 4 output[4] = 2 output = [_, 0, 1, _, 2] count = [1, 2, 4] Step 4: i = 1, A[1] = 0 count[0]-- → count[0] = 0 output[0] = 0 output = [0, 0, 1, _, 2] count = [0, 2, 4] Step 5: i = 0, A[0] = 2 count[2]-- → count[2] = 3 output[3] = 2 output = [0, 0, 1, 2, 2] count = [0, 2, 3] ═══════════════════════════════════════════════════════════FINAL OUTPUT: [0, 0, 1, 2, 2] ✓═══════════════════════════════════════════════════════════ Stability Verification: Original 2s: A[0]=2 was at index 0, A[2]=2 was at index 2 Sorted 2s: First 2 at index 3, Second 2 at index 4 A[0] (first 2 in input) → output[3] (first 2 in output) ✓ A[2] (second 2 in input) → output[4] (second 2 in output) ✓ Order preserved! That's stability.Stability might seem like a minor technical detail, but it has profound practical implications. Without stability, counting sort couldn't serve as the building block for radix sort, one of the most powerful sorting algorithms for fixed-width integers and strings.
Radix sort sorts integers digit-by-digit, least significant first. After sorting by the ones digit, tens digit sorting must not disturb the ones ordering within equal tens values. Only a stable sort preserves this. Using an unstable counting sort makes radix sort produce wrong results.
Standard counting sort requires O(n) auxiliary space for the output array. Can we sort in-place? The answer is nuanced:
For bare integers (simple version): Yes, we can write directly back to the input array:
function inPlaceSimpleCountingSort(arr: number[], k: number): void {
const count = new Array(k).fill(0);
for (const v of arr) count[v]++;
let idx = 0;
for (let v = 0; v < k; v++) {
for (let i = 0; i < count[v]; i++) {
arr[idx++] = v;
}
}
}
For stable sorting: There is no simple in-place stable counting sort. The output array is necessary to place elements while reading from the input. Some complex cycle-sort-based approaches exist but are impractical.
Practical reality: The O(n) output space is rarely a problem. If sorting n elements, you already have O(n) input space. Doubling memory for sorting is acceptable in most contexts.
| Variant | Count Array | Output Array | Total Space | Stable? |
|---|---|---|---|---|
| Simple Counting Sort | O(k) | O(n) | O(n + k) | No |
| In-Place Simple | O(k) | O(1)* | O(k) | No |
| Stable Counting Sort | O(k) | O(n) | O(n + k) | Yes |
Counting sort's simplicity is deceptive. These bugs appear frequently in implementations:
count[3.5] creates havoc (array with floating-point keys).count[-5] doesn't work as expected in most languages.123456789101112131415161718192021222324252627282930313233343536
function robustCountingSort(arr: number[]): number[] { if (arr.length === 0) return []; // Handle arbitrary integer ranges const min = Math.min(...arr); const max = Math.max(...arr); const k = max - min + 1; // Validate input (optional but recommended) for (const v of arr) { if (!Number.isInteger(v)) { throw new Error(`Non-integer value: ${v}`); } } // Count with offset normalization const count = new Array(k).fill(0); for (const v of arr) { count[v - min]++; // Shift by min } // Cumulative sum for (let i = 1; i < k; i++) { count[i] += count[i - 1]; } // Stable placement (right-to-left) const output = new Array(arr.length); for (let i = arr.length - 1; i >= 0; i--) { const normalizedValue = arr[i] - min; count[normalizedValue]--; output[count[normalizedValue]] = arr[i]; // Denormalize in output } return output;}The counting phase is the heart of counting sort. By tallying occurrences and computing cumulative positions, we transform the sorting problem into a direct addressing problem.
You now understand the counting mechanism that powers counting sort. The count array captures the frequency distribution; cumulative counts encode position information; right-to-left placement ensures stability. Next, we'll analyze the precise time complexity O(n + k) and understand what each term contributes.