SEN2212 · Week 1 · Introduction and Sorting Algorithms.

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.

merge sortquicksortpivotpartitionheapsortstable sortin-placedecision treelower boundcounting sortradix sortLSD

At the end of SEN2211 you saw the simple O(n2)O(n^2) 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:

  1. Merge sort is O(nlog⁡n)O(n \log n) in every case, but it needs O(n)O(n) extra memory.
  2. Quicksort is the fastest on average, but with bad pivots it becomes O(n2)O(n^2).
  3. Heapsort is O(nlog⁡n)O(n \log n) in every case and in place, but it is not stable.
  4. No comparison-based sort can beat Ω(nlog⁡n)\Omega(n \log n) in the worst case.
  5. 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:

T(n)=2T(n/2)+cn,T(1)=cT(n) = 2T(n/2) + cn, \qquad T(1) = c

At every level of the recursion tree the total work is cncn, and there are log⁡2n\log_2 n levels. The result is T(n)=O(nlog⁡n)T(n) = O(n \log n). This time does not depend on the input: sorted, reverse sorted and random inputs all take the same. The extra memory is O(n)O(n), 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

CaseWhat happensTime
BestThe pivot is always the median, two equal partsO(nlog⁡n)O(n \log n)
AverageRandom inputO(nlog⁡n)O(n \log n), small constant
WorstThe pivot is always the smallest or largestO(n2)O(n^2)

In the worst case the recurrence is T(n)=T(n−1)+cnT(n) = T(n-1) + cn, for a total of n(n−1)/2n(n-1)/2 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 O(nlog⁡n)O(n \log n), the worst case is O(n2)O(n^2). If the exam asks for “worst case”, the answer is O(n2)O(n^2).

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:

  1. buildHeap: from the last internal node (n/2 - 1) up to the root, percolate each node down. Time O(n)O(n).
  2. 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 O(log⁡n)O(\log n).

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 O(nlog⁡n)O(n \log n) in every case, and the extra memory is O(1)O(1).

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 n!n! possible orderings. The algorithm must tell them all apart: at least n!n! leaves.
  • A binary tree of height hh has at most 2h2^h leaves.
  • 2h≥n!⇒h≥log⁡2(n!)2^h \ge n! \Rightarrow h \ge \log_2(n!).
  • By Stirling’s approximation, log⁡2(n!)=Θ(nlog⁡n)\log_2(n!) = \Theta(n \log n).

The height of the tree is the number of comparisons in the worst case. The result:

worst-case comparisons≥⌈log⁡2n!⌉=Ω(nlog⁡n)\text{worst-case comparisons} \ge \lceil \log_2 n! \rceil = \Omega(n \log n)

nn!log⁡2n!\log_2 n!minimum comparisons
362.5853
4244.5855
51206.9077
84032015.29916
10362880021.79122

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 O(n+k)O(n + k), memory is O(n+k)O(n + k). 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, O(d(n+b))O(d(n + b)). If d and b are constants, it is linear.

Summary table

AlgorithmBestAverageWorstExtra memoryStable
InsertionO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)yes
MergeO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(n)O(n)yes
QuickO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(n2)O(n^2)O(log⁡n)O(\log n)no
HeapO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(1)O(1)no
CountingO(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)O(n+k)yes
RadixO(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(n+b)O(n+b)yes

Common mistakes

Wrong ideaThe truth
Quicksort is O(nlog⁡n)O(n \log n) in the worst case tooO(n2)O(n^2); sorted input + an end pivot
Merge sort is in placeThe standard version needs an O(n)O(n) temporary array
Heapsort is stableIt is not; swapping the root with the last element breaks the order
buildHeap is O(nlog⁡n)O(n \log n)O(n)O(n); the total O(nlog⁡n)O(n \log n) comes from the deleteMax calls
The lower bound holds for every sortOnly for comparison-based sorts; radix/counting are exempt
Radix sort starts from the most significant digitThe LSD version starts from the least significant digit
In counting sort, placing from left to right is also stableIf you place by decrementing the prefix, you must scan from right to left
In a merge sort trace, left and right halves are processed alternatelyYou 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.

Question 1easy

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
[12, 55, 70] and [3, 28, 41]

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].

Question 2medium

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
7 and 4

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.

Question 3medium

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
[14, 7, 20, 25, 61, 52, 33], index 3

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.

Question 4medium

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
28, O(n^2)

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: O(n2)O(n^2). The fix: a random pivot or median-of-three.

Question 5hard

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
[25, 13, 20, 8, 7, 17, 2, 5, 4]

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].

Question 6hard

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
[20, 13, 17, 8, 7, 4, 2, 5, 25]

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].

Question 7medium

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
7

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 2h2^h leaves: 2h≥1202^h \ge 120, h≥log⁡2120≈6.907h \ge \log_2 120 \approx 6.907.
It must be an integer: at least 7 comparisons.

Question 8easy

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 = [1, 2, 1, 2, 1, 1], prefix = [1, 3, 4, 6, 7, 8]

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.

Question 9medium

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: [170, 90, 802, 2, 24, 45, 75, 66]; Pass 2: [802, 2, 24, 45, 66, 170, 75, 90]

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].

Question 10easy

Which of merge sort, quicksort and heapsort (standard array versions) is stable, and which one needs O(n) extra memory?

Show answer
Merge sort; merge sort

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.

Question 11medium

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
O(d(n + b)); when d and b are constants (or b = O(n) and d constant)

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.

Question 12hard

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
Two indices + temp array, take from left when a[i] <= a[j], copy back

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.

Review card

Merge sort: divide, sort, merge. O(n log n) in EVERY case. Stable. O(n) extra memory. T(n) = 2T(n/2) + O(n) → O(n log n). Merge: at least min(n1,n2), at most n1+n2-1 comparisons. Quicksort: partition, the pivot lands in its correct place. Average O(n log n), worst O(n^2). Worst case: sorted array + first/last element as pivot → n(n-1)/2 comparisons. Fix: random / median-of-three. Lomuto: i=lo-1, if a[j]<p then i++ swap; at the end the pivot goes to a[i+1]. Heapsort: buildHeap O(n) + n times deleteMax O(log n) → O(n log n) in every case. In place, not stable. Max-heap 0-indexed: children 2i+1, 2i+2; last internal node n/2-1. Lower bound: comparison sorting needs ceil(log2 n!) = Omega(n log n) in the worst case. 5 elements → 7. Counting sort: O(n + k), when the key range is small. Place from right to left → stable. Radix (LSD): start from the rightmost digit, every pass must be STABLE. O(d(n+b)). Stable: merge, insertion, counting, radix. Not stable: quick, heap, selection.