LearnAbout

Sorting Intuition

On this page
Quick reference
Merge SortO(n log n)
Quick SortO(n log n) average, O(nΒ²) worst case
Heap Sort (preview β€” built in Phase 5)O(n log n)
The Idea

Sorting is a one-time investment: pay a cost once to put the data in order, and a lot of questions that would otherwise be expensive (find the median, check for duplicates, merge two lists) get cheap afterward. There are several strategies for doing the sorting itself, and they make different trade-offs.

Merge Sort splits the array in half, recursively sorts each half, then merges the two sorted halves back together β€” the exact divide-and-conquer shape from Recursion Basics's pow(x, n), just applied to sorting instead of exponentiation. Quick Sort instead picks a pivot value, partitions the array into "less than the pivot" and "greater than the pivot" in place, and recurses into each side. A third strategy, Heap Sort, always pulls out the current smallest (or largest) remaining element one at a time via a heap β€” but a heap is a structure this curriculum hasn't built yet (it's Phase 5's whole subject), so Heap Sort is previewed here by name and comparison only, not implemented. That's intentional, not a gap: you can't build Heap Sort's engine before you've built the engine itself.

All three land at O(n log n) time in typical cases β€” the real decision is which trade-off (extra memory, worst-case risk, or preserving equal elements' relative order) fits the situation, not which one is "fastest."
Build It
Two sorting strategies
// The shared divide-and-conquer skeleton (same shape as Recursion Basics):
function sort(arr):
    if arr is trivially small (0 or 1 elements):    // base case β€” already sorted
        return arr
    ... split or partition the problem ...
    ... recursively sort the smaller piece(s) ...
    ... combine the results ...
Merge Sort β€” divide, recursively sort, merge
function mergeSort(arr):
    if length(arr) <= 1:
        return arr                          // base case: 0 or 1 elements is already sorted
    mid = length(arr) // 2
    left = mergeSort(arr[0:mid])            // recursively sort the left half
    right = mergeSort(arr[mid:])            // recursively sort the right half
    return merge(left, right)

function merge(left, right):
    result = []
    i, j = 0, 0
    while i < length(left) and j < length(right):
        if left[i] <= right[j]:              // <= (not <) keeps equal elements in original
            result.append(left[i])            // relative order β€” this is what "stable" means
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.append(remaining elements of left)   // whichever side has leftovers
    result.append(remaining elements of right)
    return result
Quick Sort β€” pick a pivot, partition, recurse
function quickSort(arr, lo, hi):
    if lo >= hi:
        return                              // base case: 0 or 1 elements is already sorted
    pivotIndex = partition(arr, lo, hi)
    quickSort(arr, lo, pivotIndex - 1)      // recurse left of the pivot
    quickSort(arr, pivotIndex + 1, hi)      // recurse right of the pivot

function partition(arr, lo, hi):
    pivot = arr[hi]                          // pick the last element as the pivot (simplest choice)
    boundary = lo                            // everything before 'boundary' is < pivot
    for i from lo to hi - 1:
        if arr[i] < pivot:
            swap(arr[i], arr[boundary])
            boundary += 1
    swap(arr[boundary], arr[hi])             // put the pivot in its final sorted spot
    return boundary
Unlike Merge Sort, Quick Sort's partitioning happens in place on the original array β€” no separate result array is built, which is where its space advantage comes from.
Know It

"Stable" means equal elements keep their original relative order after sorting β€” worth caring about when you're sorting records (like people) by one field (like age) and want ties to stay in their original order.

OperationTimeSpaceWhy
Merge SortO(n log n)O(n)log n levels of splitting, O(n) work to merge at each level β€” but merging needs a separate output array, which is the O(n) extra space.
Quick SortO(n log n) average, O(nΒ²) worst caseO(log n) average, O(n) worst casePartitioning is in place (no extra array) β€” the space is the recursion stack, one frame per level. A consistently unlucky pivot choice can make one side of every partition empty, turning log n levels of recursion into n levels β€” the same pathological input that drives time to O(nΒ²) also drives the recursion stack to O(n).
Heap Sort (preview β€” built in Phase 5)O(n log n)O(1)Repeatedly pulling the current smallest/largest element from a heap costs O(log n) per pull, n pulls total β€” no extra array needed, but it needs a working heap first.
Break It

Quick Sort's O(nΒ²) worst case on sorted input

the array is already sorted (or reverse-sorted) and the pivot is always the last element

Always picking arr[hi] as the pivot on an already-sorted array means every partition puts zero elements on one side and everything else on the other β€” the recursion doesn't halve the problem, it only shrinks it by one each time, giving O(n) levels instead of O(log n): O(nΒ²) total. Picking a random pivot, or the median of a few sampled elements, makes this unlucky case astronomically unlikely instead of guaranteed by a predictable input.

Merge Sort's O(n) extra space is a real cost

sorting data too large to comfortably duplicate in memory

Merge Sort needs a second array to merge into β€” that's not free at scale. For huge datasets, doubling the memory footprint can matter more than the time complexity does; Quick Sort's O(log n) space (just the recursion stack) or Heap Sort's O(1) space can be the deciding factor even though all three share the same O(n log n) time.

Assuming a sort is stable when it isn't

sorting records by one field while relying on another field's order being preserved

Quick Sort's swap-based partitioning can reorder equal elements relative to each other β€” it's not stable by default. Code that sorts a list of records by, say, department, expecting people within the same department to stay in their original order (maybe already sorted by name), can silently scramble that secondary order if the sort used isn't stable. Check which guarantee your language's or library's sort actually makes; don't assume.

Sorting records with the wrong or an unstable comparator

the comparator doesn't fully and consistently define an order

A comparator that isn't consistent β€” for instance, one that sometimes says A < B and sometimes says B < A for the same pair, or one that doesn't handle equal elements predictably β€” can make different sorting algorithms produce different (and sometimes just wrong) results on the same data, since they all assume the comparator defines a single, consistent ordering to work toward.

Use It
Sort an Array
Implement Merge Sort or Quick Sort directly, exactly as built above β€” the reference exercise for the pattern itself, no cleverness needed beyond getting the recursion and the merge/partition steps right.
Medium
Merge Intervals
Sort the intervals by start time first (any comparison sort works). Once sorted, overlapping intervals are always neighbors, so a single pass merging adjacent ones is all that's left.
Medium
Kth Largest Element in an Array
Full Quick Sort partitions and recurses into both sides; Quickselect (its cousin) partitions once and then only recurses into whichever side contains the kth position, throwing away the other side's work entirely β€” the same "only look at what you need" saving that makes binary search faster than a full scan.
Medium
Sort Colors
The same three-pointer partition from Two Pointers, reframed: it's exactly Quick Sort's partition step with a pivot value of 1, splitting the array into less-than / equal / greater-than in a single pass.
Medium
Largest Number
The default "smaller number first" comparator is wrong here β€” sort with a custom comparator that compares two numbers by which order of concatenation (a+b vs b+a) produces the larger string, then join the sorted result.
Medium
Insertion Sort List
Walk the list one node at a time, and for each node find its correct position among the nodes already placed in sorted order β€” done with pointer rewiring instead of array shifting, the same node-relinking approach Phase 2's Linked Lists topic uses.
Medium
H-Index
Sort the citation counts (descending, or ascending and read from the back). Once sorted, walk through and find the last position where the citation count is still at least as large as the paper's rank from the top.
Medium
Relative Sort Array
Count occurrences of each value in arr1 (a frequency count, the pattern from Hashing), then rebuild the output by walking arr2 in order and emitting each value that many times, appending any leftovers (sorted normally) at the end.
Easy
Height Checker
Compare the array to a sorted copy of itself, position by position β€” any index where they differ is a student standing out of place.
Easy
Sort Array By Parity
The same partition idea as Sort Colors, simplified to two buckets instead of three: a slow pointer marks the next even-value slot, a fast pointer scans ahead and swaps evens forward.
Easy