Skip to content
Computer Science

Sorting Algorithms

How computers put things in order — and why some ways are faster than others.

9 min read·April 22, 2025

On this page

The problem with random order#

Imagine you have a thousand books stacked randomly. Finding any specific book requires checking them one by one — up to a thousand checks in the worst case. But if the books are sorted alphabetically, you can open to the middle, see which half your book is in, and recursively halve the problem. You'll find the book in at most 10 steps. That's the difference between O(n)O(n) and O(logn)O(\log n) search — an enormous practical difference when nn is large.

Sorting is such a fundamental operation that programming languages build it in: array.sort() in JavaScript, sorted() in Python. But how does it actually work? And why are some algorithms so much faster than others?

Bubble sort: the simple one#

Bubble sort makes repeated passes through the list. In each pass, it compares adjacent elements and swaps them if they're in the wrong order. After the first pass, the largest element is guaranteed to be at the end. After the second pass, the second-largest is in place. And so on.

It's called bubble sort because large elements "bubble up" to their correct positions like bubbles rising through water. It's intuitive, easy to implement, and terribly slow: O(n2)O(n^2) comparisons in the average and worst case.

For 1,000 elements: about 500,000 comparisons. For 1,000,000 elements: about 500 billion. It doesn't scale.

Why O(n log n) is the limit#

It turns out that no comparison-based sorting algorithm can do better than O(nlogn)O(n \log n) in the worst case. Here's why.

A sorting algorithm must distinguish between all n!n! possible orderings of the input (any permutation might be the input). Each comparison gives 1 bit of information — "is A less than B, or not?" To distinguish between n!n! possibilities using 1-bit questions, you need at least log2(n!)\log_2(n!) questions.

By Stirling's approximation: log2(n!)nlog2n\log_2(n!) \approx n \log_2 n. So you need at least O(nlogn)O(n \log n) comparisons. Merge sort and quicksort achieve this bound.

Merge sort: divide and conquer#

Merge sort splits the array in half, sorts each half recursively, then merges the two sorted halves into one sorted array. The merge operation is linear: you walk through both halves with two pointers, always taking the smaller element.

The recursion goes logn\log n levels deep. Each level does O(n)O(n) total work for all the merges. Total: O(nlogn)O(n \log n).

Merge sort is wonderfully predictable — it always runs in O(nlogn)O(n \log n), regardless of the input. Its disadvantage is that it requires O(n)O(n) extra memory for the merge step.

Quicksort: the practical winner#

Quicksort picks a "pivot" element and partitions the array: everything smaller than the pivot goes left, everything larger goes right. Then it recursively sorts each side.

In the best case, the pivot is always the median, splitting the problem in half each time — O(nlogn)O(n \log n). In the worst case (always picking the smallest or largest element as pivot), it degrades to O(n2)O(n^2).

Why is quicksort often faster in practice than merge sort, despite the same average complexity? Cache behavior. Quicksort operates in-place (no extra memory), accessing elements sequentially. Modern CPUs have hardware caches that make sequential memory access much faster than random access. The constant factor matters.

Real-world sort implementations (including Python's Timsort and many C++ implementations) use hybrid strategies: insertion sort for small arrays (where its overhead is low), quicksort or merge sort for larger ones.

Watch the animations. Notice how bubble sort and insertion sort take many more steps for the same array. Notice how quicksort sometimes gets unlucky with its pivot. The theoretical bounds match what you see.

Complexity in numbers#

The gap between O(n2)O(n^2) and O(nlogn)O(n \log n) sounds abstract until you see it plotted. Click "Run benchmark" below to run all four algorithms on arrays of increasing size and count exactly how many comparisons each one makes.

At n=160n = 160, bubble sort typically needs 10–15× more comparisons than merge sort. At n=1,000n = 1{,}000, that ratio would be closer to 100×. This is why algorithmic complexity matters: not as a theoretical exercise, but because the difference between choosing the right algorithm and the wrong one compounds dramatically as data grows.

Toggle "Theoretical curves" to overlay the expected n2/2n^2/2 and nlog2nn \log_2 n shapes — the measured benchmark data should track them closely, with quicksort beating merge sort on most random inputs due to better cache behavior despite identical asymptotic complexity.

Key takeaways
  • Sorting is the gateway to fast algorithms — binary search, deduplication, and range queries all assume sorted data.
  • Simple sorts (bubble, insertion) are O(n2)O(n^2); the divide-and-conquer sorts (merge, quick) are O(nlogn)O(n \log n), and that gap explodes as data grows.
  • O(nlogn)O(n \log n) is the proven lower bound for comparison sorts — you can't distinguish n!n! orderings in fewer comparisons.
  • Asymptotics aren't everything: quicksort usually beats merge sort on random data thanks to cache-friendly in-place work, despite the same big-O.
  • Worst cases matter — naive quicksort degrades to O(n2)O(n^2) on sorted input, which is why pivot choice is a real engineering decision.
Check your understanding
1. Why is the O(n log n) lower bound considered a fundamental limit for comparison-based sorting algorithms?
2. Why does quicksort often outperform merge sort in practice despite both having O(n log n) average-case complexity?
3. Under what condition does quicksort's performance degrade to O(n^2), and what does this reveal about algorithm design?
0 / 3 answered

Share this article

Share on X