What is taught wrongly

In place is a claim, and it is usually wrong about quicksort

Heapsort holds one slot at its peak. Quicksort holds twenty-two at n = 4,096 on random input and 4,097 on a sorted one. Merge sort holds 4,110. All three are described with the same two words, one of the three descriptions is false, and the false one is the algorithm the phrase is most often attached to.

“Quicksort sorts in place; merge sort does not.” It is one of the first things anybody learns about the pair, it is the reason quicksort is the default in most standard libraries, and it is the kind of statement that sounds like a fact about the algorithm rather than a claim about a quantity.

Once the quantity is counted, it is a claim, and it fails.

Peak auxiliary space at n=4,096n = 4{,}096 — the most slots held at any one moment, counting scratch arrays and stack frames together:

algorithm random input sorted input
heapsort 1 1
Shellsort 1 1
insertion sort 1 1
quicksort, median of three 22 14
quicksort, random pivot 28 32
quicksort, first-element pivot 29 4,097
merge sort 4,110 4,110

Three of those rows are Θ(1)\Theta(1). Three are Θ(logn)\Theta(\log n). One is Θ(n)\Theta(n) and is in the same column as merge sort, and it is the algorithm the phrase “in place” is most often used about.

Peak auxiliary space, already sorted input, n = 4,096The largest number of slots live at once — scratch buffers plus stack frames — for each sort. The axis is logarithmic because the range is: 5 of these hold one slot at their peak, 2 hold a stack of about log₂ n frames, and the rest hold a second copy of the array. All of them are routinely described with the same two words.peak slots held at once, logarithmic18645124096Insertion sort11Selection sort11Bubble sort11Heapsort11Shellsort11Quicksort, median of three14log nQuicksort, random pivot32log nQuicksort, first-element4,097nMerge sort with a cutoff4,106nMerge sort4,110nn = 4,096, already sorted inputone slot = one array element or one stack frame
Fig. 1 Peak auxiliary space on sorted input, at n = 4,096, on a logarithmic axis. First-element quicksort’s bar reaches merge sort’s. On this input the algorithm chosen for not needing a second array needs a second array’s worth of stack, and the two are within thirteen slots of each other.

Where the space goes

Quicksort allocates nothing. There is no buffer, no scratch array, no temporary copy; the partition swaps elements within the input and that is the whole of it. By any accounting that watches the array, it uses no extra memory at all.

It recurses. Each level of recursion is a stack frame holding the bounds of a subproblem, and those frames are live simultaneously — the outer call cannot return until the inner ones have. So the auxiliary space is the depth of the recursion, which is the height of the partition tree.

On random input the partitions are roughly balanced, the tree has depth about log2n\log_2 n, and 22 frames at n=4,096n = 4{,}096 is a little above log24096=12\log_2 4096 = 12 because the balance is only roughly right. That is genuinely small and calling it negligible is fair.

On sorted input with a first-element pivot, every partition puts one element on one side and n1n-1 on the other. The tree is a path, its depth is nn, and the measurement is 4,097 frames for 4,096 elements.

The extra memory has not appeared from nowhere and it is not an artefact of the instrument. It is the call stack, it is proportional to the input, and the reason nobody counts it is that no counter watching the array can see it.

The two failures are the same failure

Everybody knows first-element quicksort is quadratic on sorted input. It is 8,386,560 comparisons at n=4,096n = 4{,}096, against 57,431 on random input — a factor of 146, and it is the textbook trap.

The space failure is the same event, described in the other resource. An unbalanced partition tree is simultaneously the reason the comparison count is n2/2n^2/2 and the reason the depth is nn: one is the number of nodes in the tree and the other is its height.

So the two are not independent risks to weigh separately. They arrive together, they have one cause, and every fix for one is a fix for the other. What differs is the failure mode, and this is the part worth having:

The time failure degrades. A quadratic sort of 4,096 elements takes longer. It finishes. Somebody notices a slow request.

The space failure stops. A recursion 4,096 deep is fine; one seven thousand deep, on the machine this site is built on, is not. The program does not slow down — it raises a stack-overflow error and dies, at a size that depends on the engine, on the frame layout and on how much stack the caller had already used.

One of those is a performance problem and one is an availability problem, and the space axis is the only one that shows the second exists.

Recursion depth on already sorted input, against a stack of 512 framesStack frames held at the deepest point, on logarithmic axes, with a stated stack drawn across. Quicksort with a first-element pivot recurses once per element on this input and crosses the line; the other rules stay logarithmic and never approach it. The algorithm that crosses is the one universally described as sorting in place. The stack is a stated model, because a real one runs out at a size that depends on the engine and is not reproducible between runs.10010³1010010³nstack framesa stack of 512 framespivot: first-elementpivot: median of threepivot: random pivotMerge sortalready sorted input, n from 64 to 4,096one frame charged as one slot
Fig. 2 Recursion depth against n on sorted input, with a stated stack of 512 frames drawn across. Three of these lines stay logarithmic and never approach it. One crosses it before n = 1,000 and keeps going. Nothing about the comparison count says which of these algorithms is about to stop working.

The real stack, and why the figure does not use it

The interesting version of this is not the modelled stack in that figure but the actual one, and it is worth reporting exactly because it is the reason the figure uses a model instead.

Running first-element quicksort on a sorted array on the machine that builds this site: it succeeds at n=7,000n = 7{,}000 and fails at n=7,015n = 7{,}015 with a stack-overflow error. Bisecting for the precise threshold is possible, and the precise threshold is not reproducible — running the same size again after other work has been done gives a different answer, because the available stack depends on how much of it the caller has already spent.

That is a real measurement of a real failure and it cannot go in a caption. A number that changes between builds is a number the reader’s copy of the page does not have, which is the same reason no figure here uses Math.random().

So the figures use a stated stack — a limit in frames, printed on the figure, exactly as the cache model prints its line size. The gate then requires the modelled stack to bite: with 256 frames and 2,048 sorted elements, the first-element pivot must fail and median-of-three must not. A limit that rejects everything proves as little as one that rejects nothing, so both halves are checked.

A space claim the fit refused

Every algorithm here declares a space class per input, and the classes are put through the same ratio test the comparison counts get. Most of them pass comfortably — heapsort’s peak ÷ 1 has a spread of 1.00 across nn from 128 to 32,768, merge sort’s peak ÷ nn has a spread of 1.07.

Median-of-three quicksort declared Θ(logn)\Theta(\log n) on nearly-sorted input. The fit refused it, and the measurement is worth showing:

n peak frames
128 9
256 11
512 14
1,024 13
2,048 23
4,096 23
8,192 23
16,384 129
32,768 125

Logarithmic up to eight thousand, and then it is not. The spread of peak ÷ logn\log n across the whole range is 7.17 and the class is not granted; the claim was removed from the table rather than kept with a caveat.

What causes the jump is the interaction between the pivot rule and the input generator. Nearly-sorted input here is a sorted array with n/20n/20 adjacent transpositions, so the median of the first, middle and last elements is almost always an excellent pivot — until a subproblem happens to be short enough and disturbed enough that all three samples come from the same end of it, at which point that branch descends much further than the others. The depth is the maximum over branches, so one bad branch sets the number, and the probability of at least one bad branch grows with the number of branches.

This is the third declared class this site has withdrawn on measurement, after bubble sort on nearly sorted input and the hybrid on reversed input, and it is the first one in the space column. The pattern in all three is identical: the class is right about the average or about the asymptotics, the measurement is of the maximum over a finite range, and the two are different quantities.

The in-place merge sorts, and what they cost

If merge sort’s only defect is its buffer, the obvious question is whether the buffer can be removed. It can, and the answer is instructive because it is a trade rather than a fix.

Rotation-based in-place merge. Merge two adjacent sorted runs by repeatedly rotating blocks into place rather than copying into scratch. Auxiliary space becomes O(1)O(1); the comparison count is unchanged; the data movement rises from Θ(nlogn)\Theta(n \log n) to Θ(nlog2n)\Theta(n \log^2 n), because a rotation moves elements that a copy would have left alone. The algorithm has bought the space axis by giving up on the traffic axis, which is the count that usually decides when elements are anything larger than an integer.

Block merge, or “grail” sorting. Steal a block of n\sqrt n elements from the array itself to use as scratch, merge in blocks, then sort the stolen block back into place. Auxiliary space is O(1)O(1) genuinely, the movement stays Θ(nlogn)\Theta(n\log n), and the constant is several times plain merge sort’s. It is the sophisticated answer and it is a few hundred lines where merge sort is twenty.

A rotating buffer of n\sqrt n. The intermediate point: Θ(n)\Theta(\sqrt n) auxiliary space, movement close to the plain algorithm’s. At n=4,096n = 4{,}096 that is 64 slots rather than 4,096 — a factor of 64 saved for a modest constant.

Set those beside the algorithms already measured and the picture is a curve rather than two options:

approach peak slots at n = 4,096 what it costs
merge sort, one buffer 4,110 nothing
merge sort, √n buffer about 78 a modest constant on movement
block merge 1 several times the constant, and the code
rotation merge 1 a log factor on movement
quicksort, smaller side first about 12 a worse comparison count and no stability
heapsort 1 double the comparisons, and bad locality

Two of those rows are measured here — plain merge sort at 4,110 and heapsort at 1 — and the rest are arithmetic from the published designs, since this site implements neither the block merge nor the rotation merge. They are in the table because the shape is the point and the shape does not depend on the exact constants.

Nothing on that list is free, and the two that reach one slot pay in the two different currencies the site measures. Which is the whole argument of the space field compressed into a table: auxiliary space is a resource that trades against the others, so the algorithms that need none of it are paying for that somewhere, and the payment is measurable.

It is also why the phrase is doing damage rather than merely being imprecise. Calling quicksort in-place and merge sort not in-place collapses that table into two categories, puts quicksort in the wrong one, and hides the intermediate options entirely.

What “in place” ought to mean

Three definitions are in circulation and they disagree about quicksort, which is why the phrase is doing so little work.

O(1)O(1) auxiliary space. The strict reading. Heapsort qualifies, Shellsort qualifies, insertion and selection and bubble sort qualify. Quicksort does not, on any input, because logn\log n is not O(1)O(1).

O(logn)O(\log n) auxiliary space. The reading that lets quicksort in, and the one most textbooks are implicitly using. It is defensible — 22 slots for 4,096 elements is not a memory problem — and it has a hole in it, which is that quicksort’s O(logn)O(\log n) is an average-case statement and the phrase is used as though it were a guarantee.

No allocation. The reading a systems programmer often means: the algorithm calls no allocator, so it cannot fail on memory pressure and cannot fragment the heap. Quicksort qualifies and merge sort does not, and this reading is genuinely useful and genuinely different from the other two — and it says nothing at all about the stack, which is where quicksort’s failure lives.

None of the three is wrong. The problem is that they are three, they select different sets of algorithms, and the phrase is used as though there were one. A number does not have that problem: quicksort’s peak is 22 on random input, 4,097 on sorted input with the wrong pivot rule, and 14 with a good one.

Peak auxiliary space, random input, n = 4,096The largest number of slots live at once — scratch buffers plus stack frames — for each sort. The axis is logarithmic because the range is: 5 of these hold one slot at their peak, 3 hold a stack of about log₂ n frames, and the rest hold a second copy of the array. All of them are routinely described with the same two words.peak slots held at once, logarithmic18645124096Insertion sort11Selection sort11Bubble sort11Heapsort11Shellsort11Quicksort, median of three22log nQuicksort, random pivot28log nQuicksort, first-element29log nMerge sort with a cutoff4,106nMerge sort4,110nn = 4,096, random inputone slot = one array element or one stack frame
Fig. 3 The same measurement on random input. Every quicksort row has collapsed to about twenty slots and the picture looks the way the folklore describes. The difference between this figure and the one at the top of the essay is the input, and only one of the two is ever the basis for the claim.

What a language does not do about it

The smaller-side fix is described below as a two-line change with no cost, and it is worth noticing what happens without it in a language that eliminates tail calls — because the answer is nothing, and the reason is instructive.

Quicksort’s second recursive call is in tail position: nothing happens after it returns. So a runtime with proper tail calls does not allocate a frame for it, and the recursion depth becomes the length of the chain of first calls rather than the depth of the whole tree.

That does not fix anything on its own. If the first call is on the larger side, the chain of first calls can be nn long, and the depth is nn with or without tail-call elimination. Tail calls remove the frames of the branch that was already going to be shallow if the order were right, and leave the deep branch exactly where it was.

So the two changes have to be made together, and the one that matters is the ordering rather than the language feature. Recursing on the smaller side first bounds the first-call chain at log2n\log_2 n by itself, whether or not the second call costs a frame; tail-call elimination then removes the remaining frames and makes the space genuinely constant rather than logarithmic.

That is worth carrying past quicksort, because the pattern covers every algorithm that divides into two parts and handles them in sequence. Quickselect, a binary-tree traversal, the divide step of a linear-space alignment, a range query over an interval tree: in each case the depth is set by which part is descended into first, the smaller-first rule bounds it at a logarithm on every input, and the rule costs a comparison. The stack nobody counts is where the resource is measured; this is the one-line fix that applies to all of them.

The phrase also hides stability

There is a second property the two words collapse, and it is the one that decides which sort a library uses for objects.

A stable sort keeps equal elements in their original relative order, which is what makes sorting by one key and then another produce a sensible result. Merge sort is stable. Heapsort is not, quicksort is not, Shellsort is not — and those are precisely the algorithms at the low-space end of the table.

That is not a coincidence and it is close to a theorem in practice: the known comparison sorts that are simultaneously stable and O(1)O(1) in auxiliary space are the block-merge constructions in the list above, and they are several hundred lines with large constants. Everything simple is stable or frugal with space, not both.

So “sorts in place” is being used to make a decision that has three axes in it — space, stability, and how much code somebody is prepared to maintain — and it names one of the three. A library that has to sort objects picks the stable algorithm and pays the buffer, which is exactly what Java does and exactly what C++'s two separate entry points make explicit; a library sorting integers picks the frugal one because stability is meaningless when equal elements are indistinguishable.

The phrase’s real failure is not that it is imprecise about quicksort’s space. It is that it presents a three-way engineering decision as a binary property of algorithms, and the property it names is the one it gets wrong.

What a library actually promises

Standard libraries are more careful about this than textbooks are, and reading what they promise is instructive.

C++'s std::sort guarantees O(nlogn)O(n\log n) comparisons and says nothing about space, and its typical implementation is an introsort whose recursion is bounded at 2log2n2\log_2 n. C++'s std::stable_sort explicitly promises to try for a buffer of nn and to degrade to O(nlog2n)O(n\log^2 n) comparisons if it cannot get one — a published, documented trade between the two axes, stated as such.

Both of those are more precise than “sorts in place”, and neither uses the phrase. The vocabulary problem this essay is about is a teaching problem rather than an engineering one: the people who had to write the guarantee down found that the phrase would not do the job.

The measurement is a peak over one run, so it is worth taking on three inputs and two sizes before any of it is quoted.

Peak auxiliary space, random input, n = 8,192The largest number of slots live at once — scratch buffers plus stack frames — for each sort. The axis is logarithmic because the range is: 5 of these hold one slot at their peak, 3 hold a stack of about log₂ n frames, and the rest hold a second copy of the array. All of them are routinely described with the same two words.peak slots held at once, logarithmic18645124096Insertion sort11Selection sort11Bubble sort11Heapsort11Shellsort11Quicksort, median of three26log nQuicksort, random pivot29log nQuicksort, first-element35log nMerge sort with a cutoff8,203nMerge sort8,207nn = 8,192, random inputone slot = one array element or one stack frame
Fig. 4 Twice the array on random input. Five of the sorts hold one slot at their peak, three hold a stack of about log2n\log_2 n frames, and the rest hold a second copy of the array — a range wide enough to need a logarithmic axis, and all of them are routinely described with the same two words.

The input is the second dial, and it is the one that separates a peak fixed by the algorithm from a peak the data chooses.

Peak auxiliary space, already sorted input, n = 4,096The largest number of slots live at once — scratch buffers plus stack frames — for each sort. The axis is logarithmic because the range is: 5 of these hold one slot at their peak, 2 hold a stack of about log₂ n frames, and the rest hold a second copy of the array. All of them are routinely described with the same two words.peak slots held at once, logarithmic18645124096Insertion sort11Selection sort11Bubble sort11Heapsort11Shellsort11Quicksort, median of three14log nQuicksort, random pivot32log nQuicksort, first-element4,097nMerge sort with a cutoff4,106nMerge sort4,110nn = 4,096, already sorted inputone slot = one array element or one stack frame
Fig. 5 Already sorted input. Now only two of them hold a logarithmic stack rather than three, because a sort whose recursion depth depends on the data has a peak that depends on the data — which is the property “in place” is silent about.

One more input, because sorted and reversed are the two ends a naive pivot rule is worst on and they are not the same end.

Peak auxiliary space, reversed input, n = 4,096The largest number of slots live at once — scratch buffers plus stack frames — for each sort. The axis is logarithmic because the range is: 5 of these hold one slot at their peak, 2 hold a stack of about log₂ n frames, and the rest hold a second copy of the array. All of them are routinely described with the same two words.peak slots held at once, logarithmic18645124096Insertion sort11Selection sort11Bubble sort11Heapsort11Shellsort11Quicksort, random pivot28log nQuicksort, median of three29log nQuicksort, first-element4,097nMerge sort with a cutoff4,106nMerge sort4,110nn = 4,096, reversed inputone slot = one array element or one stack frame
Fig. 6 And reversed input, the case a quicksort with a naive pivot is worst on. The peak is a property of the run and the phrase is a property of the algorithm, and this is the plate where the two come apart.

What to do about it, and what it costs

The fixes are all well known and it is worth putting the space cost of each beside the time cost, because they are not the same list.

Randomise the pivot. Removes the input’s ability to choose the bad case, in both resources at once: peak 28 on random input and 32 on sorted input, which is what randomisation buys showing up in the space column. Costs a random number per partition.

Recurse on the smaller side, loop on the larger. This is the one that actually bounds the space, and it is a two-line change with no cost at all. Since the smaller side has at most n/2n/2 elements, the recursion depth is at most log2n\log_2 n on every input, including the pathological ones. The comparison count is unchanged — the algorithm does exactly the same work in the same order — and the Θ(n)\Theta(n) row in the first table becomes Θ(logn)\Theta(\log n).

That it is free, well known, and absent from most presentations of the algorithm is the strongest argument in this essay. A worst case that costs nothing to remove is a worst case people would remove if they knew it was there, and the reason they do not know is that the resource it lives in is not the one anybody counts.

Switch to heapsort when the recursion gets deep. Introsort’s answer: bound the depth at 2log2n2\log_2 n and fall back to an algorithm with a worst-case guarantee in both resources. This is what most standard libraries do and it is a decision about the space axis at least as much as the time one.

Comparisons against peak auxiliary space, n = 8,192One point per sort, both axes logarithmic, cheapest and smallest towards the bottom left. The line joins the Pareto frontier — the 5 algorithms that nothing else beats on both counts at once. The 5 points off it are dominated, and being dominated is a stronger statement than being slower: there is no weighting of these two costs under which they are the right choice. This is the honest answer to "which sorting algorithm", and it is a shape rather than a name.10⁵10⁶10⁷11010010³10⁴comparisonspeak auxiliary slotsInsertion sortSelection sortBubble sortMerge sortHeapsortQuicksortQuicksortQuicksortShellsortMerge sort with a cutoffn = 8,192, random input5 on the frontier, 5 dominated
Fig. 7 Both resources at once. Quicksort’s points sit low on the space axis and in the middle of the comparison axis; merge sort’s sit far to the left and at the top. The frontier is the set of algorithms nothing beats on both, and the reason it has several members is that these two costs are not substitutes for one another.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 10 that link here.

The objects this essay names

Each one links to every other essay that touches it.

Auxiliary spaceComparison countGuaranteeIn placePartitionPivotQuicksortRecursion depthRotationWorst case