Context:FIT1008_MOC, FIT2004_MOC · a Computational Problem · backbone clustering the three elementary O(n2) sorts (Bubble/Selection/Insertion) + sort properties Stability & Incrementality · recursive sorts (Merge Sort, Quick Sort, Heapsort) referenced
FIT2004 emphasis: every sort is now graded on four axes — correctness (Invariant), time, auxiliary space, stability — and the suite splits into comparison-based (floored at Ω(NlogN)) vs non-comparison (Counting Sort, Radix Sort, which break the floor). W3 adds the engineering layer: how to force stability onto an unstable sort, and what sorting is actually for (§9).
Quick Revision
🎯 Objective: rearrange n orderable elements → non-decreasing ➔ correctness = permutation + ordering.
⚡ Key Constraint: all three elementary sorts are O(n2) worst (arithmetic series ∑k) ➔ discriminators are stability, swap count, adaptivity; the Ω(nlogn) floor binds only algorithms that compare.
📝 Core
1. The Sorting Problem (Spec)
Specification ➔ input norderable elements ➔ output permutation a0′≤⋯≤an−1′.
Correctness ➔ permutation AND ordering (ordered-only misses drop/duplicate bugs).
2. Comparison-Based vs Non-Comparison
Comparison-based ➔ the algorithm’s only access to keys is the test “is a<b?” — bubble, insertion, selection, Merge Sort, Quick Sort, Heapsort.
The floor ➔ every comparison-based sort is Ω(NlogN) ⟹ Θ(NlogN) sorts are provably optimal within that class, and no cleverness gets below it.
Non-comparison ➔ Counting Sort and Radix Sort use the key as an array index, never comparing two items ⟹ the floor does not apply, at the cost of demanding bounded integer keys.
Selection rule ➔ keys are bounded integers with range M≪N ⟹ Counting Sort; decomposable into K narrow columns ⟹ Radix Sort; anything else ⟹ a comparison sort.
3. Bubble Sort
Mechanism ➔ walk L→R, swap each X>Y ➔ largest bubbles to the final tail.
Adaptivity ➔ BubbleSort II swapped flag ➔ O(n) best on sorted input.
Cost profile ➔ strict > ⟹ stable; up to Θ(n2) swaps (one per inversion).
4. Selection Sort
Mechanism ➔ scan suffix for min ➔ exactly one swap/pass to leftmost unsorted.
Swap-thrift ➔ Θ(n) swaps (wins when writes costly: flash, big records).
No best case ➔ the minimum must be located in full every pass, so there is no early exit ⟹ best = avg = worst O(n2); unstable, because the long-distance swap can throw an equal key backwards ([4a,2,3,4b,1] → [1,2,3,4b,4a]).
Mechanism ➔ sorted prefix; shift larger elements right, drop temp into the gap.
Adaptive + online ➔ best O(n) (sorted input ⟹ the inner while never fires) ➔ absorbs a new last element in one pass.
Stability from shifting ➔ strict > means equal keys are never swapped past each other; shifting rather than swapping is what preserves order ➔ basis of [[Sorted List (ADT)|sorted-list add]].
6. Stability (Property)
Definition ➔ equal keys keep input order; observable only sorting by key.
When it matters ➔ multi-pass sorting on several keys (sort by name, then by department, and the names stay ordered within each department) and any Radix Sort subsort; with distinct keys it is unobservable and free.
Mechanism heuristic ➔ shifting is stable, long-distance swapping is not. Bubble/insertion/merge move items past adjacent or ordered positions and preserve ties; selection/heap/quicksort throw an item across the array and can hurdle an equal key.
Bug vector ➔ > vs >= is a one-character stability break.
Engineering fix A — tuple the index in ➔ compare on (ki,i) so ties break by original position; blocked when the container cannot hold tuples.
Engineering fix B — a parallel index list ➔ keep index_list alongside the data, consult it only when list[a] == list[b], and apply every swap to both lists.
Time is UNCHANGED by either fix ➔ when list[a]=list[b] the index list is never read; when they are equal the tie-break compares two integers in O(1) ⟹ no factor is added to any bound.
Space grows by one class at most ➔ selection sort goes from O(1) auxiliary to Θ(N); but total space was already Θ(N) for the input, and Θ(N)+Θ(N)=Θ(N) ⟹ the total bound does not move, only the in-place claim is lost.
Comparisons are not O(1) ➔ if comparing two items costs O(k) (words compared letter-by-letter, tuples field-by-field), every bound gains a factor: elementary sorts become O(kN2), Merge Sort becomes O(kNlogN)· state k or declare it constant.
Why an integer comparison IS O(1) ➔ a fixed-width machine word is compared by one hardware instruction regardless of its value; a k-character string has no such instruction and must be walked symbol by symbol. The asymmetry is architectural, not algorithmic — which is why the item type, not the algorithm, decides whether k can be dropped.
Input space follows the same rule ➔ N integers occupy Θ(N); N strings of length up to k occupy Θ(Nk).
Comparisons ≥ swaps, always ➔ no algorithm swaps a pair it never compared ⟹ a swap-count bound can never exceed the comparison-count bound; use it as a self-check on a derived answer.
The recursion stack is auxiliary space ➔ logN levels of frames cost Θ(logN), and Θ(klogN) if each frame stores k words ⟹ recursive sorts are not in-place even when they never allocate a scratch array.
Three auxiliary-space classes, three causes ➔ O(1) ⟸ iterative, swaps only (the three elementary sorts, Heapsort) · O(logN) ⟸ a balanced recursion’s frame chain (Merge Sort’s stack, Quick Sort recursing smaller-first) · O(N) ⟸ a scratch array (Merge Sort’s merge buffer) or a degenerate recursion (Quick Sort’s worst-case stack). Naming the cause is what earns the mark.
Consequence ➔ an iterative rewrite is the standard route to O(1) auxiliary; this is why “in-place?” and “recursive?” are almost the same question in the summary table.
Best = worst is a diagnostic, not a coincidence ➔ the cases collapse exactly when (1) the algorithm has no early termination path and (2) the item values cannot change the control flow. Selection sort and Merge Sort satisfy both ⟹ one Θ covers every case; bubble (the swapped flag) and insertion (the inner while) violate (1), and Quick Sort violates (2) ➔ Big-O Notation.
9. What Sorting Is FOR
Sorting is rarely the goal ➔ it is the Θ(NlogN) preprocessing step that makes a later pass linear; if the follow-up pass is not cheaper, the sort was not worth it.
Grouping ➔ equal keys become contiguous, so counting occurrences, finding the mode, or aggregating by key collapses to one sequential scan instead of a nested search.
Deduplication ➔ duplicates are adjacent after sorting, so a two-pointer compaction removes them in Θ(N); the naive alternative — deleting in place by shifting the tail left — costs Θ(N) per removal and Θ(N2) overall.
An “in-place” requirement PICKS THE SORT ➔ the compaction is already O(1) auxiliary, so the sort is the only term that can break the budget: Merge Sort spends Θ(N) on scratch and Quick SortΘ(logN) on stack ⟹ Heapsort is the only valid choice, being the sole Θ(NlogN) sort with O(1) auxiliary. Naming the sort is the marked step, not the two-pointer loop.
Why not a hash set or BST ➔ both dedup in Θ(N) expected / Θ(NlogN) and preserve input order, but cost Θ(N) auxiliary ⟹ disqualified the moment “in-place” appears in the spec. Reach for them when order preservation matters more than space.
Order statistics ➔ sorting answers every rank at once; when only one rank is wanted, Quickselect does it in Θ(N) and sorting is over-solving.
The amortisation rule ➔ sort once at Θ(NlogN) and every subsequent query is cheap; sort per query and the preprocessing cost is paid again each time — the standard “is preprocessing worth it?” exam judgement.
⚙️ Core Implementation
🔹 Bubble Sort (II, adaptive)
bubble_sort with early-exit flag
def bubble_sort(the_list): n = len(the_list) for mark in range(n - 1, 0, -1): # tail [mark+1..] is sorted & final swapped = False # BubbleSort II: early-exit flag for i in range(mark): # scan only the unsorted prefix if the_list[i] > the_list[i + 1]: # STRICT '>' preserves stability the_list[i], the_list[i+1] = the_list[i+1], the_list[i] swapped = True if not swapped: # a clean pass => already sorted break
💡 Common Mistake:Strict > is load-bearing ➔ >= swaps equal neighbours and breaks stability; the swapped flag is the only source of the O(n) best case.
🔹 Selection Sort (min swaps)
selection_sort — inline minimum index, one swap per pass
def selection_sort(my_list): for i in range(len(my_list)): # leftmost unsorted position minimum = i for j in range(i + 1, len(my_list)): # find the minimum of the suffix if my_list[minimum] > my_list[j]: minimum = j my_list[i], my_list[minimum] = my_list[minimum], my_list[i] # ONE swap
💡 Common Mistake:No early-exit path ➔ comparisons are ∑k regardless of input ⟹ no O(n) best case; the long-distance swap makes it unstable.
🔹 Insertion Sort (adaptive, online)
insertion_sort — shift-left while the left neighbour is greater
def insertion_sort(my_list): for i in range(1, len(my_list)): key = my_list[i] # 1. stash it j = i - 1 while j >= 0 and key < my_list[j]: # 2. shift larger prefix elems right my_list[j + 1] = my_list[j] # (shift, never swap => stable) j = j - 1 my_list[j + 1] = key # 3. drop key into the gap
💡 Common Mistake:Flat "O(n2)" hides the best case ➔ on sorted input the while never fires ⟹ O(n); the weak “sorted-not-final” invariant is what makes it online.
🔹 Two-pointer deduplication (the payoff of sorting)
def dedup_sorted(my_list): # PRECONDITION: my_list is sorted, so duplicates are ADJACENT. # For an IN-PLACE guarantee the caller must sort with HEAPSORT -- # merge sort's scratch and quicksort's stack both break O(1) auxiliary. if len(my_list) == 0: return 0 write = 0 # last kept unique item for read in range(1, len(my_list)): # ONE forward scan if my_list[read] != my_list[write]: write = write + 1 my_list[write] = my_list[read] # overwrite, never shift return write + 1 # new logical length
💡 Common Mistake:Two pointers, not one ➔ read advances every iteration, write only on a new value; collapsing them into one index either skips elements or overwrites unread ones.
⚖️ Core Decision Matrix
(Best / Average / Worst time, auxiliary space, stability, in-place. ×O(k) comparison cost applies to every comparison-based row.)
When It Flips: choose an O(N2) sort when N is small (low overhead), nearly-sorted (insertion → O(N)), or writes dominate (selection's Θ(N) swaps). Choose a non-comparison sort only when the keys are bounded integers: Counting Sort while M≪NlogN, Radix Sort while K<log2N. Otherwise the Ω(NlogN) floor makes Merge Sort/Heapsort optimal — merge for stability, heap for O(1) auxiliary.
📊 Exam Execution Trace
Manual Execution Trace
Insertion Sort on [5, 2, 4, 1]:
Step / State
Trigger Op
mark
temp
Shifts
Array Payload
0 (Init)
init
−
−
−
[5, 2, 4, 1]
1
insert
1
2
5→
[2, 5, 4, 1]
2
insert
2
4
5→
[2, 4, 5, 1]
3
insert
3
1
5,4,2→
[1, 2, 4, 5]
Applied Exercise
Problem: Derive the worst-case bound of the elementary sorts on reverse-sorted input, then re-quote it when each comparison costs O(k).
Derivation Proof / Hand-Calculation Walkthrough:
mark j shifts up to j elementswith O(k) per comparison⟹total=j=1∑n−1j=2n(n−1)=2n2−n=Θ(n2)(the [[Arithmetic Series]])⟹Θ(kn2)
Final Extracted Output: worst case =Θ(n2) for bubble/selection/insertion, or Θ(kn2) with non-constant comparison cost; bubble & insertion reach O(n) best.
🧠 Active Recall
Why must a correct sort satisfy both permutation and ordering?
Hint: Recognise that ordering alone admits element-loss bugs.
Answer
Short answer:Permutation forces the exact input multiset; ordering forces non-decreasing.
Why:Spec completeness ➔ an ordered output that dropped/duplicated elements still “looks sorted” — only both clauses fully specify correctness.
Bubble, selection, insertion are all O(n2) — on what grounds distinguish them?
Hint: Compare by adaptivity, swap count, stability, online-ness — not Big-O.
Answer
Short answer:Selection = Θ(n) swaps but never adaptive/unstable; insertion = adaptive + online + stable; bubble = adaptive + stable but Θ(n2) swaps.
Prove the elementary sorts are O(n2), yet bubble/insertion reach O(n) best.
Hint: Evaluate the nested-loop arithmetic series and identify the short-circuit.
Answer
Short answer: Nested loops sum ∑j=1n−1j=2n2−n=Θ(n2).
Why:Short-circuit ➔ insertion’s while never fires on sorted input; bubble’s swapped flag exits after one clean pass ⟹ O(n); selection has no such path.
Sort 10M records by primary then secondary key in O(nlogn) with stability — which algorithm, and how to stabilise quicksort if forced?
Hint: Match the stability requirement to the algorithm and know the index-tag trick.
Answer
Short answer: Use merge sort (or Timsort) — guaranteed Θ(nlogn), naturally stable.
Why:Forced stability is the fallback, not the plan ➔ if quicksort is mandated, index-tagging converts it at Θ(n) space and no time penalty ➔ §6 — at which point merge sort’s scratch array costs nothing extra, so the tagging only pays when the sort itself is fixed.
Every sort we have met is Ω(NlogN) — so how can Radix Sort claim Θ(KN)?
Hint: The floor is a statement about a class of algorithms, not about the problem.
Answer
Short answer: The floor applies only to comparison-based sorts; radix and counting never compare two keys.
Why:Extra assumption buys the speed ➔ using the key as an array index requires bounded integer keys, which a comparison sort does not assume — the bound is escaped by narrowing the problem, not by beating it.
Merge sort allocates Θ(N) scratch and quicksort allocates none — why is quicksort still recorded as not in-place?
Hint: Count every word the algorithm keeps live, not just heap allocations.
Answer
Short answer: The recursion stack is auxiliary space — Θ(logN) live frames ⟹ not O(1) auxiliary.
Why:In-place ≡O(1) auxiliary ➔ frames count, and Θ(klogN) if each frame holds k words; an iterative rewrite is what drops the term, which is why Heapsort is the in-place Θ(NlogN) option.