Sorting
Put things in order once, and a dozen hard problems turn easy.
▶ guided lesson⚡ 1-minute summary↻ 14 recall cards
before this: Arrays & Strings · Big-O · Recursion · Two Pointers
01 · why it exists
Why this exists, and the weight it carries
since1956–1962
Insertion sort (and the other O(n²) sorts)
E. H. Friend (1956 survey) · Kenneth E. Iverson (the name "bubble sort", 1962)
The simple sorts are older than their names. In 1956 E. H. Friend's survey in the Journal of the ACM compared methods such as inserting and exchanging — and found exchanging needs at least twice as many comparisons as inserting. Owen Astrachan's 2003 history of bubble sort found early books calling it "exchange sorting"; the first use of the name bubble sort in print he could find is Iverson's 1962 book A Programming Language. Knuth's verdict: bubble sort "seems to have nothing to recommend it, except a catchy name and the fact that it leads to some interesting theoretical problems".
the problem it solvedSorting small amounts of data on early machines with almost no spare memory — and, today, finishing tiny or nearly sorted ranges faster than any O(n log n) sort can.
its jobs today
- Inside Java's own sorts: TimSort sorts arrays under 32 elements with binary insertion sort, and the dual-pivot quicksort switches to insertion-sort variants for small ranges (JDK 17 source).
- Keeping a short list sorted while items arrive one at a time — a top-10 leaderboard, a recent-files menu: each new item slides into place.
the weight it carries
Illustrative: 10,000 contacts already sorted by name, plus 10 new ones added at the end (n = 10,010). Insertion sort's work is n − 1 comparisons plus one shift per out-of-order pair: at most 10,009 + 10 × 10,009 ≈ 1.1 lakh steps. The same contacts in random order: about n²/4 ≈ 2.5 crore shifts.
without it
Library sorts would waste recursion and bookkeeping on 5-element ranges, and nearly sorted data would cost as much as random data.
when · why · how
- when
- n is tiny (a few dozen), the data is nearly sorted, or items arrive one by one into a list that is already sorted.
- why
- Work = n + number of inversions: O(n) on sorted input, O(n²) on reversed input; O(1) extra space and stable. Bubble sort never beats it; selection sort only wins when writes are very expensive (at most n − 1 swaps).
- how
for (int i = 1; i < n; i++) { int key = a[i], j = i - 1; while (j >= 0 && a[j] > key) a[j + 1] = a[j--]; a[j + 1] = key; }— the strict>keeps it stable.
since1945
Merge sort
John von Neumann
Merge sort is one of the oldest computer algorithms: Knuth credits John von Neumann with merge sorting in 1945, for the planned EDVAC computer. The merge card in the Two Pointers chapter tells that 1945 story — the handwritten merge routine and how Knuth studied it. This chapter adds the recursion around the merge: split in half, sort each half, merge. log₂ n levels, O(n) work per level.
the problem it solvedSorting in a guaranteed O(n log n) using only sequential passes — which is also why it can sort data bigger than memory, one chunk at a time.
its jobs today
- Java's object sort —
Arrays.sort(Object[]),Collections.sort,List.sort— is TimSort, a merge sort that first looks for runs that are already sorted. - Databases sort tables bigger than RAM (
ORDER BY, sort-merge joins) with external merge sort: sort chunks in memory, then merge them. - Counting problems: inversion counts and reverse pairs are counted inside the merge step.
the weight it carries
Illustrative: sorting 10 lakh (1,000,000) UPI transactions by time. Merge sort: ⌈log₂ 1,000,000⌉ = 20 levels × 10⁶ moves ≈ 2·10⁷ steps, whatever the input. Insertion sort on random order: about n²/4 = 2.5·10¹¹ shifts — over 10,000× more.
without it
No guaranteed O(n log n) stable sort: sorting by a second key would scramble the first, and files bigger than RAM couldn't be sorted at all.
when · why · how
- when
- You need stability, a guaranteed O(n log n), a linked list sorted, or pairs counted across the array (inversions, reverse pairs).
- why
- O(n log n) in every case and stable. Costs O(n) extra memory on arrays (the helper), but O(1) on linked lists, where merging just relinks nodes.
- how
- Allocate one
int[] auxup front; recurse on[lo, mid]and[mid + 1, hi]; merge taking the right element only when it is strictly smaller (aux[j] < aux[i]).
since1959–1961
Quicksort
C. A. R. (Tony) Hoare
In 1959, as a student at Moscow State University, Tony Hoare came up with a new way to sort. Back in England, at Elliott Brothers in 1960, he told his boss Pat Shackleton it would usually beat the brand-new Shellsort; Shackleton bet him sixpence it wouldn't — and lost, though by Hoare's own account the method was very hard to explain. At an ALGOL 60 course around Easter 1961 he learnt recursion and wrote it in a few lines; it was published that July as Algorithm 64: Quicksort, with a full paper in 1962.
the problem it solvedSorting fast in place: no helper array and few memory moves — the reason a quicksort is still Java's default for primitive arrays.
its jobs today
- Java sorts
int[],long[],float[]anddouble[]— and smallshort[]/char[]arrays — with a quicksort descendant: dual-pivot quicksort. - Its partition step is reused on its own: quickselect for the k-th element, the 3-way split behind Sort Colors and Wiggle Sort II.
the weight it carries
Illustrative: 10 lakh random ints. Random-pivot quicksort expects about 2·n·ln n ≈ 2 × 10⁶ × 13.8 ≈ 2.8·10⁷ comparisons, in place. A pivot that is always the largest value: n(n − 1)/2 ≈ 5·10¹¹ comparisons, and a recursion 10 lakh calls deep.
without it
Fast in-place sorting of primitives would need merge sort's extra O(n) memory, or heap sort's slower, cache-unfriendly jumps.
when · why · how
- when
- Sorting primitives in place when stability doesn't matter; whenever a problem needs a partition step (k-th element, 3-way split).
- why
- O(n log n) expected with an O(log n) stack and very good cache behaviour; O(n²) when pivots are bad — a random pivot makes that astronomically unlikely.
- how
int pivot = a[lo + rnd.nextInt(hi - lo + 1)];then partition (Lomuto, Hoare, or 3-waylt / i / gtfor duplicates) and recurse on both sides. In real code: just callArrays.sort(int[]).
since1961
Quickselect
C. A. R. (Tony) Hoare
Hoare published Algorithm 65: Find in the same July 1961 issue of Communications of the ACM as Quicksort. It partitions exactly like Quicksort, then keeps only the side that holds the k-th position and throws the other side away. Today it's called quickselect.
the problem it solvedFinding the k-th smallest value — a median, a percentile — without sorting everything.
its jobs today
- Medians and percentiles: the p95 response time on a monitoring dashboard is the k-th smallest of all response times.
- Top-k when the order inside the answer doesn't matter: the k closest points, the k-th largest element (LC 215).
the weight it carries
Illustrative: the median of 10 lakh response times. Sorting first: about 2·10⁷ comparisons. Quickselect: n + n/2 + n/4 + … ≈ 2n if every pivot lands in the middle; with random pivots, under 3.4n ≈ 3.4·10⁶ comparisons expected.
without it
Every median or k-th question would pay for a full O(n log n) sort, or O(n log k) with a heap.
when · why · how
- when
- You need one k-th value (or the k smallest in any order) and all the data is already in memory.
- why
- O(n) expected and O(1) extra space — but O(n²) worst case, and it reorders the array. For a stream, a size-k heap (O(n log k)) is the better tool.
- how
- Loop: random pivot, 3-way partition into
< | = | >, then moveloorhitowards the target indexn − k. No recursion needed.
since1954
Counting sort
Harold H. Seward (MIT)
In a May 1954 MIT report on sorting for business data processing (Project Whirlwind report R-232), Harold H. Seward described sorting by counting. Count how many times each key appears, turn the counts into starting positions, then drop every record straight into its slot — no comparisons at all. Knuth's The Art of Computer Programming traces this "distribution counting" idea to Seward.
the problem it solvedSorting large numbers of records whose keys are small integers in linear time, on 1950s machines with very little memory.
its jobs today
- Java's
Arrays.sortitself counting-sorts largebyte[],short[]andchar[]arrays (JDK 17 source: more than 64 bytes, or more than 1,750 shorts or chars). - Every pass of radix sort is a stable counting sort on one digit.
- Interview staples: Sort Colors, H-Index and top-k-frequent buckets are all counts indexed by value.
the weight it carries
Illustrative: 10 lakh exam marks from 0 to 100. Counting sort: 10⁶ tallies + 101 buckets + 10⁶ writes ≈ 2·10⁶ steps. Merge sort: about n·log₂ n ≈ 2·10⁷ comparisons — roughly 10× more.
without it
Small-key data — ages, marks, letters, colours — would pay the full n log n comparison price for nothing.
when · why · how
- when
- Integer keys in a small known range k, with k not much bigger than n: marks, ages, letters, 0/1/2.
- why
- O(n + k) time and space — beats O(n log n) when k is small, useless when k is 10⁹ (the count array alone won't fit).
- how
int[] count = new int[max - min + 1];tally, turn counts into prefix sums, then place values right to left intooutfor a stable sort (or just rewrite each valuecounttimes when nothing is attached).
since1890s–1954
Radix sort
Herman Hollerith (punched-card machines) · unnamed card-sorter operators · Harold H. Seward (computer version, 1954)
Radix sorting began on punched-card machines, not computers. Hollerith's electric tabulating machines processed the 1890 US census, and Knuth calls Hollerith's sorting machine the basis of radix sorting. A card sorter dropped cards into pockets by one column at a time. The non-obvious trick of starting with the units column was probably found by an unknown operator; Knuth notes it in IBM's sorter manual of 1936, with a first known mention by L. J. Comrie in 1930. In 1954 Seward made it practical on computers: counting first fixes every bucket's size, so one helper array is enough.
the problem it solvedSorting stacks of punched cards by multi-digit keys (dates, account numbers) with a machine that could only read one column at a time.
its jobs today
- Sorting huge arrays of fixed-width keys — 32/64-bit integers, IDs, IP addresses — in a few linear passes; NVIDIA's CUB library ships
DeviceRadixSortfor GPUs. - Fixed-length codes such as 6-digit PIN codes or 10-digit mobile numbers can be sorted digit by digit, with no comparisons.
the weight it carries
Illustrative: 10 lakh 10-digit mobile numbers stored as long (each < 2³⁴). Base 10: 10 counting passes × (10⁶ + 10) ≈ 10⁷ steps. Base 2¹⁶ = 65,536: ⌈34 / 16⌉ = 3 passes × (10⁶ + 65,536) ≈ 3.2·10⁶ steps. Merge sort: about 2·10⁷ comparisons.
without it
Fixed-width keys would always pay the n log n comparison price, and punched-card offices would have had no mechanical way to sort multi-column keys.
when · why · how
- when
- Many keys of fixed width (ints, longs, fixed-length codes), with n large enough that d passes beat log₂ n comparisons per item.
- why
- O(d·(n + b)) for d digits in base b — linear for a fixed d — but it needs O(n + b) extra space, and every pass must be stable or the earlier digits get scrambled.
- how
- Least significant digit first: for each
exp(kept as along), run a stable counting sort on(x / exp) % 10intoout, copy back, thenexp *= 10. Negative numbers need an offset first.
since2002
TimSort (Java's object sort)
Tim Peters (Python) · Joshua Bloch (Java port, Java 7)
In July 2002 Tim Peters replaced Python's list sort with an "adaptive stable mergesort" and described it in a text file (today's listsort.txt in CPython); it shipped with Python 2.3 and became known as TimSort.
Real data is often partly sorted, so TimSort first finds runs that are already in order, extends short runs with binary insertion sort, and merges runs with a trick called galloping.
Joshua Bloch ported it to Java (OpenJDK issue JDK-6804124), and since Java 7 it sorts every object array and List.
the problem it solvedSorting real-world data — often partly sorted already — with far fewer comparisons than a plain merge sort, while staying stable.
its jobs today
Arrays.sort(Object[]),Collections.sortandList.sortsince Java 7: stable sorts of records by any comparator.- Python's
sorted()andlist.sort(), where the algorithm was born. - Merging sorted pieces: Java's docs note you can merge sorted arrays by concatenating them and sorting the result — TimSort just merges the runs.
the weight it carries
Illustrative: 10 lakh orders already sorted by time, plus 100 new ones at the end. TimSort finds the long run, sorts the short tail and merges: about 10⁶ comparisons (Java's docs: "approximately n" for nearly sorted input). A merge sort that ignores existing order still does about (n/2)·log₂ n ≈ 10⁷ here.
without it
Partly sorted data — logs, feeds, spreadsheet columns — would cost as much to sort as random data, and Java would lack a fast stable sort for objects.
when · why · how
- when
- Any time you sort objects in Java (
list.sort(cmp)), especially by several keys or when the data is already partly sorted. - why
- Stable, O(n log n) worst case, O(n) on sorted input; needs up to n/2 extra references. A comparator that breaks its contract can make it throw
IllegalArgumentException("Comparison method violates its general contract!"). - how
people.sort(Comparator.comparingInt(Person::age).thenComparing(Person::name));— never(a, b) -> a - b.
since2009
Dual-pivot quicksort (Java's primitive sort)
Vladimir Yaroslavskiy, with Jon Bentley and Joshua Bloch
In 2009 Vladimir Yaroslavskiy proposed a quicksort with two pivots that splits a range into three parts at once.
It beat Java 6's classic quicksort in timing tests, so OpenJDK adopted it (JDK-6880672, November 2009) and Java 7 shipped it in 2011 for int[], long[], float[], double[] and smaller short[]/char[] arrays (big byte[], short[] and char[] arrays get a counting sort).
Later analyses by Sebastian Wild and Markus Nebel found that comparisons don't explain the win: with plain random pivots it makes about 5% fewer (1.9 n ln n vs 2 n ln n), but with the pivot sampling Java actually uses it makes more than Java 6's quicksort. What it saves is memory traffic — about 11% fewer array elements scanned, in line with the roughly 10% speed-up seen in timing tests. JDK 14 refreshed it again (JDK-8226297), adding a heap sort fallback.
the problem it solvedMaking the default sort for primitive arrays faster than classic one-pivot quicksort on modern hardware.
its jobs today
Arrays.sort(int[]),Arrays.sort(long[]),Arrays.sort(double[])— every primitive array you sort in Java goes through it.- In the JDK 17 source it is a whole toolbox: insertion sort for small ranges, merging of already-sorted runs, counting sort for small types, heap sort as a safety net, and a parallel version behind
Arrays.parallelSort.
the weight it carries
Illustrative: 10 lakh random ints, using the leading terms of Wild's averages (n·ln n ≈ 1.38·10⁷). With the pivot sampling each JDK really uses, Java 7's dual-pivot sort makes more comparisons than Java 6's quicksort — 1.70 vs 1.57 n ln n ≈ 2.35·10⁷ vs 2.17·10⁷ — but scans fewer array elements: 1.40 vs 1.57 n ln n ≈ 1.94·10⁷ vs 2.17·10⁷, about 11% less memory traffic. Memory, not comparisons, is where modern CPUs wait.
without it
Every Arrays.sort(int[]) would run a slower classic quicksort — and, before the JDK 14 heap sort fallback, a crafted input could push it towards O(n²).
when · why · how
- when
- Whenever you sort a primitive array in Java and stability doesn't matter (equal ints can't be told apart anyway).
- why
- O(n log n) on all inputs in current JDKs, in place apart from run merging; faster than classic quicksort in practice. Not stable — use object sorts when equal keys carry other data.
- how
Arrays.sort(nums);orArrays.sort(nums, from, to);For descending ints, sort ascending and read backwards.
since?
The Ω(n log n) limit for comparison sorts
A textbook argument rather than one inventor's paper: Knuth (TAOCP Vol. 3, §5.3.1) and Cormen et al. (CLRS, §8.1) both present it as a decision tree with n! leaves. We couldn't pin down who first wrote it down, so we don't name anyone.
its jobs today
- Tells you when to stop optimising: no comparator-based sort —
Arrays.sort,Collections.sortor your own — can beat about n log₂ n comparisons in the worst case. - Tells you how to escape it: counting, radix and bucket sorts never compare two keys, so the limit doesn't apply to them.
the weight it carries
Illustrative: n = 10 lakh. log₂(n!) ≈ n·log₂ n − 1.44·n ≈ 1.99·10⁷ − 1.44·10⁶ ≈ 1.85·10⁷ comparisons is the floor for any comparison sort. Merge sort uses at most about n·⌈log₂ n⌉ = 2·10⁷ — only about 8% above it.
without it
You could chase an impossible "O(n) comparison sort" forever — and miss that small integer keys can be sorted in O(n) by counting instead.
when · why · how
- when
- An interviewer asks "can you sort faster?", or you're choosing between a comparison sort and counting/radix sort.
- why
- n items have n! possible orders, and each yes/no comparison can at best halve the possibilities, so ⌈log₂ n!⌉ = Θ(n log n) comparisons are needed. Only extra knowledge about the keys (small range, fixed width) beats it.
- how
- Say it in one line: "n! orders, each comparison halves them, so log₂ n! ≈ n log n." Then, for bounded ints, write a counting sort with
int[] count = new int[k].
02 · plain english
The idea in 30 seconds
Sorting puts equal and nearby values next to each other — duplicates, overlaps and gaps become neighbours you can see in one pass.
Simple sorts compare neighbours and cost O(n²). Divide and conquer (merge, quick, heap) gets to O(n log n) — the best any comparison sort can do.
If values are small integers you can skip comparisons entirely and count them in O(n + k).
picture this
Clicking a column header in File Explorer or Excel: no row changes, only the order does — and suddenly the biggest file is on top, duplicate names sit together, and a missing invoice number jumps out. Sorting is that click, done in code.
03 · remember
What you must know
- Comparison sorts can't beat O(n log n) in the worst case: n! possible orders need at least log₂(n!) ≈ n log n yes/no questions to tell apart.
- Stable = equal keys keep their original order. It matters when you sort records by one field after another (sort by name, then stably by age).
- In-place = O(1) (or O(log n)) extra memory. Merge sort is not in-place on arrays; quick and heap sort are.
- Adaptive = faster on nearly-sorted input. Insertion sort (and TimSort, built on it) runs in O(n) on sorted data.
- Quicksort is fastest in practice but O(n²) with bad pivots — random pivots make that vanishingly unlikely; median-of-three helps on ordinary data, but crafted inputs can still force it.
- Merge sort is O(n log n) always, stable, and is the sort for linked lists and for counting things like inversions.
- Java:
Arrays.sort(int[])= dual-pivot quicksort (not stable, no one can tell equal ints apart).Arrays.sort(Object[])/Collections.sort= TimSort, stable. - Never write
(a, b) -> a - b— it overflows. UseInteger.compare(a, b)orComparator.comparingInt(...).
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
Bubble sort — the biggest value bubbles right
Neighbours swap until each pass parks the largest remaining value at the end (lime). A pass with no swaps ends the sort early.
Selection sort — pick the minimum, swap it to the front
Always n(n−1)/2 comparisons, but at most n−1 swaps. Watch a long-distance swap break stability.
Insertion sort — slide each new value into place
The left part is always sorted. Each new key slides left past bigger values. Try a nearly-sorted input: almost no work.
Merge sort — split, sort halves, zip them together
The lower layer is the helper array. Two pointers (i, j) walk the sorted halves; the smaller head is written back at k.
Merge sort on a linked list — relink, don't copy
Runs of width 1, 2, 4… are cut off and merged by changing next pointers only. No helper array, no recursion: O(1) extra space.
Quicksort — Lomuto partition
Values smaller than the pivot (amber) gather on the left. The pivot then drops between the zones and is final forever.
Counting sort — no comparisons at all
Tally into buckets, turn counts into end positions (prefix sums), then place right-to-left. The #ids show equal values keep their order.
05 · cost
Time & space, explained
| Best | Average | Worst | Extra space | Stable | In-place | why | |
|---|---|---|---|---|---|---|---|
| Bubble sort (early exit) | O(n) | O(n²) | O(n²) | O(1) | Yes | Yes | Best: one pass with no swaps on sorted input. Otherwise each pass fixes one value: (n−1)+(n−2)+… = n(n−1)/2 comparisons. |
| Selection sort | O(n²) | O(n²) | O(n²) | O(1) | No | Yes | It always scans the whole unsorted part to find the minimum — even if the array is sorted. Its long swaps can jump an element over its equal twin. |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) | Yes | Yes | Work = number of inversions. Sorted input has none (n−1 comparisons); reversed input has n(n−1)/2. |
| Merge sort (top-down) | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | No | log₂ n levels of halving; each level merges n values. The helper array costs n extra slots (plus O(log n) recursion). |
| Quicksort (random pivot) | O(n log n) | O(n log n) | O(n²) | O(log n) expected | No | Yes | Balanced pivots halve the range → log n levels of O(n) partitioning. Always picking the min/max gives n levels → O(n²) (and O(n) stack). |
| Heap sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No | Yes | Build a heap in O(n), then n extractions each sift down log n levels. Guaranteed, but cache-unfriendly so slower than quicksort in practice. |
| Counting sort (values 0..k) | O(n + k) | O(n + k) | O(n + k) | O(n + k) | Yes | No | One pass to count, one over the k buckets, one to place. No comparisons, so the n log n bound doesn't apply — but k must be small. |
| Radix sort (LSD, d digits, base b) | O(d·(n + b)) | O(d·(n + b)) | O(d·(n + b)) | O(n + b) | Yes | No | d stable counting passes, one per digit. For 32-bit ints in base 10, d ≤ 10 — effectively linear. |
TimSort (Java objects, List.sort) | O(n) | O(n log n) | O(n log n) | O(n) | Yes | No | Finds already-sorted runs (insertion sort for short ones) and merges them. Sorted input is one run → O(n). Merge buffer up to n/2. |
Dual-pivot quicksort (Java int[]) | O(n) | O(n log n) | O(n log n) | O(log n) stack; O(n) when it merges runs | No | Mostly | Two pivots split into three parts. Large, nearly sorted arrays are detected as runs and merged (sorted input → O(n)) with a temporary array. Since JDK 14 it also switches to heap sort when partitioning goes too deep, so the worst case is O(n log n). |
| Time | Extra space | why | |
|---|---|---|---|
| Sort, then index | O(n log n) | O(1)–O(n) | Simple and safe, but orders n values when you only need one position. |
| Min-heap of size k | O(n log k) | O(k) | Each of n values costs at most one log k push/pop. Great for streams. |
| Quickselect (random pivot) | O(n) avg, O(n²) worst | O(1) | Partition like quicksort but keep only one side: n + n/2 + n/4 + … ≈ 2n when pivots land near the middle. Random pivots give O(n) expected (under 3.4n comparisons). |
06 · java
Raw vs Collections Framework
Sorting numbers: write it vs call it
class TopDown { static void sort(int[] a) { int[] aux = new int[a.length]; // one helper array, reused by every merge sort(a, aux, 0, a.length - 1); } private static void sort(int[] a, int[] aux, int lo, int hi) { if (lo >= hi) return; // 0 or 1 element is already sorted int mid = lo + (hi - lo) / 2; sort(a, aux, lo, mid); // sort the left half sort(a, aux, mid + 1, hi); // sort the right half merge(a, aux, lo, mid, hi); } private static void merge(int[] a, int[] aux, int lo, int mid, int hi) { for (int k = lo; k <= hi; k++) aux[k] = a[k]; int i = lo, j = mid + 1; // i walks the left half, j the right half for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = aux[j++]; // left used up else if (j > hi) a[k] = aux[i++]; // right used up else if (aux[j] < aux[i]) a[k] = aux[j++]; // strict < : ties take left → stable else a[k] = aux[i++]; } }}class Library { static void demo(int[] nums, Integer[] boxed, List<String> words) { Arrays.sort(nums, 0, Math.min(3, nums.length)); // sort only the range [0, 3) Arrays.sort(nums); // primitives: dual-pivot quicksort, not stable (equal ints are indistinguishable) Arrays.sort(boxed, Collections.reverseOrder()); // objects: TimSort, stable; descending needs Integer[], not int[] Collections.sort(words); // List: TimSort (same as words.sort(null)) words.sort(Comparator.comparing(String::length)); // stable: equal lengths keep alphabetical order }}- In real code and most interviews, call
Arrays.sort— write a sort by hand only when the sort is the question (LC 912) or you need to hook into it (counting inversions). int[]sorts ascending only. For descending, either sort and walk backwards, or box toInteger[]and passCollections.reverseOrder()(slower: boxing).Collections.sort(list)andlist.sort(null)are the same thing: TimSort over the list's array, stable, O(n log n).- Objects use a stable sort on purpose: sorting by a second key must not scramble the order from the first.
Custom order: Comparable vs Comparator
class Natural { record Version(int major, int minor) implements Comparable<Version> { @Override public int compareTo(Version o) { if (major != o.major) return Integer.compare(major, o.major); return Integer.compare(minor, o.minor); } }}class Comparators { record Person(String name, int age, double score) {} static void demo(List<Person> people, int[][] intervals) { // Age ascending, then score descending, then name A→Z. people.sort(Comparator.comparingInt(Person::age) .thenComparing(Person::score, Comparator.reverseOrder()) .thenComparing(Person::name)); // int[][]: compare with Integer.compare — `a[0] - b[0]` can overflow. Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); } /** Why `a - b` is a bug: for a = MIN_VALUE, b = 1 the subtraction wraps to a positive number. */ static int brokenCompare(int a, int b) { return a - b; }}Comparable= the one natural order of a type (compareTo).Comparator= any number of external orders you build on demand.- Chain keys with
comparing(...).thenComparing(...). Add.reversed()orComparator.reverseOrder()per key for descending. - Prefer
comparingInt/comparingLong/comparingDouble— they avoid boxing each key. (a, b) -> a - bsilently overflows when values are far apart (e.g.Integer.MIN_VALUE - 1wraps to a huge positive).Integer.comparenever does.- A comparator must be consistent (if a < b and b < c then a < c). Breaking that can make TimSort throw "Comparison method violates its general contract!".
Quicksort partitions: Lomuto vs Hoare
class Hoare { private static final Random RND = new Random(); static void sort(int[] a) { quickSort(a, 0, a.length - 1); } static void quickSort(int[] a, int lo, int hi) { if (lo >= hi) return; int p = partition(a, lo, hi); quickSort(a, lo, p); // note: p, not p - 1 — the pivot is not fixed in place quickSort(a, p + 1, hi); } static int partition(int[] a, int lo, int hi) { int pivot = a[lo + RND.nextInt(hi - lo)]; // random pivot from a[lo..hi-1] int i = lo - 1, j = hi + 1; while (true) { do i++; while (a[i] < pivot); // find a value that belongs on the right do j--; while (a[j] > pivot); // find a value that belongs on the left if (i >= j) return j; // pointers crossed: a[lo..j] <= pivot <= a[j+1..hi] int t = a[i]; a[i] = a[j]; a[j] = t; } }}- Lomuto (see the simulation): pivot = last element, one pointer collects smaller values. Easiest to get right; the pivot ends in its final slot.
- Hoare: two pointers walk inward and swap misplaced pairs. About 3× fewer swaps than Lomuto, handles duplicates better — but recurse on
[lo, p]and[p+1, hi], notp − 1. - Pivot choice decides everything. First/last element on sorted input → O(n²). Random pivot or median-of-three → O(n log n) expected.
- Many duplicates? Use a 3-way partition (
< pivot | = pivot | > pivot) — the Dutch national flag. It's how LC 912 avoids TLE on all-equal arrays.
Heap sort: guaranteed O(n log n) in O(1) space
class InPlaceHeap { static void sort(int[] a) { int n = a.length; // 1. Build a max-heap bottom-up: O(n) in total. for (int i = n / 2 - 1; i >= 0; i--) siftDown(a, i, n); // 2. Repeatedly move the max (root) to the end and repair the smaller heap. for (int end = n - 1; end > 0; end--) { swap(a, 0, end); // largest value is now final at `end` siftDown(a, 0, end); // heap is a[0..end-1] } } private static void siftDown(int[] a, int i, int size) { while (2 * i + 1 < size) { int child = 2 * i + 1; // left child if (child + 1 < size && a[child + 1] > a[child]) child++; // pick the bigger child if (a[i] >= a[child]) return; // heap property holds swap(a, i, child); i = child; } } private static void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }}- Turn the array into a max-heap (children of i are 2i+1 and 2i+2), then repeatedly swap the root to the end and sift the new root down.
- Building the heap bottom-up is O(n), not O(n log n): most nodes are near the bottom and sift down only a little.
- It's the only common sort with O(n log n) worst case and O(1) extra space — but it's not stable and jumps around memory. See the Heaps chapter for
PriorityQueue.
Beyond comparisons: radix sort
class Radix { /** LSD radix sort for non-negative ints: one stable counting pass per decimal digit. */ static void sort(int[] a) { int max = 0; for (int x : a) max = Math.max(max, x); int[] out = new int[a.length]; for (long exp = 1; max / exp > 0; exp *= 10) { // long: exp * 10 must not overflow int[] count = new int[10]; for (int x : a) count[(int) (x / exp % 10)]++; for (int d = 1; d < 10; d++) count[d] += count[d - 1]; for (int i = a.length - 1; i >= 0; i--) out[--count[(int) (a[i] / exp % 10)]] = a[i]; System.arraycopy(out, 0, a, 0, a.length); } }}- Radix sort runs a stable counting sort on each digit, least significant first. Stability is what keeps earlier digits' order intact.
- Use it for many fixed-width keys (ints, IDs, fixed-length strings). Negatives need an offset or a separate pass.
- In interviews, radix sort is usually a talking point ("can you beat n log n?"); counting sort is the one you'll actually code.
| call | does | cost |
|---|---|---|
Arrays.sort(int[] a) | Ascending, in place (dual-pivot quicksort) | O(n log n) |
Arrays.sort(int[] a, from, to) | Sorts only a[from..to-1] | O(m log m), m = to − from |
Arrays.sort(T[] a, Comparator) | Stable sort of objects (TimSort) | O(n log n) |
Collections.sort(list) / list.sort(cmp) | Stable sort of a List (TimSort) | O(n log n) |
Comparator.comparingInt(f).thenComparing(g) | Sort by f, break ties with g | O(1) per compare |
Comparator.reverseOrder() / cmp.reversed() | Descending order | O(1) per compare |
Integer.compare(a, b) | Overflow-safe -1/0/1 comparison | O(1) |
Arrays.parallelSort(a) | Multi-threaded sort for very large arrays | O(n log n) work |
list.stream().sorted(cmp).toList() | Sorted copy, original untouched | O(n log n) |
Just sort numbers
→ Arrays.sort(int[])
Fastest option in the JDK; stability is meaningless for primitives.
Sort objects by several fields
→ list.sort(comparing(..).thenComparing(..))
Stable TimSort plus readable, overflow-safe comparators.
Descending ints
→ Sort ascending then read backwards (or Integer[] + reverseOrder())
int[] has no comparator overload; boxing costs memory and time.
Values in a small range (ages, grades, 0–2 colours)
→ Counting sort
O(n + k) beats O(n log n) when k is small.
Sort a linked list
→ Merge sort
No random access needed; O(1) extra space bottom-up.
Only the k-th / top k elements
→ Quickselect or a size-k heap
O(n) average / O(n log k) — no need to sort everything.
Guaranteed O(n log n) with O(1) extra memory
→ Heap sort
Merge needs O(n) memory; quicksort has an O(n²) worst case.
07 · recognise
Spot the pattern
Intervals, meetings, schedules — anything with start/end
Sort by start (or end), then sweep once
"Find duplicates / closest pair / group equal things"
Sort so equal or near values become neighbours
Pair or triplet sums without needing original indices
Sort + two pointers
"k-th largest / smallest", "top k"
Quickselect (O(n) avg) or a size-k heap (O(n log k))
Values come from a small known range (0–100, three colours, lowercase letters)
Counting sort / buckets in O(n + k)
"Arrange to form the largest/smallest…", custom ordering rule
Custom comparator — prove the rule is transitive
Count pairs i < j with some order condition (inversions, reverse pairs)
Merge sort and count during the merge
Sort a linked list in O(n log n)
Merge sort (slow/fast split + merge)
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Comparator
(a, b) -> a - boverflows for large or negative values. UseInteger.compareorcomparingInt. Arrays.sort(int[], Collections.reverseOrder())doesn't compile — comparators only work on object arrays likeInteger[].- Assuming quicksort is always O(n log n). Fixed first/last pivots on sorted input — or naive partitions on all-equal input — make it O(n²).
- Hoare partition with
quickSort(lo, p - 1): the pivot isn't fixed at p in Hoare, so you lose elements or loop forever. Use[lo, p]and[p + 1, hi]. - Merge with
<=vs<mixed up: taking the right element on ties breaks stability. - Allocating a new helper array in every merge call — O(n log n) garbage. Allocate once and pass it down.
- Sorting when the problem needs the original indices — sort an index array or pairs instead, or use a HashMap.
- Counting sort on a huge range (values up to 10⁹) — the count array alone would blow memory.
tricks
- First question in any array problem: "Would sorting help, and can I afford O(n log n)?" It often turns O(n²) into O(n log n).
- Say the trade-off in one line: "merge sort — stable, guaranteed, O(n) memory; quicksort — in-place, faster in practice, O(n²) worst."
- Know insertion sort cold: it's the base case inside TimSort and introsort, and the answer to "what's best for nearly-sorted data?"
- When writing a comparator, test it on three values a < b < c in your head — transitivity bugs are silent until production.
- For "k-th largest", mention all three: sort (simple), heap (streaming), quickselect (optimal average) — then code the one they want.
- Memorise the merge loop with the four cases (left empty, right empty, right smaller, else left). It powers sort, inversion counting and merge-k-lists.
09 · why & how
Questions you might be too shy to ask
Why can't any comparison sort beat O(n log n)?
Each comparison is a yes/no question. There are n! possible orders of n items, and every one must lead to a different answer, so you need at least log₂(n!) questions. log₂(n!) ≈ n log₂ n − 1.44n, which is Θ(n log n). Counting and radix sort escape this by not comparing at all.
If quicksort's worst case is O(n²), why is it so popular?
Because the worst case almost never happens with random (or median-of-three) pivots, and in the average case it does fewer memory moves and has great cache locality. Libraries also add guards: since JDK 14, OpenJDK's dual-pivot quicksort falls back to heap sort if partitioning goes too deep.
What does "stable" actually change for me?
Sort people by name, then stably by age: people of the same age stay in name order. With an unstable sort, that name order could be scrambled. For plain ints stability is invisible — that's why Java uses an unstable algorithm for int[] but a stable one for objects.
What does a comparator's return value mean? I always mix up which way is ascending.
compare(a, b) returns a negative number when a should come first, 0 for a tie, and a positive number when b should come first. Integer.compare(a, b) gives ascending: [5, 2, 9] → [2, 5, 9]. Swap the arguments, Integer.compare(b, a), and you get descending. Bas itna yaad rakho: negative means "a goes first".
Arrays.sort(nums) gave nothing back and my original order is gone. How do I keep the original indices?
Arrays.sort returns void: it sorts the same array in place, so the old order is lost. Need an untouched copy? Sort nums.clone() instead. Need each value's original index? Build pairs — for each i, p[i] = new int[]{nums[i], i} — then Arrays.sort(p, (x, y) -> Integer.compare(x[0], y[0])). This compiles because each row of an int[][] is an object (an int[]), so the comparator overload accepts it. The same trick sorts intervals by their start.
Why is insertion sort used inside "fast" sorts?
For tiny ranges its low overhead beats the recursion and bookkeeping of merge/quick sort.
In the JDK 17 source, TimSort (used for objects) sorts pieces under 32 elements with binary insertion sort.
The dual-pivot quicksort (used for int[]) switches to insertion sort once a part is a few dozen elements long — 44 or more, depending on where the part sits.
These cut-offs are OpenJDK implementation details, not part of the Java spec.
Is merge sort in-place?
Not on arrays — the standard merge needs an O(n) helper array. On linked lists it is: merging just relinks nodes, so bottom-up merge sort uses O(1) extra space.
Why does merge sort split into [lo, mid] and [mid + 1, hi], and not [lo, mid - 1] and [mid, hi]?
mid = lo + (hi - lo) / 2 rounds down, so with two elements (lo = 0, hi = 1) you get mid = 0. [lo, mid] and [mid + 1, hi] give [0, 0] and [1, 1]: both are smaller, so the recursion reaches the base case lo >= hi. The other split gives [0, -1] and [0, 1] — the right part is the whole range again, so it recurses until StackOverflowError. Rule: when mid rounds down, mid belongs to the left half.
I understand merge sort when I read it, but I get lost in the recursion when I dry-run or write it. What now?
Don't trace every level. Trust that the two recursive calls hand back sorted halves — the leap of faith from the Recursion chapter. Then only one thing must be right: the merge of two sorted halves with pointers i, j and k. Practise that merge alone on paper, [2, 5] + [1, 9] → [1, 2, 5, 9], until it feels boring. Then wrap it in the skeleton (base case, mid, two calls, merge) and write it from a blank file again the next day — recall, not re-reading, is what makes it stick.
In the Lomuto partition, what do i and j mean, and why does the loop stop at j < hi?
The pivot sits at a[hi], so j scans every other value — that's why j < hi. i marks where the next smaller value goes: a[lo..i-1] are all smaller than the pivot. When a[j] < pivot, swap it into slot i and grow the zone with i++. At the end, swap the pivot into slot i: smaller values on its left, the rest on its right. Try [3, 8, 1, 5] (pivot 5) in the sim: 3 and 1 end up in the left zone, and 5 lands at index 2 — final forever.
When should I use counting sort?
When values are integers in a small range k (like 0–100 or 26 letters) and k is not much bigger than n. Then O(n + k) beats O(n log n). If k is huge (values up to 10⁹), the count array is too big.
In Largest Number, why is comparing strings with compareTo safe? As strings, isn't "9" bigger than "10"?
Yes — comparing "9" with "10" directly would be wrong as numbers. But we never do that: we compare a + b with b + a, and those two strings always have the same length. For equal-length digit strings, dictionary order equals numeric order: "910" vs "109" differ first at '9' vs '1', so 910 wins. No parsing needed — and parsing would overflow anyway, since two 10-digit pieces glue into 20 digits, more than a long holds.
Which sort should I write when the interviewer says "don't use the library"?
Merge sort: it's always O(n log n), its code is short and easy to get right, and there's no worst-case trap. Mention randomized quicksort (in-place, faster in practice) and heap sort (O(1) space) as alternatives.
Do I have to memorize all these sorts?
Know how each works and its trade-offs (the big table). Be able to code merge sort, quicksort partition, and insertion sort from scratch. Bubble and selection are mainly for understanding — nobody uses them in production.
10 · practice
Interview problems
Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.
- #912
- #179
- #148
- #274H-IndexMediumBloombergGoogleAmazonMeta
- #493Reverse PairsHardAmazonGoogle
- #324Wiggle Sort IIMediumGoogleAmazon
- #1636Sort Array by Increasing FrequencyEasyAdobeAmazon
Also practise this here
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #56Merge IntervalsMediumin Intervals
Sorting by start turns an O(n²) pair search into one linear sweep — sort first to expose structure.
- #215Kth Largest Element in an ArrayMediumin Heaps
Quickselect is quicksort's partition recursing into one side only: O(n) average (see its quickselect dry run).
- #75Sort ColorsMediumin Two Pointers
Counting sort for three keys, and the 3-way partition at the heart of 3-way quicksort.
11 · revise
The whole chapter on one screen
9 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
Sorting puts equal and nearby values next to each other, so duplicates, overlaps and gaps show up in one linear scan.
- use it when
Intervals, duplicates, closest pairs, pair sums without indices, "form the largest…": ask first — would sorting expose the structure?
- cost
Comparison sorts can't beat Ω(n log n). Merge/heap: O(n log n) always. Quicksort: O(n log n) expected, O(n²) worst. Counting: O(n + k).
- template
Numbers:
Arrays.sort(nums). Objects:list.sort(comparingInt(f).thenComparing(g))— stable TimSort keeps earlier orders as tie-breakers. - trap
(a, b) -> a - boverflows:MIN_VALUE - 1wraps to a big positive. UseInteger.compare(a, b)orComparator.comparingInt. - trap
A fixed first/last pivot makes quicksort O(n²) on sorted input; a 2-way partition crawls on all-equal input. Random pivot + 3-way split fixes both.
- trap
int[]takes no comparator. For descending ints, sort and read backwards, or box toInteger[]withCollections.reverseOrder(). - pro tip
Only the k-th or top k? Don't sort everything: quickselect is O(n) on average, a size-k heap is O(n log k).
- pro tip
Sorting a linked list? Merge sort: no random access needed, and merging only relinks nodes — O(1) extra space bottom-up.
12 · recall
Flip, remember, repeat
Why can't any comparison sort beat O(n log n) in the worst case?
Question 1: Why can't any comparison sort beat O(n log n) in the worst case?
13 · check yourself
Quick quiz
Which sort is fastest on an array that is already sorted?
You sort employees by name, then by department with a stable sort. Inside one department, how are they ordered?
What does `Arrays.sort(int[])` use in modern Java?
Why is `(a, b) -> a - b` a dangerous comparator?
Merge sort's extra space on an array is…
Values are integers from 0 to 100 and n = 10⁶. Best choice?
When does quicksort hit its O(n²) worst case?
Quickselect finds the k-th largest element in…
14 · learn more
A second teacher, if you want one
- ▶ videoSorting - Part 1 | Selection Sort, Bubble Sort, Insertion Sort | Strivers A2Z DSA Course ↗Striver (take U forward) · EnglishSelection, bubble and insertion sort, each dry-run by hand with its time complexity worked out.
- ▶ videoMerge Sort | Algorithm | Pseudocode | Dry Run | Code | Strivers A2Z DSA Course ↗Striver (take U forward) · EnglishMerge sort with pseudocode, a full dry run and code. Once the merge step clicks, Sort List and inversion counting follow easily.
- ◈ visualizerSorting (Bubble, Selection, Insertion, Merge, Quick, Counting, Radix) ↗VisuAlgo (Steven Halim, NUS) · EnglishWatch every sort in this chapter run on your own input, one comparison at a time, including quicksort and counting sort.