Context:FIT2004_MOC · Partitioning (Quicksort) reused for a different problem — find the k-th smallest without sorting · the canonical decrease-and-conquer win: recurse into one side, not both · worst case eliminated by Median of Medians
Quick Revision
🎯 Objective: partition once, then discard the side that cannot contain rank k ➔ T(N)=T(N/2)+Θ(N)=Θ(N) expected, beating sort-then-index’s Θ(NlogN).
📦 Core Components:partition (shared with Quick Sort) ➔ pivot lands final at index j | compare k to j ➔ O(1) decision | one-sided recursion ➔ the entire difference from quicksort.
⚡ Key Constraint:Θ(N) is expected, not guaranteed — bad pivots give T(N)=T(N−1)+Θ(N)=Θ(N2); only a Median of Medians pivot makes Θ(N) a worst-case bound.
📝 How It Works
1. k-th Order Statistics — the Problem
Specification ➔ given N unsorted orderable items and a rank k, return the element that would sit at index k if the list were sorted — without producing the sorted list. Equivalently: return the ksmallest items, internal order irrelevant.
The whole quartile family is one call ➔ Q1 at k=N/4 · median at k=N/2 · Q3 at k=3N/4 ➔ Measures of Spread and Boxplots. The median is why it matters — it is the pivot Quick Sort wants.
“Isn’t this just partition?” ➔ yes — a partition already separates smaller from larger around one element; selection is the question of driving that partition to the rank you want ➔ Partitioning (Quicksort).
2. The Baseline — Sort and Slice
The obvious algorithm ➔ sort, then slice the first k ⟹ O(NMlogN)+O(k), M the cost of one comparison ➔ Sorting Problem.
The linear sorts do not rescue it ➔ Counting Sort needs the key range capped; Radix Sort needs bounded key width — neither can be assumed for arbitrary input, so the logN stands.
Sorting is over-solving ➔ it answers every rank at once for Θ(NlogN); selection answers one rank for Θ(N). Choosing sorting when a single rank is asked is the LO3 error.
3. One-Sided Recursion — The Whole Idea
Partition tells you the pivot’s TRUE rank ➔ after partition(lo, hi) returns boundary j, the pivot is at its final sorted index, with zero further work.
Three-way decision ➔ j=k ⟹ done · k<j ⟹ the answer is in [lo, j-1], throw the right side away · k>j ⟹ recurse on [j+1, hi] with kunchanged (absolute indices).
Why the cost collapses ➔ Quick Sort recurses on both halves so every level still costs Θ(N) across logN levels; quickselect recurses on one, so the level costs halve — a Geometric Series with r=21 summing to <2N.
4. Cost Profile
Best case is Θ(N), not Θ(1) ➔ even a first-try hit has already paid for one full partition ⟹ the whole input is read no matter what.
Expected Θ(N) ➔ T(N)=T(N/2)+Θ(N) under balanced pivots ⟹ root-dominated (r=21<1) ➔ Solving Recurrences (Telescoping). (The lecturer’s formal average-case derivation — multiply by N, subtract the (N−1) instance so the summations cancel — is flagged NOT EXAMINABLE.)
Worst Θ(N2) ➔ an extreme pivot every level peels one element ⟹ T(N)=T(N−1)+cN=Θ(N2) by the Arithmetic Series — same failure mode and same inputs as Quick Sort.
Auxiliary space is O(1) when written iteratively ➔ the recursion is tail recursion, so it rewrites as a while loop over lo/hi ⟹ genuinely in-place, unlike Quick Sort, whose first call cannot be eliminated.
Destructive ➔ partitioning permutes the caller’s array; if the original order matters, copy first and pay Θ(N) space.
5. Feeding Quick Sort a Pivot — the Exact Median Fails
The tempting plan ➔ call quickselect for the exact median, use it as the pivot, and skip quicksort’s own partition — quickselect already partitioned the range.
It does not work ➔ quickselect’s own worst case is Θ(N2), so the pivot-finding call at the root costs N2 and the level sum is N2+2N2+⋯=2N2 ⟹ the sort stays Θ(N2). Derivation in Applied Exercise 2.
The fix is to stop demanding the exact median ➔ any pivot inside the middle band suffices, because a constant-fraction split already gives logarithmic depth ➔ Quick Sort §3.
Median of Medians delivers exactly that band ➔ a pivot provably between the 30th and 70th percentile, found in Θ(N)worst case ⟹ height O(logN), partition already done ⟹ Θ(NlogN) worst-case quicksort. Lecturer’s coda: “in reality, random pivot works well due to probability” — the guarantee is bought with a constant factor nobody pays voluntarily.
6. Adapting Quickselect to a New Problem (applied W4 — LO1)
The reusable skeleton ➔ partition once ➔ read something off the pivot’s position ➔ recurse into the single side that can still contain the answer. Every variant changes only what is read off and which side survives.
Weighted median ➔ each ai carries weight wi with ∑wi=1; the answer is the ak whose left and right weight sums are each ≤21. Swap “compare rank j to k” for “compare weight sums to w”: if neither side exceeds w the pivot is the answer, otherwise recurse into the heavier side with w reduced by the weight skipped. Same Θ(n) expected.
The k closest to the median ➔ run quickselect once for the median m, then again for the k-th smallest ∣ai−m∣; take everything strictly below that value and top up to k from the ties. Two linear calls ⟹ Θ(n).
Why the top-up is mandatory ➔ the elements are unique but their distances are not (m+d and m−d tie), so “strictly closer than the k-th” can yield fewer than k; any tied element will do, being equally close.
Killing the Θ(n) space ➔ do not materialise the absolute-difference copy; run a modified quickselect that interpretsarray[i] as ∣array[i]−m∣ on the fly ⟹ O(1) extra space beyond the output.
Order statistic across two sorted arrays[D] ➔ order(a[i])=i+∣{j:b[j]<a[i]}∣ is monotone in i, hence binary-searchable: an inner Binary Search on b inside an outer one on a gives Θ(lognlogm); running the two searches simultaneously removes the nesting for Θ(logn+logm).
def quickselect(my_list, k): # returns the k-th smallest, k zero-indexed: k=0 is the minimum lo = 0 hi = len(my_list) - 1 while lo < hi: j = partition(my_list, lo, hi) # pivot lands FINAL at index j if j == k: return my_list[j] # exact rank hit -- stop immediately elif k < j: hi = j - 1 # answer is left; discard [j..hi] else: lo = j + 1 # answer is right; discard [lo..j] return my_list[lo] # window collapsed to one element
💡 Common Mistake:Recursing on both sides “to be safe” ➔ that is Quick Sort; it restores the Θ(NlogN) level sum and throws away the entire point.
🔹 Weighted quickselect — the same skeleton, a different read-off
weighted_quickselect(call with w=21 for the weighted median)
def weighted_quickselect(array, weight, lo, hi, w): if hi <= lo: return array[lo] pivot = array[lo] j = partition(array, weight, lo, hi, pivot) # permute weights IN UNISON left = sum_range(weight, lo, j - 1) if left > w: return weighted_quickselect(array, weight, lo, j - 1, w) elif left + weight[j] >= w: return array[j] # pivot IS the weighted median else: return weighted_quickselect(array, weight, j + 1, hi, w - (left + weight[j]))
💡 Common Mistake:Partitioning the values without the weights ➔ the weight array must be permuted by the same swaps, or every weight attaches to the wrong element after the first call.
💡 Common Mistake:Recursing right with the original w ➔ the weight discarded on the left must be subtracted, exactly as k would be re-indexed in a relative-index quickselect.
⚖️ Core Decision Matrix
Approach
Time
Auxiliary space
Needs all N upfront?
Selection rule
Sort, then slice
O(NMlogN)+O(k)
Θ(N) merge · O(logN) quick
Yes
many ranks queried, or the sorted list is wanted anyway
N unknown or unbounded, or all k smallest wanted as they arrive
When It Flips: quickselect wins while exactly one rank is needed from a resident array. Ask for ≥logN different ranks and sorting once amortises better; drop the "resident array" assumption and quickselect is unusable at any cost, because it must partition the whole input — that regime belongs to the size-k heap in Online Algorithm.
📊 Exam Execution Trace & Applied Exercises
Manual Execution Trace
quickselect([7, 2, 9, 1, 5, 8, 3], k=2) — the 3rd smallest. Pivot = middle element of the live window (Lomuto):
Step
Window [lo, hi]
Pivot
Array after partition
Boundary j
Decision
0 (Init)
[0, 6]
—
[7, 2, 9, 1, 5, 8, 3]
—
—
1
[0, 6]
1
[1, 2, 9, 7, 5, 8, 3]
0
j<k ⟹ lo = 1
2
[1, 6]
7
[1, 3, 2, 5, 7, 8, 9]
4
k<j ⟹ hi = 3
3
[1, 3]
2
[1, 2, 3, 5, 7, 8, 9]
1
j<k ⟹ lo = 2
4
[2, 3]
3
[1, 2, 3, 5, 7, 8, 9]
2
j=k ⟹ return 3
Window sizes 7→6→3→2 — discarded halves are never revisited, and the array is left partially sorted only: rank 2 is correct and nothing else is guaranteed.
Applied Exercise 1 — where quicksort’s logN went
Problem: derive quickselect’s expected time from its recurrence and contrast the level sum with Quick Sort’s.
Final Extracted Output:Θ(N) expected — the logN factor is bought by the second recursive call, so deleting it deletes the factor. Same depth, different total. Worst case is still Θ(N2) via T(N)=T(N−1)+cN.
Applied Exercise 2 — why an exact-median pivot does not fix quicksort
Problem: quicksort picks its pivot by calling quickselect for the exact median. Bound the result in the worst case.
level i=2iN2⟹T(N)=i≥0∑2iN2=2N2=Θ(N2)
Final Extracted Output: the split is now perfect at every level and the sort is still quadratic — the cost moved from the recursion shape into the pivot-finding step. Only a pivot rule linear in the worst case (Median of Medians) closes it, and a merely 30/70 pivot suffices because constant-fraction splits already give O(logN) depth.
Applied Exercise 3 — the probability argument for expected Θ(n)
Problem: call a pivot good if it falls in the middle 50%. Bound quickselect’s expected work.
worst good pivotT(n)Pr[good]=21:the 25th/75th percentile⇒recurse on 0.75n=cni≥0∑0.75i≤1−0.75cn=4cn⇒E[attempts]=2⇒E[T(n)]≤8cn=O(n)
Final Extracted Output:O(n) expected. Two reproducible moves: (i) a constant-fraction shrink turns level costs into a Geometric Series summing to a constant multiple of n; (ii) a pivot good with probability p costs 1/p attempts in expectation, multiplying the bound by a constant. The identical argument with both halves kept gives Quick Sort’s O(nlogn).
⚠️ Common Mistakes
💡 Quoting Θ(N) with no case ➔ Θ(N) is the best/expected bound; the worst case is Θ(N2) unless you name the pivot policy. “Quickselect is linear” is only true of the Median of Medians variant ➔ Big-O Notation.
💡 Claiming a best case better than Θ(N) ➔ hitting j=k on the first try still costs one full partition.
💡 Translating k when recursing right ➔ with absolute indices k never changes; subtracting j+1 is only correct if the recursive call re-indexes from 0. Pick one convention and state it.
💡 Assuming the array is sorted afterwards ➔ only rank k is final; the two sides are partitioned, not ordered.
🧠 Active Recall
Quicksort and quickselect run the identical partition, yet one is Θ(NlogN) and the other Θ(N). Where exactly does the logN go?
Hint: Count the work per level, not the depth.
Answer
Short answer: the logN is bought by the second recursive call, and quickselect does not make it.
Why:Level sums differ ➔ quicksort keeps both halves so every one of the logN levels totals Θ(N); quickselect keeps one, so the levels are N,2N,4N,… — a Geometric Series with r=21 summing to <2N.
You must return the median of 108 sensor readings within a hard time budget. Argue for a pivot policy.
Hint: Distinguish “improbable” from “impossible”.
Answer
Short answer: a random pivot gives Θ(N) expected but no bound; a hard budget demands Median of Medians, which makes Θ(N) the worst-case bound.
Why:Randomisation is a probability claim ➔ it removes the adversary’s ability to choose a bad input, not the possibility of a bad run. A deterministic constant-fraction split is the only thing converting the expectation into a guarantee — at a large constant factor.
Quickselect is described as in-place while Quick Sort is not, though both only permute the array. Justify the difference.
Hint: Ask which recursive call has work waiting after it returns.
Answer
Short answer: quickselect’s single recursive call is a tail call ⟹ rewritable as a loop ⟹ O(1) auxiliary; quicksort’s first call is not.
Why:The stack is auxiliary space ➔ quicksort must return from the left call to make the right one, so Θ(logN) frames stay live; quickselect has nothing pending, so lo/hi reassignment replaces the frame ➔ Analysing Recursive Algorithms (Time and Auxiliary Space).
Find the k elements closest to the median of n unique integers in Θ(n), and justify each step.
Hint: Two selections, then a tie-break you must not skip.
Answer
Short answer: quickselect for the median m; quickselect again on ∣ai−m∣ for the k-th smallest distance; take everything strictly smaller and top up from the ties.
Why:Distances need not be unique even when the values are ➔ m+d and m−d both sit at distance d, so the strict set can fall short of k; any tied element is equally close. Two Θ(n) calls are still Θ(n), and interpreting array[i] as its absolute difference from m inside the partition removes the Θ(n) auxiliary space.