DAY 10 / 30 · WEEK 2

Merge Sort

Goal of the day

Concept

Split a deck of cards in half. Then split each half in half again. Keep going until every pile has exactly one card — a pile of one card is trivially "sorted." Now merge piles back together two at a time: look at the top card of each pile, take the smaller one, repeat. Merging two already-sorted piles into one sorted pile only takes one pass through both — that's Day 4's two-pointer merge, reused here as the combining step.

The splitting takes log n levels (each split halves the size, and you can only halve n about log₂n times before reaching size 1). At every level, merging all the pieces back together costs O(n) total work. log n levels × O(n) work per level = O(n log n) — the same shape of growth curve you saw on Day 2, and genuinely faster than O(n²) for any reasonably large input.

Diagram

Split (recursion goes down) [5, 2, 8, 1] [5, 2] [8, 1] [5] [2] [8] [1] Merge (recursion returns back up) [5] [2] [2, 5] ...and so on, all the way back up to [1, 2, 5, 8] — one merge per level.

Interactive visualization: full sort, split + merge

being merged (this level) just placed fully merged, this range done
Press Play or Step to begin.

The code

function mergeSort(arr) {
  if (arr.length <= 1) return arr; // base case: 1 element is already sorted
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));   // recursively sort left half
  const right = mergeSort(arr.slice(mid));     // recursively sort right half
  return merge(left, right);                   // combine two sorted halves
}

function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
  while (i < left.length && j < right.length) {
    if (left[i] <= right[j]) result.push(left[i++]);
    else result.push(right[j++]);
  }
  while (i < left.length) result.push(left[i++]);   // leftover left elements
  while (j < right.length) result.push(right[j++]); // leftover right elements
  return result;
}

Line by line:

Dry run: merge([2, 5], [1, 8])

ijleft[i]right[j]Actionresult
00211 < 2 → take right[1]
01282 < 8 → take left[1, 2]
11585 < 8 → take left[1, 2, 5]
218left exhausted → take remaining right[1, 2, 5, 8]

Complexity

ApproachTimeSpaceWhy
Merge sortO(n log n)O(n)log n levels of splitting, O(n) work merging at each level; needs extra arrays to merge into.
Bubble/Selection/Insertion (Day 8)O(n²)O(1)No splitting — comparisons scale with n², not n log n.

Merge sort trades space for time: it needs O(n) extra memory for the merge step, but its time complexity is a full complexity class better than Day 8's sorts — the gap between O(n log n) and O(n²) that Day 2's growth chart showed becomes very real for large inputs.

Interview corner

How to talk through it out loud: narrate the three-part shape of every divide-and-conquer solution — "split the problem in half, recursively solve each half, then combine the two answers." Naming which part is the base case, which is the divide step, and which is the combine step shows structural understanding, not just a memorized implementation.

Practice problems

1. Merge Sorted Array (Easy — LeetCode: Merge Sorted Array)

Merge two sorted arrays into the first one, in place (the first array has extra trailing space to hold the result).

2. Sort an Array (Medium — LeetCode: Sort an Array)

Sort an array of integers in ascending order — implement the sort yourself.

3. Merge k Sorted Lists (Hard — LeetCode: Merge k Sorted Lists)

Given k sorted linked lists, merge them into one sorted list. (A preview — we build linked lists properly on Day 15. For now, just think through the approach in terms of arrays.)

Quiz

1. What's the time complexity of merge sort?

2. Before calling merge(left, right), what must be true?

3. What's the extra space complexity of merge sort?

4. Which comparison in merge() keeps merge sort stable?

5. Why does splitting an array in half repeatedly take log n levels?