Precondition β keys must be non-negative integers in a bounded range[0,M] that index an array in O(1); arbitrary orderable objects do not qualify.
2. The Four Phases
Phase 1 β find max β one linear scan gives M=max(input) βΉ Ξ(N); you cannot allocate before you know the range.
Phase 2 β allocate count β zero-filled array of length M+1, position v holds freq(v) βΉ Ξ(M).
Phase 3 β tally β count[item] += 1 βΉ Ξ(N), because array access is O(1); that O(1) is the whole trick.
Phase 4 β rebuild β walk count leftβright emitting value v exactly count[v] times βΉ Ξ(N+M) β M cells visited, N items written.
3. Stability Is Not Free
Naive version is UNSTABLE β storing only a frequency discards item identity; emitted items are freshly manufactured copies of the key, so payloads attached to equal keys are lost/reordered.
Fix A β chained buckets β store the items in a list per slot (separate-chaining shape), appending in input order βΉ stable.
Space of Fix A is Ξ(M+N), NOT Ξ(Mβ N) β the buckets partition the input, so all buckets together hold exactly N items β slots and payloads add, never multiply.
Fix B β position (prefix-sum) array β turn counts into starting offsets, then place items in a single input-order pass βΉ stable with flat Ξ(M+N) arrays β Β§4.
Which variant does the question want? β unstated βΉ buckets (simpler, and correct across Radix Sortβs repeated passes); a question naming a count array and a position array is asking for Fix B.
Bucket drain must be O(1) per item β concatenate with extend() (amortised O(1)), never pop(0), which shifts the whole list and turns the rebuild into Ξ(N2); a circular queue/deque restores O(1) front removal.
4. Stable Counting Sort via a Position Array
Build count β count[key] += 1 over the input.
Build position β position[first] = 1, then position[i]=position[iβ1]+count[iβ1] β a prefix sum: each key learns where its block starts.
Construct output β scan the input in order; write output[position[key]] = (key,val) then position[key] += 1 βΉ earlier-arriving equal keys land in earlier slots βΉ stable.
Cost unchanged β still Ξ(N+M) time and space β stability costs an extra array, not an extra factor.
5. Negative and Non-Zero-Based Keys
The count array is indexed from 0 β a negative key has no slot, so raw count[key] fails before it sorts.
Offset mapping β scan for min as well as max, store every key at index keyβmin βΉ every index valid (β322β0, 420β742), range M=maxβmin+1. Undo the shift on rebuild β emit index+min, or you return a correctly ordered list of the wrong values.
The bound is driven by the RANGE, not the maximum β a tight cluster of huge values (106β¦106+5) is cheap; {0,106} is ruinous.
βοΈ Core Implementation
πΉ Basic counting sort (unstable, keys only)
counting_sort β raw index manipulation, no library calls
def counting_sort(my_list): if len(my_list) == 0: return my_list # Phase 1: find the maximum -- O(N) maximum = my_list[0] for i in range(1, len(my_list)): if my_list[i] > maximum: maximum = my_list[i] # Phase 2: allocate the count array -- O(M) count = [0] * (maximum + 1) # Phase 3: tally -- O(N), O(1) per item because the key IS the index for i in range(len(my_list)): count[my_list[i]] += 1 # Phase 4: rebuild in place -- O(N + M) write = 0 for value in range(len(count)): for _ in range(count[value]): my_list[write] = value write += 1 return my_list
π‘ Common Mistake:Phase 4 looks like a nested loop but is not Ξ(NM) β the inner loop runs count[v] times and βvβcount[v]=N, so the two loops together cost Ξ(N+M).
stable_counting_sort β prefix-sum offsets, 1-indexed as in the lecture
def stable_counting_sort(pairs, max_key): # pairs = [(key, val), ...]; keys in 1..max_key count = [0] * (max_key + 1) for i in range(len(pairs)): count[pairs[i][0]] += 1 # prefix sum -> starting slot of each key's block position = [0] * (max_key + 1) position[1] = 1 for k in range(2, max_key + 1): position[k] = position[k - 1] + count[k - 1] # place in INPUT order -> equal keys keep relative order output = [None] * (len(pairs) + 1) # slot 0 unused for i in range(len(pairs)): key = pairs[i][0] output[position[key]] = pairs[i] position[key] += 1 return output[1:]
π‘ Common Mistake:Scanning the input backwards, or forgetting position[key] += 1 β either overwrites the blockβs first slot repeatedly or reverses equal keys β and an unstable subsort silently destroys Radix Sort.
When It Flips: counting sort wins while MβͺNlogN. Sorting 106 lowercase letters (M=26) is Ξ(N); sorting 7 numbers where one is 981 costs Ξ(N+981) β worse than Merge Sort. 32-bit keys give M=232 β unusable for any realistic N. Once M dominates, switch to a comparison sort or decompose the key into digits β Radix Sort.
Final Extracted Output: here Mβ«N so counting sort loses badly β the fix is to sort digit-by-digit with M=10 β Radix Sort.
β οΈ Common Mistakes
π‘ Quoting Ξ(N) unconditionally β the bound is Ξ(N+M); M may only be dropped after you state the key range is capped (alphabet M=26, digits M=10).
π‘ Claiming bucket space is Ξ(Nβ M) β lecturer-flagged as the very common misconception: buckets partition the input βΉ Ξ(M+N).
π‘ Assuming counting sort is stable by default β it is not; stability must be engineered, and Radix Sort silently breaks without it.
π‘ Draining buckets with pop(0) β O(n) per removal βΉ the rebuild becomes Ξ(N2) and the linear claim collapses.
π‘ Carrying the max() scan into Radix Sort β counting sort needs max to size the count array; radix sizes it from the base and needs max only for the column count. Lecturer-flagged as a recurring code-review error.