Sorting Algorithms: Bubble Sort, Merge Sort, Quicksort and the Quadratic Wall
Prerequisite:Fundamental Data Structures: Arrays, Linked Lists, Stacks and Queues
0. Key points
Section titled “0. Key points”- Bubble sort exchanges only adjacent elements. A single exchange removes exactly one “reversal of order”, so as long as there can be as many as reversals, there is no escaping . The cause of the slowness is not a crude implementation but the design itself: elements cannot be moved far.
- Merge sort splits the array in half, sorts each half recursively, and then merges the two sorted runs. The number of comparisons is at most and the running time is .
- Quicksort splits the array around a pivot. If the split is balanced the cost is , but if the pivot is taken from a fixed position (the last element, say), then on already sorted input every split degenerates into ” and ” and the cost falls to .
- If the pivot is chosen uniformly at random, the expected number of comparisons is at most . The worst case does not disappear, but it stops being determined by the input and becomes determined by the random choices, which makes it practically unreachable.
- Any sorting method that relies on comparisons alone needs, however cleverly it is arranged, at least comparisons in the worst case. Merge sort attains this bound up to a constant factor, and in that sense it has reached the point where “no further speedup is possible”.
1. Motivation: the wall between quadratic time and n log n
Section titled “1. Motivation: the wall between quadratic time and n log n”Sorting is one of the operations on which computers have spent the most time. In Volume 3 of The Art of Computer Programming, Knuth reports estimates from computer manufacturers of the day according to which more than 25% of running time was spent sorting. Even now, sorting sits at the core of index construction in databases, external joins, extraction of the top items, and deduplication.
Sorting is also the first subject in which the difference between complexity classes can be felt. Written naively it comes out as ; written a little more cleverly it comes out as . This gap is not a matter of constant factors.
Example 1.1(Sorting a million records)
Consider a machine performing elementary operations per second, sorting records.
A algorithm makes roughly
comparisons. Counting a comparison together with its attendant work as a single operation, this is seconds, that is, more than eight minutes.
A algorithm, since , makes roughly
comparisons, which takes about seconds. The estimate is crude and ignores constant factors, but the ratio is . It is the difference between “still running when the lunch break ends” and “finished instantly”. Moreover the ratio is about , so multiplying by ten to get gives : the gap widens by another factor of more than eight.
In this article we explain, in terms of a quantity called the number of inversions, why the naive methods are bound to , and then see how divide and conquer breaks that bond. At the end we prove that nothing based on comparisons can beat , and confirm that merge sort is asymptotically optimal. For the complexity notation itself we rely on Complexity and big-O notation (Definition 3.1[Complexity and Big-O Notation]), and for the underlying arrays, linked lists and heaps on Fundamental data structures (Definition 3.1[Fundamental Data Structures]).
2. Preliminaries: the problem and the yardsticks
Section titled “2. Preliminaries: the problem and the yardsticks”Definition 2.1(The sorting problem)
Let be a totally ordered set. The input is a sequence of length of elements of . The output is a permutation of such that
(or, equivalently, the rearranged sequence itself).
Totality matters here. Precisely because for any two elements at least one of and holds, every comparison we make returns an answer. For a partial order (where incomparable pairs exist) an output in the above sense may fail to exist at all.
Definition 2.2(The comparison sorting model)
An algorithm is called a comparison sort if the only information it obtains about the input elements is the outcome of comparisons, that is, of queries taking two elements and asking whether holds. Operations such as using the value of an element as an index, or extracting its bits, are not permitted.
All three algorithms treated here are comparison sorts. Within comparison sorting there is a wall at , as stated in Theorem 6.1; counting sort and radix sort evade the wall by stepping outside the model (Remark 6.3).
Definition 2.3(Inversions and the inversion count)
For a sequence , a pair of indices with
is called an inversion of . The total number of inversions is written and called the inversion count.
The inversion count measures “how disordered the input is”. The condition is equivalent to being sorted in increasing order, and when the elements are distinct the maximum value is , attained by the decreasing sequence. This quantity is the key to explaining the slowness of the naive sorts.
Definition 2.4(Stability and being in place)
A sorting algorithm is stable if for any two elements that are equal in the sense of the comparison, their relative order in the input is preserved in the output. It is in place if the working space it uses beyond the input array is or .
Stability matters in practice. When one takes a table sorted by revenue and re-sorts it by department, a stable sort keeps the revenue order within each department. An unstable sort destroys it.
From now on array indices start at , and denotes . Running time is counted taking “one comparison, one assignment” as the unit of time (the uniform-cost RAM model(Definition 2.1)[Complexity and Big-O Notation]).
3. Bubble sort: the price paid for adjacent exchanges
Section titled “3. Bubble sort: the price paid for adjacent exchanges”Bubble sort does nothing but repeat one operation: if two adjacent elements are in the wrong order, swap them.
def bubble_sort(a): n = len(a) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if a[j] > a[j + 1]: a[j], a[j + 1] = a[j + 1], a[j] swapped = True if not swapped: # no swap at all means the array is already sorted break return aOne pass of the inner loop makes the maximum of the scanned range “bubble up” to the right end. Hence the name.
Theorem 3.1(Correctness and operation count of bubble sort)
For any sequence of length , the algorithm above satisfies the following.
- It terminates, and its output is rearranged in increasing order.
- The number of swaps performed during execution is exactly .
- Without the early exit (the
breakgoverned byswapped), the number of comparisons is exactly . With the early exit the number of comparisons is at most , with equality on decreasing input.
Proof(Theorem 3.1)
(1) Correctness. We first establish the following invariant for the inner loop.
Immediately after the inner loop finishes the iteration with index , we have (as values of the array at that moment).
For , the comparison and swap put the larger of into , so the claim holds. Assuming it up to , at the start of iteration we have . Iteration compares with and places the larger into , so afterwards .
Consequently, immediately after the -th iteration of the outer loop (), the position holds the maximum of . We combine this with the outer invariant
immediately after outer iteration , the last entries are in increasing order and are the largest elements of the whole array
and argue by induction on . The case is exactly the conclusion above. Assuming it up to , iteration touches only the range and places its maximum into . That maximum is “the largest among what remains after removing the top elements”, hence the -st largest overall. The claim follows. Once is reached, the last positions are correct, and the remaining is automatically the minimum.
Correctness is preserved when the early exit fires. If some outer iteration performs no swap at all, then at that moment for every in the undetermined range, and the already-fixed tail consists of larger elements, so the whole array is in increasing order.
(2) Number of swaps. The only movement of elements is the operation “swap an adjacent pair with ”. Consider the effect of this operation on the relative order of pairs of elements of the array. For the pair consisting of the two swapped elements and (with ), before the swap the larger comes first, an inversion; after the swap the smaller comes first, not an inversion. For every other pair of elements, only the two positions change places, so the relative order is unaffected. Hence one swap decreases by exactly one.
By (1) the final array is increasing, that is, has inversion count . The inversion count drops by one at each swap and changes at no other time, so the number of swaps equals its initial value . Note that equal elements are not swapped, since a[j] > a[j + 1] is false; this is consistent with the strict inequality in Definition 2.3.
(3) Number of comparisons. Without the early exit, in outer iteration the inner loop compares for , that is, times. The total is therefore
The early exit only removes iterations, so it cannot increase the count. On decreasing input a swap occurs in every iteration up to the last (each iteration still leaves at least one inversion), so the exit never fires and the count is .
Example 3.2(A complete trace on six elements)
Apply bubble sort to . Each row records one pass of the inner loop.
| Pass | Comparisons | Swaps | Array at the end of the pass |
|---|---|---|---|
| 1 | 5 | 4 | |
| 2 | 4 | 2 | |
| 3 | 3 | 2 | |
| 4 | 2 | 1 | |
| 5 | 1 | 0 | (early exit) |
The comparisons total and the swaps total .
Now count the inversions of directly. After the entries smaller than are , four of them; after the entries smaller than are , two of them; after they are , again two; after there is nothing smaller, zero; after there is , one. The total is , in agreement with the number of swaps (part (2) of Theorem 3.1).
So far we have measured “bubble sort is slow”, but we have not yet explained “why it is slow”. The next proposition is the answer.
Proposition 3.3(Lower bound for sorts using only adjacent exchanges)
Consider an algorithm whose only means of moving elements is to swap two adjacent entries of the array. Then the number of swaps needed to sort the input into increasing order is at least . In particular, for inputs of length with distinct elements, swaps are needed in the worst case and, for a uniformly random permutation, swaps on average; both are .
Proof(Proposition 3.3)
As seen in the proof of part (2) of Theorem 3.1, a single adjacent exchange changes by (by if it swaps a reversed pair, by if it swaps a correctly ordered one). A sorted sequence has , so since each step can decrease the count by at most one, going from down to requires at least swaps.
The worst value is attained on decreasing input, where . As for the average, Exercise 8.1 shows .
The slowness of bubble sort is therefore not something that implementation tricks (early exit, bidirectional passes, and so on) can repair. The toolkit of adjacent exchanges itself demands . To go faster we need a mechanism that moves elements far, so that a single operation removes many inversions at once. This is the idea shared by the following sections.
Insertion sort is also , but its running time is (for the count of comparisons see Example 2.5[Complexity and Big-O Notation]), so on nearly sorted input () it finishes in linear time. Bubble sort does not have this property, because even with the early exit its outer loop runs a number of times proportional to the distance travelled by the element that must move furthest to the left. For small arrays and for nearly sorted arrays, practitioners choose insertion sort.
4. Merge sort: divide and conquer breaks the wall
Section titled “4. Merge sort: divide and conquer breaks the wall”Divide and conquer is a design principle: split a problem into smaller problems of the same kind, solve them recursively, and combine the solutions. The three stages have names.
- Divide: split the problem into smaller subproblems.
- Conquer: solve the subproblems recursively; if they are small enough, solve them directly.
- Combine: assemble the partial solutions into a solution of the original problem.
Applied to sorting, this reads: “split the array into a front half and a back half, sort each, and merge the two sorted runs.” That is merge sort.
flowchart TB A["8 3 5 1 9 2"] --> B["8 3 5"] A --> C["1 9 2"] B --> D["8"] B --> E["3 5"] C --> F["1"] C --> G["9 2"] E --> H["3"] E --> I["5"] G --> J["9"] G --> K["2"]
def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: # putting the equality on the left is what makes it stable result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result
def merge_sort(a): if len(a) <= 1: return a[:] m = len(a) // 2 return merge(merge_sort(a[:m]), merge_sort(a[m:]))Lemma 4.1(Correctness and cost of merging)
If left is an increasing sequence of length and right is an increasing sequence of length , then the merge above satisfies the following.
- The output is an increasing sequence of length consisting of all elements of
leftandright. - The number of comparisons is at most , and the running time is .
- If
leftandrightcontain elements that are equal in the sense of the comparison, the element fromleftis output first.
Proof(Lemma 4.1)
(1) We show that the while loop maintains the following invariant.
resultis increasing, and each of its entries is less than or equal to every element ofleft[i:]and ofright[j:]. Moreoverresultcoincides withleft[:i]together withright[:j].
Initially and result is empty, so the condition “each entry is at most …” holds vacuously, and left[:0] and right[:0] are both empty. Consider an iteration in which left[i] <= right[j] and left[i] is appended. Since left is increasing, left[i] is the minimum of left[i:]; by the condition it is at most right[j], and since right is increasing it is at most every element of right[j:]. Thus left[i] is the minimum of all remaining elements, so after appending it the invariant “each entry of result is at most every remaining element” still holds and result is still increasing. In the else case (left[i] > right[j]) the element right[j] is likewise the minimum of what remains.
When the loop ends, one of left, right has been consumed entirely. What remains on the other side is increasing and, by the invariant, at least every entry of result, so concatenating it yields an increasing whole. The number of elements and their multiplicities are preserved as well: inside the loop each consumed element is appended to result one at a time, and at the end the unconsumed remainder is concatenated in order, with nothing deleted and nothing duplicated.
(2) Each iteration of the while loop performs one comparison and increases the length of result by one. The loop stops as soon as one of left, right is exhausted, so the number of iterations is at most (the last element is always disposed of by concatenation). Together with the concatenations, the number of operations is proportional to .
(3) For equal elements left[i] <= right[j] is true, so the element from left is output first.
Theorem 4.2(Correctness and complexity of merge sort)
For any input of length , merge_sort terminates and returns the sequence sorted in increasing order. Moreover the number of comparisons and the running time satisfy
The algorithm is also stable in the sense of Definition 2.4.
Proof(Theorem 4.2)
Correctness and termination. By strong induction on the input length . If the input is already increasing, and a copy is returned, so the procedure halts. For , put ; then and , so by the induction hypothesis the two recursive calls halt and return increasing sequences. Part (1) of Lemma 4.1 then makes the output of merge an increasing sequence of the whole. Stability follows in the same inductive way from part (3) of Lemma 4.1 together with the fact that the front half always occupies the left side.
Number of comparisons. Write . Part (2) of Lemma 4.1 gives
(since merge_sort cuts at , the two parts have lengths and ).
We first record an auxiliary inequality. Let and , so that and . Halving and taking the ceiling gives , hence ; and since is nondecreasing and , also .
Using this we prove by strong induction on . For we have . Let and assume the claim for all smaller values. Putting and , so that and , we get
which is the claim.
Running time. The work outside the merge (slicing the subsequences and managing the recursive calls) is also proportional to the length, so there is a constant with
We prove by the same induction. For , . For , with the same as above,
(in the second line we used and ). Since , we obtain .
Drawing the content of this proof gives the following picture. At each level of the recursion, the lengths of the subarrays always sum to . The cost of merging is proportional to length, so the total cost at every level is . There are levels, so the whole thing is of order . Almost everything about why divide-and-conquer complexities take this shape is contained in this single picture.
Example 4.3(Counting every merge comparison)
We merge-sort the same as in Example 3.2. The division is as in the figure above; we merge upwards from the leaves.
| Merge | Inputs | Output | Comparisons |
|---|---|---|---|
| 1 | , | 1 | |
| 2 | , | 2 | |
| 3 | , | 1 | |
| 4 | , | 1 | |
| 5 | , | 5 |
Look closely at merge 2. Here left and right . Since is false we output ; since is also false we output ; at this point right is exhausted, so the remaining is concatenated, giving . Two comparisons. In merge 5 we compare with , with , with , with , and with , five comparisons, and once left is exhausted the remaining is concatenated.
The comparisons total , comfortably below the bound of Theorem 4.2. On the same input bubble sort used 15 comparisons and 9 swaps (Example 3.2). Even at the difference already shows.
Note also that in merge 5 the entries , output just before , overtake all three elements of the side at once. Several inversions are removed by a single operation; this is exactly where the restriction of Proposition 3.3 is escaped.
The weak point of merge sort is working space. The implementation above builds a new list at every level of the recursion, so it uses extra memory and is not in place in the sense of Definition 2.4. Even an implementation that allocates a single scratch array and reuses it still needs . On the other hand, linked lists can be merged by relinking pointers with no auxiliary array at all, which makes merge sort the first choice for external sorting and for list structures. For the trade-offs between data structures see Fundamental data structures, and for the cost of linked-list operations see Proposition 4.2[Fundamental Data Structures].
5. Quicksort: fast on average, slow at worst
Section titled “5. Quicksort: fast on average, slow at worst”Merge sort was “easy to divide, hard work to combine”. Quicksort reverses this: do the work while dividing, and do nothing while combining. Devised by C. A. R. Hoare in 1959, it repeatedly gathers the elements smaller than a reference value (the pivot) on the left and the larger ones on the right. Once a partition is done the pivot sits in its final position, so sorting the two sides and concatenating them sorts the whole.
We use the Lomuto partition scheme, which is simple to implement.
def partition(a, lo, hi): pivot = a[hi] # take the last element as the pivot i = lo for j in range(lo, hi): if a[j] <= pivot: a[i], a[j] = a[j], a[i] i += 1 a[i], a[hi] = a[hi], a[i] # move the pivot to the boundary return i
def quick_sort(a, lo=0, hi=None): if hi is None: hi = len(a) - 1 if lo < hi: p = partition(a, lo, hi) quick_sort(a, lo, p - 1) quick_sort(a, p + 1, hi) return aLemma 5.1(Correctness of the Lomuto partition)
Let and call partition(a, lo, hi). Writing for the return value and for the value of a[hi] before the call, after the call we have
and the multiset of elements of the subarray is unchanged. The number of comparisons is exactly .
Proof(Lemma 5.1)
For the for loop we prove by induction on that the following invariant holds at the start of iteration :
For we have and both ranges are empty, so the invariant holds. Iteration splits into two cases. If nothing happens, and the range merely acquires the entry , so the invariant persists. If we swap and . By the invariant the pre-swap is (when ) greater than , and after the swap it moves to position , landing at the right end of the “greater than ” region. Meanwhile moves to position , and then increases by one, so it joins the “at most ” region. When the element is swapped with itself and the claim again holds.
At the end of the loop () we have for and for . Finally swapping with brings to position and moves the value formerly at (which is greater than ) to the last position. Thus with the assertion takes the stated form. That follows because starts at and increases at most times. The multiset of elements is unchanged because the only operations are swaps. The comparison a[j] <= pivot is performed exactly once for each , giving comparisons.
By Lemma 5.1, the correctness of quick_sort follows by strong induction on the input length. Since is a final position, sorting and separately sorts the whole, and both subarrays are strictly shorter than the original. The combining step costs nothing: there is no analogue of the merge.
5.1. Worst-case complexity
Section titled “5.1. Worst-case complexity”Theorem 5.2(Worst-case complexity of deterministic quicksort)
For any input consisting of distinct elements, the quick_sort above performs at most comparisons. Moreover, if the input is already sorted in increasing order, the number of comparisons is exactly and the recursion depth reaches . Hence the worst-case complexity is .
Proof(Theorem 5.2)
Upper bound. Comparisons occur only in the form a[j] <= pivot, so one of the two compared elements is always the pivot of that call. By Lemma 5.1 the pivot is placed at its final position , and neither of the subsequent recursive calls quick_sort(a, lo, p-1) and quick_sort(a, p+1, hi) contains position . The pivot therefore never appears in any later subarray, so a given pair of elements is compared at most once. The number of pairs is , which bounds the number of comparisons.
Equality on sorted input. Calling partition on an increasing subarray of length , the pivot a[hi] is the maximum of that subarray. Hence a[j] <= pivot holds throughout the loop, increases every time and ends at , and the return value is . All the swaps are with the element itself, so the array remains increasing. The recursion therefore splits into “length ” and “length ”, and by Lemma 5.1 the number of comparisons is . Repeating this for , the total number of comparisons is
and the recursion depth is . The work per comparison is constant, so the running time is as well.
Example 5.3(The most unfavourable input)
Apply quick_sort to .
partition(a, 0, 4): pivot . All of are at most , so advances ; finallya[4]is swapped witha[4]and . Four comparisons. The left part is , the right part is empty.partition(a, 0, 3): pivot . Likewise . Three comparisons.partition(a, 0, 2): pivot , . Two comparisons.partition(a, 0, 1): pivot , . One comparison.
The total is . For sorted data with this means comparisons and, on top of that, a recursion depth of , so on most implementations the stack overflows first. This is what lies behind the phenomenon “re-sorting already sorted data froze the program”.
Duplicates are another pitfall. Applying the Lomuto partition to an array whose entries are all equal, a[j] <= pivot is always true, so and we again get the ” and ” split, hence . The remedy is a three-way partition (of Dutch-national-flag type) that collects the elements equal to the pivot in the middle. Bentley and McIlroy’s “Engineering a Sort Function” is the classic systematic treatment of traps of this kind.
5.2. Average-case complexity
Section titled “5.2. Average-case complexity”Quicksort remains in use despite its worst case because the average is fast and because that “average” can be guaranteed by randomness. The version that picks the pivot uniformly at random from the subarray is called randomized quicksort (instead of pivot = a[hi], choose an index uniformly between lo and hi, swap it with a[hi], and proceed as before).
Theorem 5.5(Expected number of comparisons of randomized quicksort)
For any input consisting of distinct elements, the expected number of comparisons of quicksort with the pivot chosen uniformly at random from the current subarray (independently of previous choices) at every recursive call satisfies
where is the harmonic number. In particular the expected running time is .
Proof(Theorem 5.5)
Write the input in increasing order as and, for , put . Introduce the random variables
As seen in the proof of Theorem 5.2, no pair is compared more than once, so the total number of comparisons is . By linearity of expectation, .
Claim: and are compared if and only if the first element of to be chosen as a pivot is or .
First, as long as all elements of lie in the same subarray, and have not been compared. Comparisons always involve the pivot, so as long as no pivot has been chosen from , the pair cannot have been compared. Moreover, during this time a pivot chosen from outside does not split . Indeed, if then either or (because is an interval in the value order); in the first case Lemma 5.1 sends every element of to the right side, and in the second case all of them to the left side.
So let be the first element of chosen as a pivot. At that moment still lies in one subarray. If or , the pivot is compared with every element of that subarray, so and are compared. If , then by Lemma 5.1 goes to the left and to the right, they never again share a subarray, and they are never compared. The claim is proved.
Computing the probability. When a pivot is chosen uniformly at random from a subarray containing , then conditioned on the chosen element belonging to , each element of is equally likely. If an element outside is chosen, then as seen above is carried over intact into the next subarray and the same argument repeats. Hence “the first element of to become a pivot” is uniformly distributed on , and the probability that it is or is
Estimating the sum. Put and count.
Finally we use . This follows from the inequality for (since on the interval ), which gives
Altogether . The work outside the comparisons (swaps and index arithmetic) is proportional to the number of comparisons, so the expected running time is .
Since , randomized quicksort makes on average about 1.4 times as many comparisons as merge sort’s upper bound. That quicksort is nevertheless often faster in measurements is because it rewrites the array in place without extra memory, giving good cache behaviour, and because its inner loop consists of nothing but “compare, conditionally swap, increment an index”. It is a textbook case of two algorithms with the same complexity differing in the constant factor.
Countermeasures against the worst case come down to the choice of pivot.
- Random choice: the setting of Theorem 5.5. The worst case does not disappear, but its probability depends on the random bits rather than on the input. It also matters in practice that nobody can craft a particular input to slow the program down (short of knowing the random sequence).
- Median of three: take the median of the first, middle and last elements as the pivot. Since a value near the middle is selected on sorted input, this prevents the breakdown of Example 5.3. However, adversarial inputs constructed with knowledge of this rule still force .
- Introsort: switch to heapsort once the recursion depth exceeds . Since heapsort is in the worst case, the whole thing guarantees in the worst case while normally running at quicksort speed. This is the scheme used by
std::sortin the C++ standard library.
6. The limits of comparison sorting
Section titled “6. The limits of comparison sorting”We now have two algorithms. Is there a faster comparison sort? The answer is no. Moreover, the proof does not examine individual algorithms: it handles all comparison sorts at once.
The idea is this. A comparison sort cannot look at the input elements directly; it operates using only the answers to comparisons (true or false). The behaviour on distinct elements can therefore be described by a binary tree (a decision tree) whose internal nodes are queries “is ?”, whose edges are the answers, and whose leaves are the permutations to be output. A single comparison yields one bit of information, so comparisons distinguish at most outcomes. On the other hand, sorting correctly requires distinguishing all rearrangements.
Theorem 6.1(Lower bound for comparison sorting)
For any deterministic comparison sort that correctly sorts distinct elements, the number of comparisons required in the worst case satisfies
In particular .
Proof(Theorem 6.1)
Fix the algorithm and consider its decision tree on inputs of distinct elements. There are ways of arranging the input, and each requires a different output permutation. If two distinct arrangements reached the same leaf of the decision tree, the algorithm would return the same output for both, and at least one of them would be wrong. Hence the decision tree has at least leaves.
A binary tree of height has at most leaves (by induction on : for there is one leaf; a tree of height has two subtrees at the root, each of height at most , so it has at most leaves). The worst-case number of comparisons equals the height of the decision tree, so , that is, .
Next we show . The power series of the exponential function
(the right-hand side keeps only the term , all other terms being nonnegative) gives . Taking of both sides,
and .
Corollary 6.2(Asymptotic optimality of merge sort)
The worst-case number of comparisons of merge sort is at most , and its ratio to the lower bound for any comparison sort tends to as . Thus merge sort is asymptotically optimal among comparison sorts, and its worst-case complexity is .
Proof(Corollary 6.2)
The upper bound is Theorem 4.2 and the lower bound is Theorem 6.1. Assume from now on (then , so the lower bound is positive). Using , and the fact that the upper bound is at least the lower bound, the ratio of comparison counts satisfies
(the limit on the right follows by dividing numerator and denominator by , giving ). As for running time, the upper bound is Theorem 4.2, while the lower bound comes from Theorem 6.1 together with the fact that each comparison takes at least constant time; together they give .
This lower bound comes from the restriction “comparisons only”. Drop the restriction and it breaks. If, for instance, the input is known to consist of integers in the range from to , then counting sort, which counts the occurrences of each value, runs in . Radix sort, which stably sorts -digit integers digit by digit starting from the least significant, runs in . These use element values as array indices, so they lie outside Definition 2.2 and Theorem 6.1 does not apply to them. A lower bound must always be read together with the statement of the computational model it holds in.
7. Choosing among them
Section titled “7. Choosing among them”We summarize the three algorithms, adding heapsort for comparison. Here is the number of elements, and extra memory means working space needed beyond the input array.
| Algorithm | Worst-case time | Average time | Extra memory | Stable | Remarks |
|---|---|---|---|---|---|
| Bubble sort | yes | Almost no practical value; useful as a vehicle for inversions | |||
| Insertion sort | yes | , hence strong on small or nearly sorted inputs | |||
| Merge sort | yes | Worst-case guarantee; suited to external sorting and linked lists | |||
| Quicksort | no | Small constant factor, fast in practice; pivot safeguards are essential | |||
| Heapsort | no | Worst-case guarantee and in place; somewhat larger constant factor |
The extra memory for quicksort is the recursion stack. Written naively the depth can reach in the worst case (Theorem 5.2), but recursing only on the shorter side always keeps it within (Exercise 8.3). The table assumes that refinement.
Practical decisions run roughly as follows.
- Use the standard library sort. This is the first choice. CPython’s
list.sortand Java’sArrays.sortfor object arrays are stable merge sorts of the Timsort family, which detect and exploit the ascending and descending runs already present in the input. C++‘sstd::sortis an introsort, and Java’sArrays.sortfor primitive arrays is a quicksort with two pivots. Note that the choice branches on whether stability is required. - When a worst-case guarantee is needed (when a bound on response time is contractual, or when the input may be chosen adversarially), pick merge sort, heapsort or introsort.
- When memory is tight, pick heapsort or quicksort. There are situations where the of merge sort cannot be afforded.
- Insertion sort for small subarrays. Fast implementations in practice stop recursing once a subarray falls below a dozen or so elements and switch to insertion sort. Even at the constant factor is small, so for small it wins.
Finally, “being sorted” is itself a powerful piece of preprocessing. Binary search on a sorted array runs in (see Theorem 3.3[探索アルゴリズム] in Search algorithms). A design of the form “sort once, then search many times” works precisely because a single investment makes every subsequent search cost . Divide and conquer, moreover, is the technique for subproblems that do not overlap; when subproblems do overlap, the corresponding technique is dynamic programming (Definition 3.1[動的計画法]), which reuses results.
8. Exercises
Section titled “8. Exercises”Exercise 8.1Standard
Show that for a sequence obtained by arranging distinct elements uniformly at random, the expected inversion count is
Then use this to determine the average number of swaps performed by bubble sort.
Solution
For a pair of indices with , let be the indicator random variable taking the value when and otherwise. By definition .
We show that for a uniformly random permutation. On the set of all permutations, consider the map that exchanges the values at positions and . Applying twice returns to the original ( is the identity), so is a bijection and therefore preserves the uniform distribution. And interchanges the values and of . Hence , and since the elements are distinct these two sum to , so both equal .
By linearity of expectation (independence is not needed),
By part (2) of Theorem 3.1 the number of swaps performed by bubble sort equals , so the average number of swaps is also , that is, . It is merely half of the worst case; the order does not improve.
Exercise 8.2Hard
Construct an algorithm that computes the inversion count of an array in time by modifying merge sort, and explain why it is correct.
Solution
Split the array into a front half and a back half . Then the inversions fall into three classes.
- Both and lie in the front half.
- Both and lie in the back half.
- lies in the front half and in the back half (a crossing inversion).
Classes 1 and 2 are counted by the recursive calls. Class 3 is counted during the merge. The point is that at merge time and are already sorted. When the leading element right[j] of is output, we have left[i] > right[j]. Since is sorted, the not-yet-output entries left[i], left[i+1], ..., left[-1] are all at least left[i], hence all greater than right[j]. All of them precede right[j] in the original array, so they create exactly len(left) - i crossing inversions.
Conversely, every crossing inversion (with , , ) is counted exactly once. In the merge the left element is output when left[i] <= right[j], so an with is output after . That is, at the moment is output, is necessarily still unoutput, and it is counted in the len(left) - i at that moment. That is the only time it is counted.
def sort_and_count(a): if len(a) <= 1: return a[:], 0 m = len(a) // 2 left, c_left = sort_and_count(a[:m]) right, c_right = sort_and_count(a[m:]) merged, i, j, cross = [], 0, 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: merged.append(left[i]) i += 1 else: merged.append(right[j]) j += 1 cross += len(left) - i # every remaining left entry exceeds right[j] merged.extend(left[i:]) merged.extend(right[j:]) return merged, c_left + c_right + cross
assert sort_and_count([8, 3, 5, 1, 9, 2]) == ([1, 2, 3, 5, 8, 9], 9)The complexity obeys the same recurrence as merge sort, so the same argument as in the proof of Theorem 4.2 gives . The closing assert agrees with the inversion count computed by hand in Example 3.2. What would take with a double loop is obtained here as a by-product of sorting.
Exercise 8.3Standard
In the worst case the recursion depth of quick_sort reaches (Theorem 5.2). Rewriting it so that only the shorter of the two recursive calls is handled by recursion, while the longer one is handled by a loop, bounds the recursion depth by without changing the number of comparisons. Write this implementation and prove the bound on the depth.
Solution
def quick_sort_bounded(a, lo=0, hi=None): if hi is None: hi = len(a) - 1 while lo < hi: p = partition(a, lo, hi) if p - lo < hi - p: # the left side is shorter quick_sort_bounded(a, lo, p - 1) lo = p + 1 # handle the right side in the loop else: # the right side is shorter quick_sort_bounded(a, p + 1, hi) hi = p - 1 # handle the left side in the loop return aThe collection of subarrays processed is exactly the same as in the original implementation, so neither the number of calls to partition nor the number of comparisons changes.
Now the depth. Partitioning a subarray of length produces two parts of lengths with (one element, the pivot, is removed). We recurse on the shorter one, whose length is . So each additional level of recursion cuts the subarray length to less than half. Starting from length , the recursion stops once the length is at most ; at depth the length is less than , which is at most when . Hence the recursion depth is at most and the stack usage is .
This can be viewed as performing tail call elimination by hand. Even on the sorted input of Example 5.3, the shorter side (of length ) is recursed on and the longer side is handled by the loop, so the depth is . The running time is still , but at least abnormal termination through stack overflow is avoided.
Exercise 8.4Easy
(a) Explain why it is necessary for the stability of merge sort that the comparison in merge be left[i] <= right[j] and not <.
(b) Show by a concrete three-element example that quicksort with the Lomuto partition is not stable. Take the elements to be pairs consisting of a key and associated data, compared by key alone.
Solution
(a) Consider two elements (coming from the front half ) and (coming from the back half ) with equal keys. In the original array every element of precedes every element of , so stability requires that be output before . With the comparison left[i] <= right[j], equal keys make the condition true and the left-hand comes out first. Had we written left[i] < right[j], equality would send us into the else branch and the right-hand would come out first, reversing the order. A single character, the side on which the equality sign is placed, decides stability. The order of two elements coming from the same half is preserved by the induction hypothesis, as in the proof of Theorem 4.2.
(b) Write for elements with key 2 and for an element with key 1, and take the input
Trace partition(a, 0, 2). The pivot is a[2], that is, (key 1), and .
- : the key of
a[0]is 2 and is false. Nothing happens. - : the key of
a[1]is 2 and is false. Nothing happens.
Leaving the loop, swapping a[0] with a[2] makes the array with return value . In the ensuing recursion the right side is partitioned with pivot ; since is true, a[1] is swapped with itself, becomes 2, and finally a[2] is swapped with a[2], leaving the arrangement unchanged. The final output is
The input had the key-2 elements in the order , but the output has them in the order . So this implementation is not stable. The cause is that the swap moving the pivot to the boundary at the end of the partition exchanges two elements that are far apart. Being able to move elements far was the source of quicksort’s speed; that same property destroys stability.
References
Section titled “References”- T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4th ed., MIT Press, 2022 — Chapter 2 (insertion sort and merge sort), Chapter 7 (quicksort and the analysis of its expected complexity), Chapter 8 (the comparison-sorting lower bound, counting sort, radix sort).
- D. E. Knuth, The Art of Computer Programming, Volume 3: Sorting and Searching, 2nd ed., Addison-Wesley, 1998 — Chapter 5 (sorting), especially 5.2 (internal sorting). Historical background of each method and precise analyses of the constants.
- C. A. R. Hoare, “Quicksort”, The Computer Journal 5 (1962), 10–16. DOI: 10.1093/comjnl/5.1.10 — the original paper by the inventor.
- J. L. Bentley, M. D. McIlroy, “Engineering a Sort Function”, Software: Practice and Experience 23 (1993), 1249–1265. DOI: 10.1002/spe.4380231105 — implementation design including pivot selection and the handling of duplicate elements.
- D. R. Musser, “Introspective Sorting and Selection Algorithms”, Software: Practice and Experience 27 (1997), 983–993 — the paper proposing introsort (switching between quicksort and heapsort).
- R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011 — Chapter 2 (sorting). An exposition centred on implementation and measurement.
Appendix: Solving divide-and-conquer recurrences wholesale
Section titled “Appendix: Solving divide-and-conquer recurrences wholesale”The master theorem. Recurrences such as the that appeared for merge sort show up whenever divide and conquer is used. Instead of solving each one by induction, the following theorem (Theorem 6.1[Complexity and Big-O Notation]) handles them mechanically. Let and be constants, let be a nonnegative function, and put
(where may be read as or ). Setting , the following hold.
- If for some , then . (The work at the leaves dominates.)
- If , then . (Every level contributes equally.)
- If for some and moreover for some and all sufficiently large , then . (The work at the root dominates.)
Application to merge sort. Here , and , so and , which is the second case, giving . This is the same conclusion as the induction in Theorem 4.2.
The effect of unbalanced splits. The worst case of quicksort is , which is not of the form and so falls outside the master theorem; expanding it directly gives . On the other hand, a split that is always as lopsided as "" — which at first sight looks bad — gives , where the recursion tree has depth and each level costs , so still. An imbalance by a constant ratio only changes the base of the logarithm; it does not break the order. This is why quicksort is fast enough even with merely “decent” splits. Breakdown occurs only when the split fails to maintain a constant ratio, as with “one element and all the rest”.
A caveat. The regularity condition in the third case () is not decoration. There are examples, such as oscillating , that fail it and for which the conclusion does not hold. Also, the gaps between the three cases (for instance ) are beyond the reach of the master theorem, and require evaluating the recursion tree directly or the more general Akra–Bazzi method. Proofs and precise statements are in Chapter 4 of Cormen et al.
Report an error in this article ・Operated by: Mugen Giken LLC ・Pricing ・Terms ・Legal notice
© 2026 夢現技研合同会社 ・Feeding the text to an LLM is welcome. Code samples are MIT licensed.