Why can quicksort's recursion depth reach O(n), and how do you cap it at O(log n)?
answer
- In-place says nothing about the call stack
- Each pending call is live memory
- Depth equals the number of nested levels
- You only need to recurse on one side
- Push the smaller half, loop on the bigger
basics
~20 sQuicksort allocates no proportional buffer, but call frames are space: degenerate splits nest one call per element, so depth reaches n and the stack can be exhausted. Recursing into the smaller side and looping on the larger caps depth at log2 n.
solid answer
~50 sQuicksort is in-place in that it rearranges elements within the range and allocates no proportional buffer, but its **call stack is part of its space cost**. Depth equals the number of nested partition levels, so the same degenerate splits that cause `O(n^2)` time also produce `O(n)` frames — a worse failure, because exhausting the stack crashes the process rather than merely delaying it. The classic trigger is input that arrives already ordered against a fixed-position pivot. The fix is structural, not statistical: after partitioning, **recurse into the smaller side and loop on the larger** by reassigning the range bounds. Since the recursed side holds at most half the current range, depth is bounded by `log2 n` for any pivot quality whatsoever. Time is unaffected — a bad pivot still costs `O(n^2)` — but the crash becomes impossible.
code
pseudocode · 9 linesquicksort(a, lo, hi):
while lo < hi:
p = partition(a, lo, hi)
if p - lo < hi - p:
quicksort(a, lo, p - 1) // smaller side: at most half
lo = p + 1 // larger side: loop, no frame
else:
quicksort(a, p + 1, hi)
hi = p - 1go deeper
Know that each pending recursive call occupies stack memory, so a recursion that nests n deep uses memory proportional to n even when the algorithm copies no data.
Explain why depth equals level count, why degenerate splits make it n rather than log n, and why the smaller side is at most half the range — that halving argument is the whole proof of the log2 n bound.
Show the operational instinct: recognise a crash of thousands of identical frames as a depth failure, connect it to an upstream change in input ordering, and separate the space fix from the still-open time problem.
Own the preference for unconditional guarantees over probabilistic ones in shared code paths whose inputs you cannot see, and weigh a cheap structural bound against hardening that only reduces the likelihood of the bad case.
## The incident A nightly job sorts a few million rows in memory and has done so for a year. One morning it dies immediately with a stack exhaustion, not a timeout. Nothing in the sorting code changed. What changed is upstream: another team added an ordering step to the stage that feeds it, so rows now arrive already sorted by the very key this job sorts on. The sort uses a fixed first-element pivot. Every partition picks the minimum of its range, every split is `1 : m-1`, and the recursion nests once per row. That is the double failure worth internalising: degenerate splits cost `O(n^2)` **time** and `O(n)` **stack**, and the stack limit is usually hit long before anyone notices the time. The job did not get slow — it stopped existing. ## Recursion depth is space "In-place" is a statement about **auxiliary data structures**: quicksort rearranges elements within the range and needs no buffer proportional to `n`, unlike schemes that merge into a second array. It is not a statement about total memory. Every pending recursive call holds a frame — the range bounds, the pivot index, a return address — and those frames are live memory that grows with depth. So quicksort's space complexity is `O(depth)`: - Balanced splits: depth `~ log2 n`, so `O(log n)` space. For a few million elements that is around 20 frames — nothing. - Degenerate splits: depth `~ n`, so `O(n)` space. For a few million elements that is millions of frames, and the stack is a fixed, comparatively small region. It runs out. This is the general rule, not a quicksort quirk: **for any recursive algorithm, the stack is part of its space complexity**, and a candidate who reports space without counting depth has reported the wrong number. ## The structural fix After partitioning, you hold two subranges. The insight is that you do not need a recursive call for both. Recursion in the *tail* position can be turned into a loop by reassigning the range bounds and going round again, which consumes no frame at all. And you get to choose **which** side is the loop. ``` while lo < hi: p = partition(a, lo, hi) if the left side is smaller: recurse on the left; then set lo = p + 1 // continue with the right else: recurse on the right; then set hi = p - 1 // continue with the left ``` Always recurse into the **smaller** side. Now every frame that is actually pushed covers a range of at most half its parent's size, because the smaller of two pieces of an `m`-element range is at most `m/2`. Halving from `n` can happen at most `log2 n` times, so **depth <= log2 n regardless of how badly the pivot behaves**. The pathological `1 : m-1` split now pushes a frame for a one-element range and loops on the rest — the deep chain of frames simply never forms. ## What the fix does and does not buy - **It bounds space, not time.** A first-element pivot on ordered input still does `O(n^2)` comparisons. You converted a crash into a slowdown; you did not make the sort fast. Anyone who offers this as the cure for the quadratic worst case has confused the two axes. - **It costs nothing.** One size comparison per partition, and fewer calls than before on every input, balanced or not. - **It is unconditional.** It does not rely on the pivot being good, on the data being unordered, or on any distributional assumption. That property — a hard guarantee rather than a probabilistic one — is what makes it the right first move on a shared sorting routine, where you cannot see who will feed it what. - **The order matters, and only in one direction.** Recursing into the *larger* side and looping on the smaller inverts the argument: the pushed frame can cover `m-1` elements, and the depth is back to `O(n)`. Writing the comparison backwards silently removes the entire guarantee, which makes this a detail worth a review comment. ## How the failure presents in production A time blowup shows up as a latency graph bending upward, a task overrunning its window, a queue growing — you get warning, and you can profile a running process. A depth blowup shows up as an immediate crash with a stack trace thousands of frames deep, all of them the same function. That trace is the tell: an enormous run of identical frames means unbounded recursion depth, and for a sort it means the partitions degenerated. The two questions worth asking next are what the input's ordering looks like now versus before, and whether the pivot rule reads a fixed position. ## Framing the answer Say four things and you have covered it: recursion depth is space; degenerate partitions make it `O(n)`; recursing into the smaller half and looping on the other caps it at `log2 n` for any pivot; and that bound is on space only, so the time worst case still needs a separate answer.
- Does recursing into the smaller side first fix quicksort's O(n^2) worst-case time?No. It bounds the depth of the recursion, which is a space guarantee. The comparison count is unchanged: a fixed-position pivot on ordered input still does about n^2/2 comparisons, now inside a loop instead of a deep call chain. You have converted a crash into a slowdown, which is progress, but the time worst case needs a separate answer about pivot choice.
- What happens if the comparison is written backwards, so the larger side is recursed into?The guarantee disappears entirely. The pushed frame can then cover m-1 elements, so degenerate splits rebuild the O(n) chain exactly as before, while the code still looks like it has the optimisation. It is a silent regression: correct output, correct performance on balanced input, and a crash on the one input class the change was meant to survive.
- How would you recognise this failure from a crash report alone?By the shape of the stack trace: thousands of identical frames for the same partition function, rather than a varied call chain. That signature means recursion depth, not memory volume, was exhausted. From there you compare the input's ordering against previous runs and check whether the pivot rule reads a fixed position such as the first or last element.
You can only hold so many half-finished tasks on your desk. If you always delegate the small piece and keep working the big one yourself, the pile of half-finished tasks never grows past a handful, however lopsided the work turns out to be.
saying these in an interview costs you the question
- Says quicksort uses O(1) space because it is in-place
- Reports space complexity without counting recursion depth
- Claims smaller-side-first also fixes the quadratic time bound
- Thinks a deep recursion only makes the sort slower
- Recurses on the larger side and still claims a log n depth bound
- Assumes balanced-input depth of log n holds for every input