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 β for each item, 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; the emitted items are freshly manufactured copies of the key, so the payload attached to equal keys is lost/reordered.
Fix A β chained buckets β store the items in a list per slot (the separate-chaining shape) and append 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 β the M slots and the N 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 a flat Ξ(M+N) array, no lists β Β§4.
Which variant does the question want? β unstated βΉ buckets (simpler, and correct across the repeated passes of Radix Sort); a question naming a count array and a position array is asking for Fix B β that pairing is the non-bucket variantβs signature.
Bucket drain must be O(1) per item β concatenate with extend() (amortised O(1)), never pop(0), which shifts the whole list at O(n) per item and silently turns the rebuild into Ξ(N2); a circular queue / deque restores O(1) front removal if FIFO order must be popped rather than copied.
4. Stable Counting Sort via a Position Array
Build count β count[key] += 1 over the input, as before.
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; for each (key,val) 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, Ξ(N+M) 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 a raw count[key] fails before it sorts.
Offset mapping β scan for min as well as max, then store every key at index keyβmin βΉ every index becomes valid (β322β0, 420β742) and the range is M=maxβmin+1.
Undo the shift on rebuild β emit index+min, never the index itself β forgetting this returns a correctly ordered list of the wrong values.
The bound is driven by the RANGE, not the maximum β Ξ(N+M) with M=maxβmin+1; a tight cluster of huge values (106β¦106+5) is cheap, while {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. Once M dominates, either switch to a comparison sort or decompose the key into digits β Radix Sort.
Final Extracted Output:Ξ(N+M) time, Ξ(M) / Ξ(M+N) auxiliary; 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 that 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, so the total payload is N βΉ Ξ(M+N).
π‘ Assuming counting sort is stable by default β it is not; stability must be engineered (buckets or prefix-sum positions), and Radix Sort silently breaks without it.
π‘ Draining buckets with pop(0) β O(n) per removal from shifting βΉ the rebuild becomes Ξ(N2) and the whole linear claim collapses; use extend() or a circular queue.
π‘ 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 to compute the column count. Lecturer-flagged as a recurring code-review error.
Why does making counting sort stable cost Ξ(M+N) space rather than Ξ(Mβ N)?
Hint: Count the items across all buckets, not per bucket.
Answer
Short answer: The buckets partition the N items β βvβcount[v]=N β so slots and payloads add.
Why:Additive, not multiplicative β M empty slots plus N stored items gives Ξ(M+N); Ξ(Mβ N) would require every bucket to hold all N items.
Your keys are 32-bit integers. Justify, with the bound, why counting sort is the wrong choice and what replaces it.
Hint: Put a number on M.
Answer
Short answer:M=232 βΉ Ξ(N+232) time and space β unusable for any realistic N.
Why:Decompose the key β Radix Sort treats the integer as K digits in base Mβ², paying Ξ(K(N+Mβ²)) with Mβ² small, which is Ξ(N) for fixed-width keys.