Sorting & Searching
Time limit: 45 minutes Total marks: 60 Instructions: Write code from memory where asked. Derivations must be shown from first principles. "Explain-out-loud" prompts expect you to reason in prose as if teaching.
Question 1 — Decision tree lower bound (10 marks)
(a) State the comparison-sort model and explain why any deterministic comparison sort corresponds to a binary decision tree. (3)
(b) Prove from scratch that any comparison sort on elements requires comparisons in the worst case. Use the height-of-decision-tree argument and Stirling's approximation. (5)
(c) A student claims a comparison sort exists that runs in on every input. In one sentence, refute this using your proof. (2)
Question 2 — Quicksort partition + randomization (12 marks)
(a) Write, from memory, the Lomuto partition procedure in pseudocode for partition(A, lo, hi) using A[hi] as pivot. State what it returns and the invariant it maintains. (5)
(b) Give a concrete worst-case input of size for deterministic (last-element pivot) quicksort and state its running time with justification. (3)
(c) Explain out loud why randomized quicksort achieves expected regardless of input. Reference the indicator-variable argument for the expected number of comparisons. (4)
Question 3 — Merge sort correctness & stability (10 marks)
(a) Write the recurrence for merge sort's running time and solve it with the Master Theorem, stating the case used. (3)
(b) Prove merge is stable: given the standard "take from left on ties" tie-break, equal keys preserve input order. Argue by the loop invariant. (4)
(c) Merge sort (subscripts mark two equal 5's). Show the final order of the two 5's and confirm it matches stability. (3)
Question 4 — Linear-time selection (12 marks)
(a) Describe the median-of-medians algorithm to select the -th smallest element. List its five steps. (5)
(b) Show that the pivot chosen by median-of-medians is guaranteed to be greater than at least roughly elements and less than at least . Derive the -style bound. (4)
(c) Write the recurrence and argue informally why it solves to . (3)
Question 5 — Counting / Radix sort (8 marks)
(a) Write counting sort from memory for an array of integers in range . Explain why building the prefix-sum count and iterating the input right-to-left gives stability. (5)
(b) You must sort integers each in range . Explain why plain counting sort is a bad idea and how LSD radix sort with base fixes it. Give the number of passes . (3)
Question 6 — Binary search on rotated array (8 marks)
(a) A sorted array of distinct integers is rotated at an unknown pivot. Write, from memory, an function search(A, target) returning an index or . (5)
(b) Explain out loud the key invariant: at each step at least one half is sorted, and how you decide which half to recurse into. (3)
Answer keyMark scheme & solutions
Question 1 (10)
(a) (3) The comparison-sort model: algorithm accesses input only through comparisons ; it may not inspect key values otherwise. Each comparison has two outcomes → binary branching. A run is a root-to-leaf path; each leaf must correspond to a distinct permutation that reorders the input into sorted order. (1 model, 1 binary branching, 1 leaves = permutations)
(b) (5)
- There are possible input orderings, all needing distinct sorted outputs, so the tree must have leaves. (1)
- A binary tree of height has at most leaves, so . (1)
- Worst-case comparisons height . (1)
- Stirling: ; more precisely so . (2)
(c) (2) False: the lower bound holds for the worst case of any comparison sort, so no comparison sort can be on every input. (non-comparison sorts like counting sort escape by not being comparison-based.)
Question 2 (12)
(a) (5)
partition(A, lo, hi):
pivot = A[hi]
i = lo - 1
for j = lo to hi-1:
if A[j] <= pivot:
i = i + 1
swap A[i], A[j]
swap A[i+1], A[hi]
return i + 1
Returns final pivot index . Invariant: at start of each iteration, A[lo..i] <= pivot and A[i+1..j-1] > pivot. (3 code, 1 return, 1 invariant)
(b) (3) An already-sorted (ascending) array with last-element pivot: pivot is always the max, partition splits into sizes and . Recurrence . (1 input, 2 justification)
(c) (4) Randomly choosing the pivot makes the partition split independent of input order; every element is equally likely to be pivot. Using indicator , are compared iff one is the first pivot chosen from the range , giving . Then (harmonic sum). Expectation is over the random pivots, so it holds for every input. (1 pivot indep, 1 indicator setup, 1 prob 2/(j-i+1), 1 harmonic sum→O(n log n))
Question 3 (10)
(a) (3) . Master Theorem: , ; since this is Case 2 → .
(b) (4) Loop invariant during merge: the output built so far is a sorted permutation of the consumed prefixes, and among consumed elements, equal keys retain input relative order. On a tie (L[i] == R[j]), we take from left first. Since left subarray held earlier-input elements, taking left preserves original order for equal keys. Maintenance: each step either extends invariant by a strictly correct or tie-broken-left element. Termination gives full stable merge. (1 invariant, 1 tie-break rule, 2 argument)
(c) (3) Split and . Sorted: and . Merge → . The order before is preserved → stable confirmed. (2 trace, 1 conclusion)
Question 4 (12)
(a) (5) Median-of-medians select(A,k):
- Divide elements into groups of 5.
- Sort each group, take its median → collect medians.
- Recursively
selectthe median-of-medians of those medians. - Partition around ; let have rank .
- If return ; if recurse left; else recurse right for . (1 each step)
(b) (4) There are group medians. is half of them, i.e. medians. Each such median has 2 elements in its group it (plus itself), so at least elements are . By symmetry the same count is . Hence each recursive partition discards elements, leaving . (2 median count, 2 the 3n/10 bound)
(c) (3) . Since , the subproblem sizes form a geometric-decaying sum; guess , then holds for . Hence . (1 recurrence, 2 argument)
Question 5 (8)
(a) (5)
countingSort(A, k):
C = array[0..k] of 0
for x in A: C[x] += 1
for i = 1 to k: C[i] += C[i-1] # prefix sums
B = array[0..n-1]
for j = n-1 downto 0: # right-to-left
x = A[j]
C[x] -= 1
B[C[x]] = x
return B
C[x] after prefix sum = number of elements = last output slot for key . Iterating right-to-left and decrementing places later-input equal keys into later slots, preserving input order → stable. (3 code, 2 stability explanation)
(b) (3) Plain counting sort needs a count array of size → ~4 GB memory, far exceeding ; time/space dominated by . LSD radix with base : each digit range is (small ), and passes of stable counting sort → . d = 3. (2 why bad, 1 d=3)
Question 6 (8)
(a) (5)
search(A, target):
lo, hi = 0, len(A)-1
while lo <= hi:
mid = (lo + hi) // 2
if A[mid] == target: return mid
if A[lo] <= A[mid]: # left half sorted
if A[lo] <= target < A[mid]: hi = mid - 1
else: lo = mid + 1
else: # right half sorted
if A[mid] < target <= A[hi]: lo = mid + 1
else: hi = mid - 1
return -1
(4 code correctness, 1 correct −1 return)
(b) (3) Invariant: for any window [lo,hi], at least one of the two halves split at mid is fully sorted (rotation point lies in the other half). Determine the sorted half by comparing A[lo] vs A[mid]. If target lies within that sorted half's value range, recurse there; otherwise recurse into the other half. This halves the search each step → . (1 invariant, 1 which-half test, 1 halving)
[
{"claim":"log2(n!) >= n log2 n - n log2 e (Stirling lower bound direction) at n=8", "code":"import math\nn=8\nlhs=math.log2(math.factorial(n))\nrhs=n*math.log2(n)-n*math.log2(math.e)\nresult = lhs >= rhs"},
{"claim":"Randomized quicksort expected comparisons sum 2/(j-i+1) equals 2*(n+1)*H_n - 4n form; check n=4 numeric", "code":"from sympy import Rational\nn=4\ntotal=0\nfor i in range(1,n+1):\n for j in range(i+1,n+1):\n total+=Rational(2,j-i+1)\nresult = total == Rational(2*(sum(Rational(1,t) for t in range(1,n+1)))*(n+1)-4*n)"},
{"claim":"Radix passes d for range 1e9 base 1000 is 3", "code":"import math\nd=math.ceil(math.log(10**9, 1000))\nresult = d == 3"},
{"claim":"Median-of-medians leaves at most 7n/10 (approx) so 1/5+7/10<1", "code":"from sympy import Rational\nresult = Rational(1,5)+Rational(7,10) < 1"}
]