28. Kth Largest Rerank Score
Problem
After Fin's reranker scores a batch of candidate passages, a monitoring job occasionally needs to pull the kth-best score from the batch, e.g. to check where the score cutoff for the top-3 shown to the model would fall. This runs on every single query, so fully sorting the batch just to read off one order statistic is wasteful.
Given the unsorted scores and an integer k, return the kth largest
score in the batch WITHOUT sorting the whole batch.
Source: src/28_kth_largest_rerank_score.py
def kth_largest(scores: list[float], k: int) -> float: ...
kth_largest([3.0, 2.0, 1.0, 5.0, 6.0, 4.0], 2)
# -> 5.0 (sorted desc: 6, 5, 4, 3, 2, 1 -- 2nd largest is 5)
kth_largest([3.0, 2.0, 3.0, 1.0, 2.0, 4.0, 5.0, 5.0, 6.0], 4)
# -> 4.0
Step-by-Step Approach
- Copy
scoresinto a working arrayarrso the input isn't mutated, and computetarget_index = len(arr) - k— the index the kth largest value would land at ifarrwere sorted ascending. - Write a
partition(left, right, pivot_index)helper: move the pivot value to the end, then walklefttorightswapping any element smaller than the pivot into a growing "less than pivot" region, and finally swap the pivot into its correct sorted position. - Set
left = 0,right = len(arr) - 1and loop whileleft < right. - Each iteration, pick a random index in
[left, right]as the pivot (randomizing avoids worst-case behavior on adversarial or already-sorted input) and partition around it. - If the pivot lands exactly at
target_index, that value is the answer — stop. If the pivot's final index is less thantarget_index, the answer is to its right, so search[pivot_index + 1, right]. Otherwise search[left, pivot_index - 1]. - Once the loop ends (or breaks early), return
arr[target_index].
The key insight is that finding the kth order statistic doesn't require a total
order over the whole array — each partition step only needs to narrow down which
side the target index falls on, discarding the other side entirely. This gives
average O(n) time versus O(n log n) for a full sort.
Reference solution
import random
def kth_largest(scores: list[float], k: int) -> float:
# quickselect with a random pivot: average O(n) time, O(n) space for the working copy
# shallow copy, caller's list untouched
arr = list(scores)
# index of the kth largest once arr is sorted ascending
target_index = len(arr) - k
def partition(left: int, right: int, pivot_index: int) -> int:
pivot_value = arr[pivot_index]
# swap pivot to end (no temp var)
arr[pivot_index], arr[right] = arr[right], arr[pivot_index]
store_index = left
for i in range(left, right):
if arr[i] < pivot_value:
# move smaller element into place
arr[store_index], arr[i] = arr[i], arr[store_index]
store_index += 1
# drop pivot into sorted position
arr[right], arr[store_index] = arr[store_index], arr[right]
return store_index
left, right = 0, len(arr) - 1
while left < right:
# random pivot avoids O(n^2) worst case
pivot_index = random.randint(left, right)
pivot_index = partition(left, right, pivot_index)
if pivot_index == target_index:
break
elif pivot_index < target_index:
left = pivot_index + 1
else:
right = pivot_index - 1
return arr[target_index]
Key Functions & Tricks
random.randint(left, right)— picks a pivot index, inclusive of both ends.- Random pivot — avoids worst-case
O(n^2)on sorted or adversarial input. list(scores)— shallow copy so in-place swaps don't mutate the caller's list.arr[i], arr[j] = arr[j], arr[i]— tuple-unpacking swap without a temp variable.- Lomuto partition — swaps smaller-than-pivot elements left, then drops the pivot at
store_index. target_index = len(arr) - k— kth largest is indexn - kin ascending order.
How to Recognize This Pattern
Signals: "find the kth largest/smallest" or "find the median" without needing the rest of the array sorted is the classic quickselect tell. If sorting would work but feels like overkill because you only need one order statistic, quickselect (or a heap of size k) is the intended optimization.
Variations: find the kth smallest instead (just flip target_index to
k - 1); or maintain a min-heap of size k when scores
arrive as a stream rather than all at once, since quickselect needs random access
to the full array.
Common pitfall: forgetting to randomize the pivot (or at least pick a
median-of-three) — a fixed pivot choice like "always the last element" degrades to
O(n^2) on sorted or reverse-sorted input. Also easy to get
target_index backwards (kth largest is index n - k in
ascending order, not k - 1).