Top-k becomes a user-tunable parameter up to the full catalog — which selection strategy do you commit to?
answer
- Recheck every assumption that needed k << n
- What does log k become as k grows?
- Space O(k) is no longer a small number
- The crossover is benchmarked, not derived
- Count the code paths a team must maintain
basics
~20 sCommit to one default with a documented, measured threshold rather than three tuned paths. As k approaches n, O(n log k) converges on O(n log n) and the candidate structure holds O(n) items — above the crossover, just sort.
solid answer
~50 sThe asymptotics stop separating the options as `k` grows: `log k` approaches `log n`, so the candidate-retaining scan converges on the full sort's cost while also holding O(n) state and delivering an unordered result the product then has to order at O(k log k). Quickselect stays linear in `k`, which is its strongest region — but it still mutates the buffer, still carries a quadratic tail, and still hands back an unordered prefix. So my recommendation is one interface, two paths at most: the retained-candidate scan below a crossover measured on production hardware and data, plain sort above it, with that crossover a configuration value rather than a constant readers must trust. I would resist a third tuned strategy unless profiling on real traffic shows it pays for the code every engineer must now read correctly, and I would publish the k range the service supports rather than let a tunable imply every value is cheap.
go deeper
Take away the shape of it: the small-k arguments stop applying when k gets large, because the candidate structure ends up holding nearly everything and costing nearly as much as ordering the lot.
Be able to show the convergence concretely — log k approaching log n, O(k) space approaching O(n), and the post-selection ordering step growing from negligible to significant.
Show you would benchmark the crossover on real records rather than derive it, and that you would pick a branch structure whose behaviour you can explain from a profile.
Own the whole contract: how many code paths the team maintains, what the interface promises about ordering, the published maximum k, and whether the median or the tail is the number the SLA is written against.
## What actually changes when k is no longer tiny Every comfortable claim about selection assumes `k << n`. Track each one as `k` grows toward `n`: **The retained-candidate scan.** Time O(n log k) — but `log k` approaches `log n`, so the bound converges on the full sort's O(n log n). Space O(k) approaches O(n), so the memory argument that justified it disappears. Its per-record work is a comparison plus an occasional sift, with scattered memory access; a good library sort over a contiguous buffer has excellent locality. Long before the asymptotics converge, the measured curves cross. **Quickselect.** Expected O(n) *independent of k* — the partition-and-narrow structure does not care where rank `k` sits. This is the region where quickselect genuinely shines, e.g. "give me the cheaper half of the catalog." The costs do not go away: it permutes the caller's buffer, it needs the whole input resident, and its worst case under naive pivots is O(n^2). It also hands back an unordered prefix. **The full sort.** O(n log n) regardless of `k`, and it is the only option that delivers a ranked result with no follow-up work. When `k` is large, the post-selection ordering step the others need — O(k log k) — stops being negligible and starts approaching the sort you were avoiding. ## The crossover is measured, not derived There is no universal `k/n` threshold. It depends on record size, comparator cost, memory hierarchy, and how well the sort exploits pre-existing order — an adaptive merge-based sort runs close to O(n) on already-ordered runs, which a price export very often is. The defensible engineering answer is to benchmark the two or three candidates against real records at several `k` values and pick a threshold from the curves, then keep the benchmark so the threshold can be re-derived when hardware or data changes. Presenting a crossover as a derived constant is the tell of someone who reasoned only on paper. ## The part that makes it a leadership call The technical answer is only half of it. A tunable `k` is a promise to the user that any `k` is supported, and it lands in code somebody else maintains: - **How many code paths?** Three tuned strategies behind one function is three times the surface for a subtle bug, and the bug will be in the branch nobody exercises. Two paths with an explicit, named threshold is usually the right trade; a single path is better still if the profile allows it. - **What does the interface promise?** If the result is documented as "the k cheapest, in price order," you have committed to the ordering step in every path — and made the sort branch strictly simpler than the alternatives at large `k`. If it is documented as "the set of the k cheapest, unordered," you have kept the cheap paths but pushed work onto callers who will each re-sort it, badly. - **What is the supported range?** A tunable that accepts k = n implies the service can return 10 million records. Memory ceilings, response sizes and timeouts all argue for a published maximum, pagination, or an asynchronous export path — that is a product boundary, not an algorithm choice. - **Which risk do you accept?** In a per-request path, an expected-linear method with a quadratic tail trades a better median for a worse tail. If the SLA is written on the tail, the bounded method wins even though it loses on average. In an offline batch, a rare slow run is cheap and the average is what you are paying for. ## A defensible recommendation 1. One public entry point taking `k`, documented as returning the k cheapest **in price order** — because that is what the product will ask for eventually anyway, and it makes the branches comparable. 2. Below a measured crossover, the single-pass retained-candidate scan plus an O(k log k) ordering of the result; above it, sort and slice. 3. A published maximum `k`, with anything beyond it served by pagination or an offline export. 4. Quickselect only if profiling on real traffic shows a specific mid-range of `k` where it pays for its preconditions — and only where the buffer is ours to permute. 5. The benchmark checked in next to the threshold, so the number can be re-justified rather than inherited. ## The register that lands "I would not ship three strategies. As k grows the heap's advantage disappears — log k becomes log n and the state becomes O(n) — so I would measure the crossover on real records, run the scan below it and a plain sort above it, publish a maximum k with pagination beyond that, and document that the result comes back ranked. Quickselect earns a place only if the profile shows a k range where it wins and the buffer is ours to disturb."
- Why does the retained-candidate scan lose its edge before the asymptotics converge?Because the constants move first. Its per-record work is a comparison plus an occasional sift over a k-sized structure with scattered access, while a library sort works over a contiguous buffer with excellent locality and can exploit pre-existing runs. So the measured curves cross at a k far below the point where log k is arithmetically close to log n.
- Where does quickselect look best in this range, and what still stops you defaulting to it?It is strongest at mid-to-large k, since expected O(n) is independent of k while the scan's advantage decays. What stops it as a default is unchanged: it permutes the caller's buffer, needs the whole input resident, carries an O(n^2) tail under naive pivots, and returns an unordered prefix that must still be ordered for display.
- How do you justify the threshold to a reviewer who wants a formula?With a benchmark, not algebra. Record size, comparator cost, memory hierarchy and pre-existing order all move the crossover, so the honest artifact is a measurement over real records at several k values, checked in beside the constant. The formula answer is worse because it looks authoritative while being unfalsifiable on this hardware.
- The tunable accepts k equal to the whole catalog. Is that an algorithm problem?No — it is an interface problem. Returning 10 million records has memory, serialization and timeout consequences no selection strategy fixes. The right response is a published maximum k with pagination or an asynchronous export beyond it, agreed with the product owner, so the algorithm choice only has to cover the range you actually support.
saying these in an interview costs you the question
- Keeping the O(n log k) argument when k approaches n
- Deriving a universal crossover constant instead of measuring
- Shipping three tuned strategies behind one call
- Ignoring that large k makes the ordering step significant
- Treating an unbounded k as purely an algorithm decision
- Optimizing the median when the SLA is written on the tail