16  Sorting Algorithms

17 Sorting Algorithms

Week 7, Session 2. CSS 342.

17.1 Warmup

Sort Colors (#75). Given nums of 0s, 1s, and 2s, sort in place — without using a sort function.

class Solution {
 public:
  void sortColors(vector<int>& nums) {
    int lo = 0, mid = 0, hi = nums.size() - 1;
    while (mid <= hi) {
      if (nums[mid] == 0) swap(nums[lo++], nums[mid++]);
      else if (nums[mid] == 2) swap(nums[mid], nums[hi--]);
      else ++mid;
    }
  }
};

That is the Dutch National Flag three-way partition. O(n) one pass, O(1) extra space. Beautiful.

17.2 Learning objectives

  1. Implement bubble sort, selection sort, and insertion sort and state their complexities.
  2. Implement merge sort recursively and derive its O(n log n) complexity.
  3. Compare sorting algorithms on time complexity, space complexity, stability, and practical performance.
  4. Trace through each algorithm on a 6-element array.
  5. Explain why no comparison-based sort can beat O(n log n) in the worst case.

17.3 Bubble sort — O(n²)

Repeatedly pass through the array, swapping adjacent out-of-order pairs.

void bubbleSort(vector<int>& v) {
  int n = v.size();
  for (int i = 0; i < n - 1; ++i) {
    bool swapped = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (v[j] > v[j + 1]) {
        swap(v[j], v[j + 1]);
        swapped = true;
      }
    }
    if (!swapped) return;        // early exit if already sorted
  }
}

After i outer iterations, the last i elements are in their final position. The early-exit swapped flag gives O(n) best case on an already-sorted input.

17.4 Selection sort — always O(n²)

Find the minimum, swap to front. Find the next-smallest, swap to position 1. Repeat.

void selectionSort(vector<int>& v) {
  int n = v.size();
  for (int i = 0; i < n - 1; ++i) {
    int minIdx = i;
    for (int j = i + 1; j < n; ++j) {
      if (v[j] < v[minIdx]) minIdx = j;
    }
    swap(v[i], v[minIdx]);
  }
}

Always Θ(n²) — no early exit possible because every minimum-finding pass scans the whole remainder.

17.5 Insertion sort — O(n²) worst, O(n) best

Build sorted order one element at a time.

void insertionSort(vector<int>& v) {
  for (int i = 1; i < (int)v.size(); ++i) {
    int key = v[i];
    int j = i - 1;
    while (j >= 0 && v[j] > key) {
      v[j + 1] = v[j];
      --j;
    }
    v[j + 1] = key;
  }
}

On already-sorted input, the inner while never executes. Best case: O(n). For nearly sorted input, insertion sort is shockingly fast — better than O(n log n) in practice. That is why std::sort falls back to insertion sort for small subarrays.

17.6 Merge sort — O(n log n) guaranteed

Divide in half, sort each half, merge.

void merge(vector<int>& v, int lo, int mid, int hi) {
  vector<int> tmp;
  tmp.reserve(hi - lo + 1);
  int i = lo, j = mid + 1;
  while (i <= mid && j <= hi) {
    if (v[i] <= v[j]) tmp.push_back(v[i++]);
    else tmp.push_back(v[j++]);
  }
  while (i <= mid) tmp.push_back(v[i++]);
  while (j <= hi) tmp.push_back(v[j++]);
  for (int k = 0; k < (int)tmp.size(); ++k) v[lo + k] = tmp[k];
}

void mergeSort(vector<int>& v, int lo, int hi) {
  if (lo >= hi) return;
  int mid = lo + (hi - lo) / 2;
  mergeSort(v, lo, mid);
  mergeSort(v, mid + 1, hi);
  merge(v, lo, mid, hi);
}

17.6.1 Why O(n log n)?

The recurrence is T(n) = 2T(n/2) + O(n).

  • Two recursive calls on inputs of size n/2.
  • The merge step is O(n).

Drawing the recursion tree: log n levels, O(n) work per level, total O(n log n).

              n              ← O(n) work
            /   \
          n/2   n/2          ← 2 × O(n/2) = O(n) work
         / \   / \
       n/4 n/4 n/4 n/4       ← 4 × O(n/4) = O(n) work
       ...
         (log n levels)

Total work: n × log n.

Merge sort’s space cost is O(n) — the temporary buffer. That is why std::sort uses introsort (quicksort) — it sorts in place, at the cost of an O(n²) worst case that introsort prevents by switching to heapsort.

17.7 Quicksort — O(n log n) average, O(n²) worst

We will not implement quicksort in full here (343 covers it more deeply). The idea: pick a pivot, partition the array so smaller elements go left and larger go right, recurse on each side.

  • Average case (random pivot): O(n log n).
  • Worst case (pivot is always min or max): O(n²).

In practice, quicksort with a randomized pivot or median-of-three is faster than merge sort. That is what std::sort uses.

17.8 Comparison table

Algorithm Best Average Worst Space Stable In place
Bubble O(n) O(n²) O(n²) O(1) Yes Yes
Selection O(n²) O(n²) O(n²) O(1) No Yes
Insertion O(n) O(n²) O(n²) O(1) Yes Yes
Merge O(n log n) O(n log n) O(n log n) O(n) Yes No
Quick O(n log n) O(n log n) O(n²) O(log n) No Yes
Heap O(n log n) O(n log n) O(n log n) O(1) No Yes

Stable = preserves order of equal elements. Matters when sorting by a secondary key.

17.9 The Ω(n log n) lower bound

Any comparison-based sort makes a sequence of pairwise comparisons. Each comparison gives one bit of information. There are n! possible orderings of n elements. To distinguish them you need at least log₂(n!) ≈ n log n bits. So no comparison sort can beat O(n log n) in the worst case.

(Non-comparison sorts like radix sort can beat it — they exploit the structure of the keys.)

17.10 Try it

Implement insertion sort. Add a print statement at the start of each outer iteration so you can watch it work. Then sort {5, 2, 8, 1, 9, 3} and trace the output.

Now sort {1, 2, 3, 4, 5} (already sorted). Confirm the inner while never executes. That is the O(n) best case.

17.11 Self-check

1. Which sort guarantees O(n log n) worst case without using extra O(n) space?
d. Heap sort is in-place AND has O(n log n) worst case. Merge needs O(n) extra; quicksort has O(n²) worst case.
2. The merge step of merge sort takes:
c. Each element is examined once. Two pointers walking down two sorted halves of total length n.

17.12 Challenges

  1. Implement merge sort. Verify on random arrays.
  2. Merge Two Sorted Lists (#21) — the merge step on linked lists.
  3. Sort an Array (#912) — implement merge sort or quicksort.

17.13 Where this fits

Sorting is the worked example for big-O. The next chapter — induction — provides the formal machinery for proving that recursive algorithms (like merge sort) actually work.

You are here Coming up
Bubble, selection, insertion, merge, comparison table Chapter 15: mathematical induction