Sorting algorithms: analysis, lower bound, counting and radix sort
Step-by-step traces and Big-O analysis of merge sort, quicksort and heapsort, the n log n lower bound for comparison-based sorting, and the non-comparison counting and radix sorts.
At the end of SEN2211 you saw the simple sorts such as bubble, selection and insertion sort. In this course the questions go one level up. The exam has three kinds of questions: trace (what are the halves before the merge, what is the array after the first partition, what is the heap after buildHeap), analyze (best, average, worst case, how many comparisons), compare (which is stable, which is in place, which one to use when).
Golden rules:
- Merge sort is in every case, but it needs extra memory.
- Quicksort is the fastest on average, but with bad pivots it becomes .
- Heapsort is in every case and in place, but it is not stable.
- No comparison-based sort can beat in the worst case.
- Counting and radix sort beat this bound, because they make no comparisons; they put extra assumptions on the keys.
Merge sort
Divide and conquer. Split the array in the middle, sort the two halves recursively, then merge the two sorted halves in a single pass. The merge step looks at the head of both lists and writes the smaller one to the output.
static void mergeSort(int[] a, int lo, int hi) {
if (lo >= hi) return; // 0 or 1 element: already sorted
int mid = (lo + hi) / 2;
mergeSort(a, lo, mid);
mergeSort(a, mid + 1, hi);
merge(a, lo, mid, hi); // O(hi - lo + 1)
}
For [42, 17, 8, 93, 25, 61, 4, 30], the order of merge calls (each line is the state of that range after one merge):
merge [0..1] -> [17, 42]
merge [2..3] -> [8, 93]
merge [0..3] -> [8, 17, 42, 93]
merge [4..5] -> [25, 61]
merge [6..7] -> [4, 30]
merge [4..7] -> [4, 25, 30, 61]
merge [0..7] -> [4, 8, 17, 25, 30, 42, 61, 93]
toplam karşılaştırma: 17
Note: you do not move to the right half until the left half is completely done. For “which is the second merge?” the answer is [2..3], not [4..5].
Analysis
The recurrence:
At every level of the recursion tree the total work is , and there are levels. The result is . This time does not depend on the input: sorted, reverse sorted and random inputs all take the same. The extra memory is , because merge uses a temporary array.
Quicksort
Choose a pivot. Partition the array so that elements smaller than the pivot go to the left and larger ones go to the right. The pivot is now in its final place. Sort both sides recursively. There is no merge step; the work is in the partition.
On this page we use the Lomuto scheme: the pivot is the last element, and i marks the end of the “smaller” region.
static int partition(int[] a, int lo, int hi) {
int p = a[hi], i = lo - 1;
for (int j = lo; j < hi; j++)
if (a[j] < p) { i++; swap(a, i, j); }
swap(a, i + 1, hi); // pivot to its place
return i + 1;
}
Full trace on [29, 71, 12, 55, 8, 40, 33]:
partition [0..6] pivot=33 -> [29,12,8,33,71,40,55]
partition [0..2] pivot=8 -> [8,12,29,33,71,40,55]
partition [1..2] pivot=29 -> [8,12,29,33,71,40,55]
partition [4..6] pivot=55 -> [8,12,29,33,40,55,71]
In the first partition, 29, 12 and 8 were collected at the front because they are smaller than the pivot; 33 landed at index 3 and never moved again.
Analysis
| Case | What happens | Time |
|---|---|---|
| Best | The pivot is always the median, two equal parts | |
| Average | Random input | , small constant |
| Worst | The pivot is always the smallest or largest |
In the worst case the recurrence is , for a total of comparisons. With the last element as pivot, an already sorted array produces exactly this case. Practical fixes: a random pivot or median-of-three (the median of the first, middle and last elements). Switching to insertion sort for small subarrays (around n < 10) is also common.
PITFALL · “quicksort is always n log n”. It is not. The average and best cases are , the worst case is . If the exam asks for “worst case”, the answer is .
Heapsort
We will cover heaps in detail in week 7. For now you only need to know this: a max-heap is a complete binary tree in which every node is greater than or equal to its children, and it is stored in an array. In a 0-indexed array the children of i are 2i+1 and 2i+2.
Heapsort has two phases:
- buildHeap: from the last internal node (
n/2 - 1) up to the root, percolate each node down. Time . - n - 1 times deleteMax: swap the root (the largest) with the last element of the heap, reduce the heap size by 1, and percolate the new root down. Each takes .
For [6, 3, 9, 1, 8, 2]:
buildHeap -> [9, 8, 6, 1, 3, 2]
deleteMax 1 -> [8, 3, 6, 1, 2 | 9]
deleteMax 2 -> [6, 3, 2, 1 | 8, 9]
deleteMax 3 -> [3, 1, 2 | 6, 8, 9]
deleteMax 4 -> [2, 1 | 3, 6, 8, 9]
deleteMax 5 -> [1 | 2, 3, 6, 8, 9]
The part to the right of the | mark is the sorted region. With a max-heap the array ends up in ascending order, because each step writes the largest element to the end. The total time is in every case, and the extra memory is .
The lower bound for comparison-based sorting
Any algorithm that works only with comparisons like a[i] < a[j] can be drawn as a decision tree. Each internal node is a comparison, and each leaf is one ordering (permutation) of the input.
- For n distinct elements there are possible orderings. The algorithm must tell them all apart: at least leaves.
- A binary tree of height has at most leaves.
- .
- By Stirling’s approximation, .
The height of the tree is the number of comparisons in the worst case. The result:
| n | n! | minimum comparisons | |
|---|---|---|---|
| 3 | 6 | 2.585 | 3 |
| 4 | 24 | 4.585 | 5 |
| 5 | 120 | 6.907 | 7 |
| 8 | 40320 | 15.299 | 16 |
| 10 | 3628800 | 21.791 | 22 |
This is why merge sort and heapsort are asymptotically optimal comparison-based sorts.
Counting sort
If the keys are integers in 0..k, you can sort without comparisons. Count how many times each value occurs, use prefix sums to find each value’s position in the output, and place the input items by scanning from right to left.
[2, 5, 3, 0, 2, 3, 0, 3], k = 5:
count = [2, 0, 2, 3, 0, 1]
prefix = [2, 2, 4, 7, 7, 8]
output = [0, 0, 2, 2, 3, 3, 3, 5]
prefix[3] = 7 means: the last 3 goes to index 6 (7 - 1) of the output. The counter drops by one at each placement. Scanning from right to left preserves the order of equal keys: the sort is stable. Time is , memory is . It is impractical when k is very large (e.g. 32-bit numbers).
Radix sort
Sort multi-digit numbers starting from the least significant digit (LSD), using a stable bucket/counting pass for each digit.
[329, 457, 657, 839, 436, 720, 355]:
ones -> [720, 355, 436, 457, 657, 329, 839]
tens -> [720, 329, 436, 839, 355, 457, 657]
hundreds -> [329, 355, 436, 457, 657, 720, 839]
In the second pass, 720 and 329 both have tens digit 2, so they are in the same bucket. They kept their order from the first pass (720 first). Every pass must be stable, otherwise the work of the earlier digits is lost.
Time: for d digits and base b, . If d and b are constants, it is linear.
Summary table
| Algorithm | Best | Average | Worst | Extra memory | Stable |
|---|---|---|---|---|---|
| Insertion | yes | ||||
| Merge | yes | ||||
| Quick | no | ||||
| Heap | no | ||||
| Counting | yes | ||||
| Radix | yes |
Common mistakes
| Wrong idea | The truth |
|---|---|
| Quicksort is in the worst case too | ; sorted input + an end pivot |
| Merge sort is in place | The standard version needs an temporary array |
| Heapsort is stable | It is not; swapping the root with the last element breaks the order |
| buildHeap is | ; the total comes from the deleteMax calls |
| The lower bound holds for every sort | Only for comparison-based sorts; radix/counting are exempt |
| Radix sort starts from the most significant digit | The LSD version starts from the least significant digit |
| In counting sort, placing from left to right is also stable | If you place by decrementing the prefix, you must scan from right to left |
| In a merge sort trace, left and right halves are processed alternately | You do not move to the right half until the left half is completely done |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Merge sort (top-down, mid = (lo + hi) / 2, left half is [lo..mid]) is applied to [55, 12, 70, 3, 41, 28]. What are the two sorted halves just before the final merge?
Show answer
n = 6, mid = (0+5)/2 = 2. The left half [0..2] is 55, 12, 70; the right half [3..5] is 3, 41, 28.
Left: [55,12] merges to [12,55], then with 70 → [12,55,70].
Right: [3,41] → [3,41], then with 28 → [3,28,41].
The final merge combines these two arrays: [3,12,28,41,55,70].
How many element comparisons does the standard merge step make when merging [1, 3, 5, 7] with [2, 4, 6, 8]? And [1, 2, 3, 4] with [5, 6, 7, 8]?
Show answer
In the first case the elements come alternately from the two lists, and one list runs out only at the last element: n1 + n2 - 1 = 7 comparisons.
In the second case the 4 elements on the left are compared with 5 and output one by one (4 comparisons), then the left list is empty and the right elements are copied without comparisons.
Rule: merge makes at least min(n1, n2) and at most n1 + n2 - 1 comparisons.
Quicksort uses the last element as pivot and the Lomuto partition scheme. Show the array after the first partition of [14, 52, 7, 33, 61, 20, 25] and give the pivot's final index.
Show answer
Pivot = 25, i = -1. j scans from left to right; if a[j] < 25, do i++ and swap a[i] with a[j].
j=0: 14 < 25 → i=0 (stays in place). j=1: 52 no. j=2: 7 < 25 → i=1, swap 52 and 7: [14,7,52,33,61,20,25].
j=3: 33 no. j=4: 61 no. j=5: 20 < 25 → i=2, swap 52 and 20: [14,7,20,33,61,52,25].
Finally swap a[i+1]=a[3] with the pivot: [14,7,20,25,61,52,33]. The pivot index is 3, with 6 comparisons in total.
Quicksort with last-element pivot (Lomuto) is run on the already sorted array [1, 2, 3, 4, 5, 6, 7, 8]. How many element comparisons are made in total, and what is the complexity class?
Show answer
In a sorted array the pivot is always the largest element. Each partition separates only the pivot and one side stays empty.
Comparisons: 7 + 6 + 5 + 4 + 3 + 2 + 1 = 28 = n(n-1)/2.
This is the worst case of quicksort: . The fix: a random pivot or median-of-three.
Heapsort (max-heap, 0-indexed array) first runs buildHeap on [5, 13, 2, 25, 7, 17, 20, 8, 4]. What is the array after buildHeap?
Show answer
n = 9, the last internal node is n/2 - 1 = 3. Percolate down for i = 3, 2, 1, 0:
i=3 (25): children are 8, 4. 25 is larger, no change.
i=2 (2): children are 17, 20 → swaps with 20: [5,13,20,25,7,17,2,8,4].
i=1 (13): children are 25, 7 → 25 moves up, 13 goes down to index 3; its children are 8, 4, so it stops: [5,25,20,13,7,17,2,8,4].
i=0 (5): children are 25, 20 → 25 moves up; 5 is at index 1, children 13, 7 → 13 moves up; 5 is at index 3, children 8, 4 → 8 moves up; 5 stops at index 7.
Result: [25,13,20,8,7,17,2,5,4].
Continue the previous question: after buildHeap gives [25, 13, 20, 8, 7, 17, 2, 5, 4], heapsort performs one deleteMax (swap root with the last heap element, shrink heap, percolate down). What is the array now?
Show answer
25 is swapped with the last element 4: [4,13,20,8,7,17,2,5,25]. The heap size is 8, and 25 is now in the sorted region.
4 sinks from the root: children 13, 20 → 20 moves up. 4 is at index 2, children 17, 2 → 17 moves up. 4 is at index 5 and has no children (11 > 7).
Result: [20,13,17,8,7,4,2,5,25].
Using the decision-tree argument, what is the minimum number of comparisons any comparison-based algorithm needs in the worst case to sort 5 distinct elements?
Show answer
5 elements have 5! = 120 different orderings. Each leaf of the decision tree is one ordering, so at least 120 leaves are needed.
A binary tree of height h has at most leaves: , .
It must be an integer: at least 7 comparisons.
Counting sort is applied to [3, 1, 4, 1, 5, 2, 3, 0] with keys in 0..5. Give the count array and the prefix-sum (cumulative) array.
Show answer
count[v] = how many times the value v occurs: 0 once, 1 twice, 2 once, 3 twice, 4 once, 5 once.
Prefix: add the sum of everything to the left to each cell: 1, 1+2=3, 3+1=4, 4+2=6, 6+1=7, 7+1=8.
prefix[v] = the last position of value v in the output + 1. Placing from right to left gives [0,1,1,2,3,3,4,5], and the sort is stable.
LSD radix sort (base 10, stable bucket pass per digit) is applied to [170, 45, 75, 90, 802, 24, 2, 66]. What is the array after the first pass and after the second pass?
Show answer
Pass 1 (ones digit): 0 → 170, 90; 2 → 802, 2; 4 → 24; 5 → 45, 75; 6 → 66. Items in the same bucket keep their input order.
Pass 2 (tens digit, on the output of pass 1): 0 → 802, 2; 2 → 24; 4 → 45; 6 → 66; 7 → 170, 75; 9 → 90.
After pass 3 (hundreds): [2,24,45,66,75,90,170,802].
Which of merge sort, quicksort and heapsort (standard array versions) is stable, and which one needs O(n) extra memory?
Show answer
Merge sort takes the left element first on ties (a[i] <= a[j]), so it is stable. But merge needs a temporary array of size n: O(n) extra memory.
Quicksort and heapsort swap distant elements, so the order of equal elements can break: they are not stable. Both work in place; quicksort uses O(log n) on average for the recursion stack.
Radix sort sorts n integers that each have d digits in base b. What is its running time? When is it O(n)?
Show answer
Each digit takes one counting/bucket pass: O(n + b). For d digits, O(d(n + b)).
If d is constant (e.g. 32-bit numbers, fixed-length keys) and b is at most on the order of n, the running time is O(n).
This does not break the n log n lower bound, because radix sort makes no comparisons.
Write a Java method static void merge(int[] a, int lo, int mid, int hi) that merges the sorted ranges a[lo..mid] and a[mid+1..hi] into one sorted range, keeping the sort stable.
Show answer
Idea: two pointers i (left) and j (right). Take the smaller one into a temporary array. On a tie, take the left one (stable). When one side runs out, copy the rest of the other, then write the temporary array back.
static void merge(int[] a, int lo, int mid, int hi) {
int[] tmp = new int[hi - lo + 1];
int i = lo, j = mid + 1, k = 0;
while (i <= mid && j <= hi) {
if (a[i] <= a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while (i <= mid) tmp[k++] = a[i++];
while (j <= hi) tmp[k++] = a[j++];
for (k = 0; k < tmp.length; k++) a[lo + k] = tmp[k];
}The running time is O(hi - lo + 1). If you write <, the result is still sorted but no longer stable.Verification: All traces, comparison counts, heap arrays, counting/radix passes and log2(n!) values were computed with Java (verify/SEN2212/W01.java, java W01.java). The merge and quickSort methods were compared against Arrays.sort on 1000 random arrays.