Every other sort on this site picks one bet and keeps it: insertion sort bets the input is nearly sorted already,
merge sort assumes nothing about it and guarantees
O(n log n) regardless. Timsort, designed by Tim Peters for Python in 2002 and since
adopted as the sort behind Java's Arrays.sort() for objects (and Android, and V8's
stable-sort path), refuses to pick: it first looks for stretches of the array that are already
sorted — real-world data is full of them, partially-sorted logs, nearly-sorted resubmissions,
concatenated sorted chunks — and only falls back to insertion sort and merging where none exist.
Find no order at all and it degrades to merge sort with extra bookkeeping. Find a mostly-sorted
array and it can do a fraction of the work.
Enter a comma-separated list of numbers, then step through the sort. Real Timsort computes a
minRun between 32 and 64 from the array length — too large to show more than one
run on a short typed-in array, so this demo uses a small fixed minRun = 4 instead,
labeled as such below; the algorithm itself is unmodified. Colored blocks are a
natural run being detected or extended; the panel on the right is the
run stack — pending runs waiting to merge, shown as length tags,
newest on top.
Finding a run. Scan forward from the current position: if the next element is
>= the one before it, the run is ascending and keeps extending the same way; if
it's smaller, the run is strictly descending and keeps extending only while each new
element is strictly less than the last. A descending run gets reversed in place once it ends —
safe specifically because it's strictly descending, no two equal elements sit inside it to
have their order disturbed by the reversal. This is the same reasoning merge sort's own
<= in its merge step protects for a different reason — see merge sort's own pitfalls on stability.
Extending short runs. A natural run shorter than minRun gets
padded out to minRun elements (or the end of the array) using binary insertion
sort — the same technique as insertion sort, except
each new element's insertion point is found with a binary search instead of a linear backward
scan. This doesn't change the O(n²) worst case for the insertion phase overall, since
shifting elements into place is still linear — but it does cut the number of comparisons
needed to find where each one goes, from linear to logarithmic.
Merging without losing the guarantee. Runs get pushed onto a stack as they're
found. After every push, Timsort checks two rules on the top few run lengths and merges adjacent
runs whenever a rule is violated: the third-from-top run must be longer than the combined length
of the two above it, and the second-from-top must be longer than the top. Both rules exist to keep
run lengths roughly exponential going down the stack — the same balance a self-balancing tree
maintains on subtree heights — which is what turns "however the natural runs happened to fall"
into a guaranteed O(n log n) total merge cost, the same bound plain merge sort gets
by force-splitting exactly in half every time.
Natural-run detection, binary-insertion extension, and the stack-based merge policy above, un-instrumented. This omits one real optimization production Timsort has and this page doesn't implement — see Pitfalls.
function minRunLength(n) {
let r = 0;
while (n >= 64) { r |= (n & 1); n >>= 1; }
return n + r; // returns a value in [32, 64)
}
function countRunAndMakeAscending(arr, left, right) {
let runEnd = left + 1;
if (runEnd > right) return 1;
if (arr[runEnd] < arr[left]) {
while (runEnd <= right && arr[runEnd] < arr[runEnd - 1]) runEnd++;
for (let lo = left, hi = runEnd - 1; lo < hi; lo++, hi--) {
[arr[lo], arr[hi]] = [arr[hi], arr[lo]];
}
} else {
while (runEnd <= right && arr[runEnd] >= arr[runEnd - 1]) runEnd++;
}
return runEnd - left;
}
function binaryInsertionSort(arr, left, right, sortedUpTo) {
for (let i = sortedUpTo; i <= right; i++) {
const key = arr[i];
let lo = left, hi = i;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (arr[mid] <= key) lo = mid + 1; else hi = mid;
}
for (let j = i; j > lo; j--) arr[j] = arr[j - 1];
arr[lo] = key;
}
}
function merge(arr, left, mid, right) {
const leftArr = arr.slice(left, mid), rightArr = arr.slice(mid, right);
let i = 0, j = 0, k = left;
while (i < leftArr.length && j < rightArr.length) {
arr[k++] = leftArr[i] <= rightArr[j] ? leftArr[i++] : rightArr[j++];
}
while (i < leftArr.length) arr[k++] = leftArr[i++];
while (j < rightArr.length) arr[k++] = rightArr[j++];
}
function timSort(arr) {
const n = arr.length;
if (n < 2) return arr;
const minRun = minRunLength(n);
const runStarts = [], runLens = [];
function mergeAt(i) {
const s1 = runStarts[i], l1 = runLens[i], s2 = runStarts[i + 1], l2 = runLens[i + 1];
merge(arr, s1, s2, s2 + l2);
runLens[i] = l1 + l2;
runStarts.splice(i + 1, 1);
runLens.splice(i + 1, 1);
}
function collapse() {
while (runStarts.length > 1) {
const top = runStarts.length - 1;
if (top >= 2 && runLens[top - 2] <= runLens[top - 1] + runLens[top]) {
mergeAt(runLens[top - 2] < runLens[top] ? top - 2 : top - 1);
} else if (runLens[top - 1] <= runLens[top]) {
mergeAt(top - 1);
} else break;
}
}
let start = 0;
while (start < n) {
let runLen = countRunAndMakeAscending(arr, start, n - 1);
const end = Math.min(start + minRun, n);
if (runLen < end - start) {
binaryInsertionSort(arr, start, end - 1, start + runLen);
runLen = end - start;
}
runStarts.push(start);
runLens.push(runLen);
collapse();
start += runLen;
}
while (runStarts.length > 1) mergeAt(runStarts.length - 2);
return arr;
}
The claim "it exploits existing order" is easy to state and easy to get wrong in the details, so
here it's counted rather than asserted. Both the real timSort above and the plain
mergeSort from merge sort's own page ran,
unmodified, on the same 1,000-element arrays below, with every <=/<
comparison counted:
[0, 1, 2, ..., 999]): Timsort finds one run
covering the whole array, reverses nothing, extends nothing, and merges zero times —
1,000 comparisons, exactly the linear scan that confirms the order. Merge sort has
no way to know this and does its full O(n log n) split-and-merge regardless:
4,932 comparisons.[1000, 999, ..., 1]): same story, one run
(reversed in place), zero merges — 1,000 comparisons against merge sort's
5,044.minRun-sized chunks
(minRun = 63 at this size), breaking most natural runs down to needing the insertion-sort
extension anyway — 6,687 comparisons, against merge sort's 6,522.
Timsort loses this one. The same amount of disorder costs almost nothing when it's clustered and
almost everything when it's spread out — run detection finds contiguous order, not sparse order,
and 20 scattered swaps look statistically like no order at all.Run the same comparison yourself below, on data shaped either way, or on plain random data where neither algorithm has an edge:
No galloping mode here. Production Timsort adds one more trick this reference implementation leaves out: during a merge, if one side keeps winning several comparisons in a row, it switches to a binary search ("galloping") to pull a whole chunk from that side at once instead of comparing element by element. That's a real further speedup on top of what's measured above, not implemented here to keep the reference implementation reasonably short — the run-detection and stack-merge behavior above is genuine, not a simplification of it.
Small arrays never merge at all. minRunLength returns
n itself whenever n < 64 — the whole array becomes a single run,
extended (if needed) by binary insertion sort, with no merge step ever triggered. Real Timsort on
a short array is binary insertion sort. The demo above only shows multiple runs merging
because it substitutes a small fixed minRun for the real formula — worth knowing before
assuming a small live example proves the merge logic runs in practice on small inputs, since it
doesn't, not with the real threshold.
No advantage on genuinely random data — confirmed above (8,659 vs. 8,667
comparisons at n = 1,000 across repeated random trials, statistically tied) — and a
real, if small, disadvantage on scattered disorder, also confirmed above. Timsort's worst case is
still O(n log n), same as merge sort, but its bookkeeping (run detection, stack
invariant checks) is pure overhead when there's no order to find.
A known subtlety in the merge invariant is not addressed here. The two-rule stack policy above is the original 2002 design; a 2015 paper by Peters and de Gouw found that certain long inputs could violate the intended stack-depth bound under those exact two rules and proposed a third rule to close the gap. Real production Timsort implementations adopted the fix; this page's reference implementation uses the original two rules, which is correct for every input this page tests but isn't the newest production algorithm word-for-word.
Time: O(n) best case (the whole array is already one run, or
close to it), O(n log n) worst case — the stack-merge policy above guarantees this the
same way merge sort's forced halving does, regardless of how the natural runs happen to fall.
Space: O(n) for the merge buffers, same as plain merge sort, plus
O(log n) for the run stack itself. Stable: yes — ties inside an
ascending run keep their original order, a strictly-descending run has no ties to disturb when
reversed, and the merge step's <= favors the left (earlier) run on equal
keys, exactly merge sort's own reasoning.
That combination — adaptive to real-world order, stable, with merge sort's worst-case guarantee
as a floor — is why Timsort backs Python's sorted()/list.sort() and
Java's Arrays.sort() for object arrays, rather than plain merge sort or insertion sort alone — it's built directly from both,
using each one exactly where the other is weak.
See Choosing a Comparison Sort for how Timsort fits alongside the other ten comparison sorts on this site.