A report loop sorts the whole transaction set on every row — what is the total cost?
answer
- per-iteration costs multiply, never add
- what does the call itself cost?
- does the call's input depend on the loop?
- n passes times m log m per pass
- loop-invariant work belongs above the loop
basics
~20 sEvery iteration pays for the sort, so n rows over m transactions cost O(n · m log m). The sort's input never changes here, so hoisting it above the loop drops the job to O(m log m + n).
solid answer
~50 sCosts inside a loop multiply, they do not add. If the loop runs `n` times and each pass calls a comparison sort over `m` transactions, the total is O(n · m log m); when the report has one row per transaction (`n = m`) that is O(n^2 log n) for work whose useful part is O(n log n). The giveaway is that the sort's input never changes inside the loop — it is loop-invariant, so the result can be computed once above the loop and reused, giving O(m log m + n). Do not let an adaptive sort talk you out of the fix: a stable adaptive merge-insertion hybrid such as Timsort recognises already-ordered input and runs in O(m) on it, which lowers the per-pass cost but is still paid every pass, so the total is O(n · m). Cheaper per pass is not the same as not paying.
code
pseudocode · 10 linesfor i in 0..n-1:
row = rows[i]
ranked = sort(transactions) // input never changes
row.midpoint = ranked[length(ranked) / 2]
row.top = ranked[length(ranked) - 1]
emit(row)
// hoisted version:
// ranked = sort(transactions)
// for i in 0..n-1: ... read ranked ...go deeper
Be ready to say that a call inside a loop is paid on every iteration, so n passes over an O(m log m) sort cost O(n · m log m). Naming the fix — sort once, above the loop — is the expected follow-through.
Explain the multiplication rule and the loop-invariance test out loud: does the call's input depend on the loop variable? Then give the hoisted cost as O(m log m + n) and note that comparison sorts cannot beat m log m in the worst case.
Handle the case where hoisting is impossible: sort once by the dominant key and read ranges per row, keep an incremental structure, or use selection when only one position is needed. Talk about how you would demonstrate the regression with a ten-times-volume run.
Frame the recurring risk rather than the single line: which report jobs sit on an unbounded input, what the growth curve implies for the batch window, and whether the team needs a review habit or a load check that catches loop-invariant heavy calls before they reach production.
## The scene A monthly report generator walks the rows it must emit, and for each row it needs a ranking over the month's transactions — the middle value, the top few, a percentile. The straightforward code puts the ranking where it is needed: ``` for i in 0..n-1: row = rows[i] ranked = sort(transactions) // same input on every pass row.midpoint = ranked[length(ranked) / 2] ... ``` One loop, a handful of statements. It reads as linear. It is not. ## The rule: per-iteration costs multiply The cost of a loop is (number of iterations) × (cost of one iteration), and the cost of one iteration includes every call it makes. A comparison sort over `m` items is O(m log m) — and that bound is not a pessimistic guess, it is close to the floor, because any sort that only compares elements needs Omega(m log m) comparisons in the worst case. So: `total = n × O(m log m) = O(n · m log m)` The two classic errors here are (1) *adding* instead of multiplying — "O(n + m log m), the loop plus the sort" — which is what you get if you think of the sort as happening once, and (2) assuming a repeated call on unchanged input is somehow cached. Nothing memoises a library call for you; each pass re-does the work. If the report emits one row per transaction, `n = m` and the expression collapses to **O(n^2 log n)**. That is worse than quadratic, for a job whose honest content is one sort plus one linear walk. ## Why this one is easy to miss The hidden cost here is different in flavour from a linear membership scan. There, the searched collection grew, so the badness accumulated visibly. Here every pass costs exactly the same, which is precisely why nobody notices: the profile shows a uniform hot line, the code looks regular, and small test fixtures finish instantly. The failure only shows up as the month gets busier, and it shows up steeply — double the transactions and the runtime goes up more than fourfold. ## The fix: hoist the loop-invariant work Ask of every expensive call in a loop body: *does its input depend on the loop variable?* If not, the call is **loop-invariant** and belongs above the loop. ``` ranked = sort(transactions) // once mid = ranked[length(ranked) / 2] for i in 0..n-1: rows[i].midpoint = mid ... ``` Cost: O(m log m) once, plus O(n) for the loop — written as O(m log m + n). This is a strictly better algorithm, not a micro-optimisation: it changes the growth class. When the sort's input *does* depend on the row, hoisting is unavailable and you need a different lever: sort once by the dominant key and derive each row's view from the sorted order; maintain a heap or an order-statistics structure incrementally; or, if you only need the k-th element rather than the whole order, use a selection algorithm, which is O(m) expected rather than O(m log m). Needing one element out of an ordered arrangement is a strong hint you never needed the arrangement. ## The adaptivity trap A sharp candidate raises: many mainstream standard-library sorts are adaptive — a stable merge-insertion hybrid like Timsort detects existing ordered runs and finishes an already-sorted input in O(m) — so after the first pass, isn't the rest free? No. It is *cheaper*, not free. The per-pass cost falls from O(m log m) to O(m), so the total falls from O(n · m log m) to O(n · m): still quadratic when n and m grow together. Adaptivity lowers a constant-ish factor on the wrong algorithm; hoisting removes the repetition. The same reasoning applies to any superlinear helper called per iteration — building a lookup structure, re-deriving an aggregate, re-parsing a configuration. Read the loop body as a bill of materials, and multiply. ## Interviewer variations to expect - "The sort is on a slice whose length shrinks each pass." Then sum the per-pass costs instead of multiplying a constant: `sum over k of k log k` is O(m^2 log m) — still quadratic-and-a-log. - "What if the sort is not comparison-based?" A counting or radix sort over bounded integer keys is O(m + range) per pass, escaping the comparison lower bound, but again it is per pass; the multiplication survives. - "Is the space complexity affected?" A merge-based sort needs O(m) auxiliary space, but it is reclaimed each pass, so peak extra space stays O(m) rather than n times that — allocation churn, however, is real and repeated.
- Each row needs only the middle transaction, never the full order. Does that change your answer?Yes. Wanting one position rather than the whole arrangement points at a selection algorithm, which finds the k-th smallest in O(m) expected time without ordering the rest. Per pass that beats O(m log m), and combined with hoisting — the value is the same for every row here — you compute it once in O(m) and the loop is O(n).
- The sort input differs per row, so it cannot be hoisted. What now?Restructure rather than repeat. Sort once by the dominant key and let each row read a range of that single ordered arrangement, or maintain the answer incrementally with a heap or running aggregate as you sweep the rows. The goal is to pay the ordering cost once and amortise it across rows instead of paying a full sort per row.
- How would you convince a reviewer this is a real problem and not premature optimisation?Show the growth, not the opinion: time the job at today's volume and at ten times it. A hoistable sort in a loop turns a fourfold data increase into a sixteenfold-plus runtime increase, which is visible in two runs. Pair that with the fact that the fix is smaller and simpler code, which removes the usual complexity-versus-clarity objection.
saying these in an interview costs you the question
- Adds the sort cost instead of multiplying it per iteration
- Assumes a repeated call on unchanged input is cached
- Calls the fix a micro-optimisation rather than a class change
- Thinks an adaptive sort makes repeated sorting free
- Sorts the whole set when only one position is needed