Level 3 — ProductionSorting & Searching

Sorting & Searching

45 minutes60 marksprintable — key stays hidden on paper

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 nn elements requires Ω(nlogn)\Omega(n \log n) 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 O(n)O(n) 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 nn for deterministic (last-element pivot) quicksort and state its running time with justification. (3)

(c) Explain out loud why randomized quicksort achieves expected O(nlogn)O(n\log n) 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 [5a,3,5b,1,4][5_a, 3, 5_b, 1, 4] (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 kk-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 3n10\tfrac{3n}{10} elements and less than at least 3n10\tfrac{3n}{10}. Derive the 3n/103\lceil n/10\rceil-style bound. (4)

(c) Write the recurrence T(n)T(n/5)+T(7n/10)+O(n)T(n) \le T(\lceil n/5\rceil) + T(7n/10) + O(n) and argue informally why it solves to O(n)O(n). (3)


Question 5 — Counting / Radix sort (8 marks)

(a) Write counting sort from memory for an array of integers in range [0,k][0, k]. Explain why building the prefix-sum count and iterating the input right-to-left gives stability. (5)

(b) You must sort n=106n = 10^6 integers each in range [0,109][0, 10^9]. Explain why plain counting sort is a bad idea and how LSD radix sort with base b=103b=10^3 fixes it. Give the number of passes dd. (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 O(logn)O(\log n) function search(A, target) returning an index or 1-1. (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 aiaja_i \lessgtr a_j; 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 n!n! possible input orderings, all needing distinct sorted outputs, so the tree must have n!\ge n! leaves. (1)
  • A binary tree of height hh has at most 2h2^h leaves, so 2hn!hlog2(n!)2^h \ge n! \Rightarrow h \ge \log_2(n!). (1)
  • Worst-case comparisons == height hh. (1)
  • Stirling: log2(n!)=i=1nlog2i1nlog2xdx=Θ(nlogn)\log_2(n!) = \sum_{i=1}^n \log_2 i \ge \int_1^n \log_2 x\,dx = \Theta(n\log n); more precisely n!(n/e)nn! \ge (n/e)^n so log2(n!)nlog2nnlog2e=Ω(nlogn)\log_2(n!) \ge n\log_2 n - n\log_2 e = \Omega(n\log n). (2)

(c) (2) False: the Ω(nlogn)\Omega(n\log n) lower bound holds for the worst case of any comparison sort, so no comparison sort can be O(n)O(n) 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 pp. 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 n1n-1 and 00. Recurrence T(n)=T(n1)+Θ(n)=Θ(n2)T(n)=T(n-1)+\Theta(n) = \Theta(n^2). (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 Xij=[zi compared with zj]X_{ij}=[z_i \text{ compared with } z_j], zi,zjz_i,z_j are compared iff one is the first pivot chosen from the range zi..zjz_i..z_j, giving Pr=2ji+1\Pr = \frac{2}{j-i+1}. Then E[comparisons]=i<j2ji+1=O(nlnn)E[\text{comparisons}] = \sum_{i<j}\frac{2}{j-i+1} = O(n\ln n) (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) T(n)=2T(n/2)+Θ(n)T(n)=2T(n/2)+\Theta(n). Master Theorem: a=2,b=2,f(n)=Θ(n)a=2,b=2,f(n)=\Theta(n), nlogba=n1n^{\log_b a}=n^1; since f(n)=Θ(nlogba)f(n)=\Theta(n^{\log_b a}) this is Case 2T(n)=Θ(nlogn)T(n)=\Theta(n\log n).

(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 [5a,3][5_a,3] and [5b,1,4][5_b,1,4]. Sorted: [3,5a][3,5_a] and [1,4,5b][1,4,5_b]. Merge → [1,3,4,5a,5b][1,3,4,5_a,5_b]. The order 5a5_a before 5b5_b is preserved → stable confirmed. (2 trace, 1 conclusion)


Question 4 (12)

(a) (5) Median-of-medians select(A,k):

  1. Divide nn elements into n/5\lceil n/5\rceil groups of 5.
  2. Sort each group, take its median → collect medians.
  3. Recursively select the median-of-medians MM of those medians.
  4. Partition AA around MM; let MM have rank rr.
  5. If k=rk=r return MM; if k<rk<r recurse left; else recurse right for krk-r. (1 each step)

(b) (4) There are n/5\lceil n/5\rceil group medians. MM is \ge half of them, i.e. 12n/5\ge \tfrac{1}{2}\lceil n/5\rceil medians. Each such median has 2 elements in its group \le it (plus itself), so at least 3(12n/52)3n1063\left(\tfrac{1}{2}\lceil n/5\rceil - 2\right) \ge \tfrac{3n}{10} - 6 elements are M\le M. By symmetry the same count is M\ge M. Hence each recursive partition discards 3n/10\ge 3n/10 elements, leaving 7n/10\le 7n/10. (2 median count, 2 the 3n/10 bound)

(c) (3) T(n)T(n/5)+T(7n/10)+O(n)T(n)\le T(\lceil n/5\rceil)+T(7n/10)+O(n). Since 15+710=910<1\tfrac15+\tfrac{7}{10}=\tfrac{9}{10}<1, the subproblem sizes form a geometric-decaying sum; guess T(n)cnT(n)\le cn, then cn/5+7cn/10+an=9c10n+ancnc\,n/5 + 7cn/10 + an = \tfrac{9c}{10}n + an \le cn holds for c10ac\ge 10a. Hence T(n)=O(n)T(n)=O(n). (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 x\le x = last output slot for key xx. 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 k+1=109+1k+1 = 10^9+1 → ~4 GB memory, far exceeding nn; time/space O(n+k)O(n+k) dominated by knk \gg n. LSD radix with base b=103b=10^3: each digit range is [0,999][0,999] (small k=1000k=1000), and d=log1000(109)=3d = \lceil \log_{1000}(10^9) \rceil = 3 passes of stable counting sort → O(d(n+b))=O(3(106+103))O(d(n+b)) = O(3(10^6+10^3)). 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 → O(logn)O(\log n). (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"}
]