TimSort
Find the already sorted stretches (runs) in the list, extend short runs with insertion sort, then merge the runs.
O(n log n)Space O(n)3 practice problemsStep-through pictureThe idea
TimSort is the sort behind Array.prototype.sort in V8 (Chrome, Node.js, Deno) since 2018, and behind Python's sorted. It was designed by Tim Peters for real-world data, which is often partly sorted already.
It walks the list looking for runs: stretches that already go up (or strictly go down, which it reverses). Short runs are grown to a minimum length, called minRun, using insertion sort, which is fast on short pieces. Then it merges the runs together, like merge sort, until one run covers the whole list.
Because it reuses existing order, TimSort sorts an already sorted list in O(n) and never does worse than O(n log n). It is also stable, which the JavaScript spec has required of sort since ES2019.
An everyday example
Combining partly sorted stacks of mail
A clerk receives mail that is mostly in date order already, in stretches. She walks along the pile and splits it wherever the dates stop going up, setting each stretch aside as a stack. A stretch that runs backwards she just flips over. Very short stacks she tops up with a few more letters, slotting each one into place by hand. Then she merges stacks two at a time, always taking the earlier date from the top, until one stack is left.
| In the example | In the code |
|---|---|
| A stretch already in date order | A run: items[start..end-1] |
| Flipping a backwards stretch | Reversing a strictly descending run |
| Topping up a short stack by hand | Insertion sort up to start + minRun |
| Merging two stacks | Merging neighboring runs [from, mid) and [mid, to) |
From the slow way to timsort
- Merge sort splits a list at fixed midpoints, no matter what is in it. But real data is rarely random. Lists are often mostly sorted, sorted with a few new items at the end, or made of sorted pieces glued together. Merge sort spends
O(n log n)on them anyway. - So start by looking at the data. Walk from the left and find the longest stretch that is already going up, called a run. A stretch going strictly down is just as good: reverse it and it goes up. A sorted list is one run. Then merge runs instead of halves, and order already in the data is never redone.
- Random data has a problem: its runs are tiny, often 2 items, and merging thousands of tiny runs wastes time. So set a minimum run length,
minRun. If a natural run is shorter, extend it tominRunitems with insertion sort, which is very fast on short lists. - Finally, merge the runs together, two at a time, until one run covers the list, taking from the left run on ties so equal items keep their order. Find runs, grow short ones with insertion sort, and merge them stably: that is TimSort, in a simplified form.
How it works
- Start at
start = 0. Find the run there: extendendwhile items keep going up (>=). If the run starts by going strictly down, extend while it keeps going strictly down and then reverse it. - If the run is shorter than
minRun, extend it tostart + minRun(or the end of the list) by insertion-sorting the extra items into it. - Record the run as
[start, stop]and continue fromstopuntil the whole list is split into sorted runs. - Merge neighboring runs in pairs: copy the left run, then merge it with the right run back into the array.
- Repeat the pairwise merging until only one run is left: the list is sorted.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function timSort(items, minRun = 32) {let runs = [];let start = 0;while (start < items.length) {let end = start + 1;if (end < items.length && items[end] < items[start]) {while (end < items.length && items[end] < items[end - 1]) end++;items.splice(start, end - start, ...items.slice(start, end).reverse());} else {while (end < items.length && items[end] >= items[end - 1]) end++;}const stop = Math.min(items.length, Math.max(end, start + minRun));for (let i = end; i < stop; i++) {const current = items[i];let j = i - 1;while (j >= start && items[j] > current) items[j + 1] = items[j--];items[j + 1] = current;}runs.push([start, stop]);start = stop;}while (runs.length > 1) {const next = [];for (let r = 0; r + 1 < runs.length; r += 2) {const [from, mid] = runs[r];const to = runs[r + 1][1];const left = items.slice(from, mid);let i = 0;let j = mid;for (let k = from; k < to; k++) {if (j >= to || (i < left.length && left[i] <= items[j])) items[k] = left[i++];else items[k] = items[j++];}next.push([from, to]);}if (runs.length % 2 === 1) next.push(runs[runs.length - 1]);runs = next;}return items;}
How fast it is
| Time | O(n log n) |
|---|---|
| Extra memory | O(n) |
Finding and growing runs is O(n × minRun), which is linear because minRun is a small constant. Each merge round touches every item once, and there are log of the number of runs rounds. The merge copies a run into a buffer.
- Best case
O(n)An already sorted list like
[1, 2, 3, 4, 5], or a strictly descending one: it is one run, found with aboutncomparisons, and there is nothing to merge. - Average case
O(n log n)Random data has short runs, so it becomes about
n / minRunruns of insertion sort andlogof that many merge rounds. - Worst case
O(n log n)No input is worse than random data: growing runs costs at most
O(n × minRun)and merging is merge sort on runs.
Common mistakes
- Reversing descending runs that contain equal items. That breaks stability. Only reverse strictly descending runs.
- Using
<instead of<=when merging, which also breaks stability. - Thinking
minRunmust be tiny. Real TimSort picks 32 to 64 so that the number of runs is close to a power of two, which keeps merges balanced. - Assuming every engine uses TimSort. The spec only requires a stable sort; V8 uses TimSort, but other engines may use other stable algorithms such as merge sort.
When to use it
Look for these clues in a problem:
- You are asked how JavaScript's built-in
sortworks, or why it is stable. - The data is likely partly sorted, such as a list re-sorted after a few changes or logs from several ordered sources.
- The question is about runs: finding stretches that are already in order, or merging them.
Check yourself
Answer each one in your head first, then open it.
How many comparisons does this TimSort make on an already sorted list of 1,000 items?
1,000. One comparison decides that the run goes up, then the run-finding loop compares each of the 999 later items with the one before it. It finds a single run, so there is nothing to merge.
Why does TimSort use insertion sort for short runs instead of splitting further?
For a few dozen items, insertion sort's simple loop is faster than the overhead of more merging, and it is stable. Growing runs to minRun also limits how many merges are needed.
Why must runs be merged with their neighbors and not, say, the first with the third?
Merging non-neighbors would move items of the third run ahead of equal items in the second run, which breaks stability, and the merged result wouldn't be one contiguous stretch.
What happens to the last run when the list ends before start + minRun?
stop is capped at items.length, so the last run is simply shorter than minRun. It is still sorted, so it is merged like any other.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The run-finding loop keeps this rule: every pair in runs covers a sorted stretch, the stretches sit side by side, and together they cover items[0..start-1]. Each step adds one sorted stretch starting at start: an ascending run is sorted as found, a strictly descending run is sorted after reversing, and the insertion sort that grows a run to stop keeps it sorted, just as insertion sort always does.
Each step moves start forward to stop, which is at least start + 1, so the loop ends with runs covering the whole list. Each merge round combines neighboring sorted runs into one sorted run, by the same argument as merge sort's merge. The number of runs roughly halves each round, so the merge loop ends with one run, which is the whole sorted list.
Stability needs care in three places, and the code handles all three. Only strictly descending runs are reversed, so no equal items flip. Insertion sort stops at an equal item (items[j] > current). The merge takes from the left run when items are equal (left[i] <= items[j]). Because runs are always merged with their neighbors, items from an earlier run are always on the left.
Going deeper
3 more ideas
Why reverse only strictly descending runs
Reversing a run flips the order of equal items. If a descending run could contain equal items, like [5, 3, 3, 1], reversing it would swap the two 3s and break stability. So TimSort only reverses runs that go strictly down, where no two items are equal. Ascending runs may contain equal items, because they are kept as they are.
What the real TimSort adds
This template keeps the core ideas but simplifies the rest. The real algorithm computes minRun from the length (between 32 and 64), uses binary insertion sort, and merges runs as it finds them, using a stack with rules that keep merges balanced. During a merge, if one run keeps winning many times in a row, it switches to galloping: it jumps ahead by 1, 3, 7, 15, ... places to find how far it can copy in one block, then binary searches inside that jump.
How long it takes
Finding runs is one pass. If the list is already sorted, there is one run and nothing to merge: O(n). In the worst case there are about n / minRun runs, and merging them pairwise takes log of that many rounds of O(n) work: O(n log n). The merge needs a buffer the size of the smaller run, so space is O(n).
Work it by hand
Every step in a table, with why
Sort 11 numbers with minRun = 3. The list has an ascending run, a descending run, a short run to grow, and a one-item tail.
items = [3, 6, 8, 11, 9, 5, 2, 10, 4, 7, 1], minRun = 3
| Step | Step | Range | items | runs | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | Find run | start = 0, end = 4 | [3, 6, 8, 11, 9, 5, 2, 10, 4, 7, 1] | [0, 4] | Ascending run of 4 | 6 < 3 is false, so the run goes up. It extends while each item is >= the one before, and stops at 9. Its length 4 is already at least minRun. |
| 2 | Find run | start = 4, end = 7 | [3, 6, 8, 11, 9, 5, 2, 10, 4, 7, 1] | [0, 4] | Strictly descending: 9, 5, 2 | 5 < 9, so this run goes down. It extends while each item is strictly smaller, and stops at 10. |
| 3 | Reverse | start = 4, end = 7 | [3, 6, 8, 11, 2, 5, 9, 10, 4, 7, 1] | [0, 4], [4, 7] | Now 2, 5, 9 | The splice reverses it in place. It had no equal items, so reversing can't break stability. Its length is 3, so stop = 7, nothing grows, and [4, 7] is recorded. |
| 4 | Find run | start = 7, end = 9 | [3, 6, 8, 11, 2, 5, 9, 4, 10, 7, 1] | [0, 4], [4, 7] | Descending 10, 4, reversed | A run of only 2. stop = Math.min(11, Math.max(9, 7 + 3)) = 10, so it must grow by one item. |
| 5 | Grow | i = 9 | [3, 6, 8, 11, 2, 5, 9, 4, 7, 10, 1] | [0, 4], [4, 7], [7, 10] | Insert 7 into the run | Insertion sort shifts 10 right and puts 7 at index 8. The run 4, 7, 10 is recorded. |
| 6 | Find run | start = 10, end = 11 | [3, 6, 8, 11, 2, 5, 9, 4, 7, 10, 1] | 4 runs, last [10, 11] | One item left | stop would be 13, but Math.min caps it at 11, the end of the list. A short last run is allowed. |
| 7 | Merge | [0, 4) with [4, 7) | [2, 3, 5, 6, 8, 9, 11, 4, 7, 10, 1] | - | First pair of runs | Copy the left run into left, then merge it with 2, 5, 9 back into indexes 0 to 6. |
| 8 | Merge | [7, 10) with [10, 11) | [2, 3, 5, 6, 8, 9, 11, 1, 4, 7, 10] | [0, 7], [7, 11] | Second pair of runs | 1 comes from the right run before all three left items. The round ends with 2 runs. |
| 9 | Merge | [0, 7) with [7, 11) | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | [0, 11] | One run left: stop | runs.length is 1, so the merge loop ends. |
It returns [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]. It found 4 runs, reversed 2 of them, grew 1 with a single insertion, and needed 2 rounds of merging.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| TimSort | O(n log n) | O(n) | General-purpose stable sorting of real data, which is often partly sorted. It is what Array.prototype.sort uses in V8. |
| Merge sort | O(n log n) | O(n) | You want a short, stable sort to write by hand, and the data is random, so finding runs would not help. |
| Insertion sort | O(n²) | O(1) | Lists of a few dozen items. TimSort uses it for exactly that. |
| Quick sort | O(n log n) | O(log n) | Stability isn't needed and the data is random numbers, where in-place partitioning is faster. |
Extra memory: O(n): each merge copies the left run into left, and runs holds up to n / minRun pairs. The real TimSort copies only the smaller run, but it is still O(n) in the worst case.
How the steps grow
- TimSort, Merge sort, Quick sort
O(n log n) - Insertion sort
O(n²)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| TimSort | 33 | 9,966 | 20 million |
| Merge sort | 33 | 9,966 | 20 million |
| Insertion sort | 100 | 1 million | 1 trillion |
| Quick sort | 33 | 9,966 | 20 million |
Inputs that need care
5 tricky inputs
- An empty list
start < items.lengthis false at once,runsstays empty, and the merge loop needs more than one run, so the list is returned as it is.- Strictly descending:
[9, 8, 7, 6, 5, 4] - It is one run. It is reversed in place in a single step, and there is nothing to merge:
O(n). - All equal:
[4, 4, 4, 4] - The ascending check uses
>=, so the whole list is one run. Nothing is reversed or merged, and equal items keep their order. - A descending stretch with a tie:
[5, 3, 3, 1] - The descending run stops at the second
3, because3 < 3is false. Only5, 3is reversed, then the second3is inserted after the first. The result[1, 3, 3, 5]keeps the two3s in order. - Data sorted, then a few new items appended
- One long run, then a short stretch at the end. The stretch is grown into a run with insertion sort and merged in, close to
O(n)when only a few items were added. This is common in real apps, like adding items to a sorted feed.
Variations and follow-ups
Twists interviewers add
Galloping merges
When one run wins many comparisons in a row (7 in the real TimSort), it is likely to keep winning. The merge then jumps ahead 1, 3, 7, 15, ... places to find where the streak ends, binary-searches inside that jump, and copies the whole block at once. Merging a run that fits entirely before another takes about log comparisons instead of one per item.
A stack of runs
The real algorithm doesn't collect all runs first. It pushes each run on a stack and merges the top runs whenever their lengths break two rules: each run must be longer than the one above it, and longer than the two above it together. This keeps merges balanced and the stack short.
Computing minRun
The real minRun is between 32 and 64, chosen so that the list length divided by minRun is a power of 2 or just under. Then the runs pair up evenly in the merges. Lists shorter than 64 are sorted with binary insertion sort alone.
Powersort
A newer way to decide which runs to merge, based on where each run would sit in a balanced merge tree. Python switched its list sort from TimSort's merge rules to Powersort's in version 3.11.
In an interview
Spot it, explain it, answer follow-ups
- TimSort comes up when someone asks "what does
sort()actually do?" or "why is sorting an already sorted array so fast?". The answer is runs: sorted data is one run, found in one pass. - Explain it as three steps: find natural runs (reversing strictly descending ones), grow short runs to
minRunwith insertion sort, and merge runs stably. Then name its two big guarantees:O(n)on sorted input,O(n log n)always. - Test by hand with an ascending run followed by a descending run, as in the trace. Check that a descending stretch with equal items is not reversed as a whole.
- A common follow-up is "why does it reverse only strictly descending runs?". Reversing a run with equal items would put those items in the opposite order, which breaks stability.
Where frontend code uses it
The same idea in real UI code
- V8's
Array.prototype.sort, used in Chrome, Edge, Node.js, and Deno, has been TimSort since V8 7.0 (2018). Before that it used quick sort and was not stable. - Because the spec requires
sortto be stable (since ES2019), sorting a table by one column and then another keeps ties in the order of the earlier sort. - Re-sorting a list after a small change (a new message, an edited row) is fast with the built-in sort, because most of the list is one long run.