DSA Sorting
TimSort
The UI freezes on every keystroke if list.sort rebuilds order from chaos. Real feeds are mostly sorted with local edits; a hybrid that keeps runs wins.
TimSort is a hybrid of merge sort and insertion sort. It finds , extends short ones, and merges on a stack with balance rules. It is strongly on real-world data (CPython's list.sort path).
Algorithm journey
Find runs
Detect ascending or reversible descending sequences.
Extend to minrun
Insertion-sort short runs up to a computed minimum.
Push and merge
Maintain stack invariants so merges stay balanced.
Gallop when skewed
Accelerate merges when one run dominates.
What actually happens
Naive idea: always mergesort from scratch. Correct and Theta(n log n), but ignores existing order.
Better idea: keep natural runs and only pay for disorder.
It still allocates merge buffers and has nontrivial control logic; for tiny n, plain insertion may be enough.
Invariant
Every completed run on the stack is sorted and stably ordered; stack-size / run-length rules ensure merges do not leave a pathological tower of tiny runs.
How to choose TimSort
1. Need a stable general-purpose sort? TimSort-family algorithms are production defaults in several runtimes.
2. Data often partially sorted? Adaptivity is the reason TimSort shines.
3. Hard real-time with tiny code size? A simpler mergesort/introsort may be easier to reason about.
Quantitative check
Fully sorted n = 10^6: essentially one giant run, near-linear work. Random data: still O(n log n) merges. The win is the gap between those regimes on real lists.
How it works
Identify Runs
Scan the array for natural runs (sequences that are already sorted). If a run is smaller than a minimum size (minrun), use Insertion Sort to extend it.
Merge Runs
Push runs onto a stack. Merge runs together using the Merge Sort logic to maintain stability and balance.
Galloping Mode
During merging, if one run consistently "wins" comparisons, TimSort switches to "galloping mode" (exponential search) to find the insertion point faster, skipping many comparisons.
🔎 Invariants, Trace, and Java Behavior
Run Stack Invariant
TimSort keeps a stack of sorted runs and merges them before the stack becomes badly unbalanced. This preserves stable merging while avoiding long chains of tiny merges.
Trace: [1, 2, 3, 7, 6, 5]
- Find ascending run [1, 2, 3, 7].
- Find descending run [6, 5] and reverse it to [5, 6].
- Merge runs into [1, 2, 3, 5, 6, 7].
Java Library Note
In common OpenJDK implementations, Arrays.sort(Object[]), Collections.sort(), and List.sort() use stable TimSort-family code for reference types. Primitive Arrays.sort(int[]) follows a different unstable primitive-array path. Treat these as implementation details, not API guarantees about algorithm identity.
💻 Implementation
These complete, runnable examples are an educational TimSort-like hybrid: minrun-sized stable insertion sorts followed by balanced merges. They do not implement production TimSort's natural-run detection, run-stack collapse rules, or galloping; those mechanisms are explained below.
Python
# Educational TimSort-like hybrid, not production TimSort.
MIN_MERGE = 32
def calc_min_run(n):
"""Returns the minimum length of a run to be merged"""
r = 0
while n >= MIN_MERGE:
r |= n & 1
n >>= 1
return n + r
def insertion_sort(arr, left, right):
for i in range(left + 1, right + 1):
j = i
while j > left and arr[j] < arr[j - 1]:
arr[j], arr[j - 1] = arr[j - 1], arr[j]
j -= 1
def merge(arr, l, m, r):
left = arr[l:m + 1]
right = arr[m + 1:r + 1]
i = j = 0
write = l
while i < len(left) and j < len(right):
# Taking left on equality preserves stability.
if left[i] <= right[j]:
arr[write] = left[i]
i += 1
else:
arr[write] = right[j]
j += 1
write += 1
while i < len(left):
arr[write] = left[i]
i += 1
write += 1
while j < len(right):
arr[write] = right[j]
j += 1
write += 1
def tim_sort(arr):
n = len(arr)
if n < 2:
return arr
min_run = calc_min_run(n)
# Sort individual subarrays of size min_run
for start in range(0, n, min_run):
end = min(start + min_run - 1, n - 1)
insertion_sort(arr, start, end)
# Merge runs
size = min_run
while size < n:
for left in range(0, n, 2 * size):
mid = min(n - 1, left + size - 1)
right = min((left + 2 * size - 1), n - 1)
if mid < right:
merge(arr, left, mid, right)
size = 2 * size
return arr
# Usage
arr = [5, 21, 7, 23, 19]
print(tim_sort(arr))
# In production Python, use: arr.sort()Java
import java.util.Arrays;
// Educational TimSort-like hybrid, not production TimSort.
public class TimSort {
static int MIN_MERGE = 32;
public static int minRunLength(int n) {
int r = 0;
while (n >= MIN_MERGE) {
r |= (n & 1);
n >>= 1;
}
return n + r;
}
// Insertion sort for small runs
public static void insertionSort(int[] arr, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int temp = arr[i];
int j = i - 1;
while (j >= left && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
}
// Merge function
public static void merge(int[] arr, int l, int m, int r) {
int len1 = m - l + 1, len2 = r - m;
int[] left = new int[len1];
int[] right = new int[len2];
for (int x = 0; x < len1; x++) left[x] = arr[l + x];
for (int x = 0; x < len2; x++) right[x] = arr[m + 1 + x];
int i = 0, j = 0, k = l;
while (i < len1 && j < len2) {
if (left[i] <= right[j]) arr[k++] = left[i++];
else arr[k++] = right[j++];
}
while (i < len1) arr[k++] = left[i++];
while (j < len2) arr[k++] = right[j++];
}
public static void timSort(int[] arr) {
int n = arr.length;
if (n < 2) return;
int minRun = minRunLength(n);
// Sort individual subarrays of size minRun
for (int i = 0; i < n; i += minRun) {
insertionSort(arr, i, Math.min((i + minRun - 1), (n - 1)));
}
// Merge runs
for (int size = minRun; size < n; ) {
for (int left = 0; left < n; left += 2 * size) {
int mid = left + size - 1;
int right = Math.min((left + 2 * size - 1), (n - 1));
if (mid < right)
merge(arr, left, mid, right);
}
if (size > n / 2) break; // avoid integer overflow on doubling
size *= 2;
}
}
public static void main(String[] args) {
int[] arr = { 5, 21, 7, 23, 19 };
timSort(arr);
System.out.println(Arrays.toString(arr));
// In reality, use Arrays.sort(Object[]) or List.sort(...) for Java's TimSort.
}
}Natural runs and stability details
Ascending run
Continue while keys are nondecreasing. Equal records stay in encounter order, so no reversal is needed.
Descending run
Detect a strictly decreasing run, then reverse it. Strictness prevents equal records from reversing and breaking stability.
Short run
Extend toward minrun with stable binary insertion sort, exploiting that the detected prefix is already ordered.
Minrun is chosen so the final run count merges into a reasonably balanced shape; common implementations target a small range around a machine-tuned constant, but exact thresholds differ. It is a scheduling parameter, not a universal value that belongs in an API contract.
Run-stack invariants prevent pathological merge schedules
For three consecutive run lengths near the top of the stack: older A, then B, newest C: the classic policy aims for A > B + C and B > C. If a push violates the policy, nearby runs merge until balance is restored. Implementations use carefully specified and sometimes strengthened collapse rules, but the purpose is shared: keep run sizes growing geometrically, bound stack height, and avoid repeatedly merging a huge run with tiny runs.
| Stack before push | Push | Violation | Scheduling response |
|---|---|---|---|
| [64, 40] | 24 | 64 is not greater than 40+24 | Merge an adjacent pair selected by the collapse policy |
| […, B, C] | new short run | B ≤ C | Collapse before the stack becomes a chain of non-growing runs |
Run-stack logic is correctness-sensitive infrastructure. Historically, insufficient invariant enforcement has caused stack-capacity failures in implementations; test adversarial run-length sequences, not only output order.
Galloping is adaptive search inside a merge
trigger
one run wins repeatedly
Straight pairwise merge suggests a long streak.
Exercises
- Why must descending runs be reversed carefully for stability?
- What goes wrong if minrun is always 1 on random data?
- Compare auxiliary memory of TimSort vs in-place heapsort for large n.
Primary sources
- Peters, TimSort description / CPython listobject sort commentary.
- Cormen et al., CLRS: merge sort and insertion sort building blocks.
- OpenJDK TimSort sources (reference-array path) for run-stack invariants.
search
exponential, then binary
Bracket where the losing key belongs, then refine the boundary.
Exercises
- Why must descending runs be reversed carefully for stability?
- What goes wrong if minrun is always 1 on random data?
- Compare auxiliary memory of TimSort vs in-place heapsort for large n.
Primary sources
- Peters, TimSort description / CPython listobject sort commentary.
- Cormen et al., CLRS: merge sort and insertion sort building blocks.
- OpenJDK TimSort sources (reference-array path) for run-stack invariants.
copy
move a whole slice
Reduce comparisons and exploit contiguous bulk copying.
Exercises
- Why must descending runs be reversed carefully for stability?
- What goes wrong if minrun is always 1 on random data?
- Compare auxiliary memory of TimSort vs in-place heapsort for large n.
Primary sources
- Peters, TimSort description / CPython listobject sort commentary.
- Cormen et al., CLRS: merge sort and insertion sort building blocks.
- OpenJDK TimSort sources (reference-array path) for run-stack invariants.
Galloping is not always faster: alternating winners make mode switches overhead. Production implementations adapt the trigger based on recent merge behavior. Stability still requires precise lower-versus-upper-bound choices when equal keys cross run boundaries.
Adaptive complexity and memory
| Input structure | Natural runs r | Expected work shape | Why |
|---|---|---|---|
| Already monotone | 1 | Θ(n) | One scan; a descending run is reversed once |
| Few long runs | r ≪ n | Often described as O(n log r) | Merge work depends on run structure, not only n |
| Random/adversarial | many short runs | O(n log n) | Minrun extension and balanced merges cap the worst case |
Stable merging needs temporary storage proportional to the smaller merged run in tuned implementations, with O(n) worst-case auxiliary memory. The educational code above allocates both halves and performs fixed balanced merges, so it is not naturally adaptive and remains Θ(n log n) on sorted input.
Production and testing checklist
- • Define comparator, null policy, mutation, stability, and exception behavior.
- • Reuse a merge buffer and copy the smaller run where the implementation permits.
- • Preserve equal-record order through descending-run detection and both merge directions.
- • Use platform sorting in production; full TimSort is subtle, heavily tuned infrastructure.
- • Test empty/singleton, sorted/reverse, equal-heavy, sawtooth, and alternating run lengths.
- • Tag equal keys to test stability; test strict descending runs containing adjacent equals.
- • Generate adversarial run-length stacks that repeatedly approach collapse boundaries.
- • Property-test sortedness/permutation and invalid comparator behavior separately.
⚡ Complexity Analysis
Time Complexity
- ✓O(n) - Best Case
Already sorted data (adaptive).
- •O(n log n) - Average/Worst
Standard efficient sorting.
Space Complexity
- •O(n)
Requires temporary buffer for merging.
Tip: Why TimSort?
Real-world data is rarely completely random. It often contains ordered subsequences. TimSort is designed to take advantage of this (adaptive), making it much faster than Merge Sort or Quick Sort on real-world data, while maintaining stability and O(n log n) worst-case guarantee. It is the right default when sorting Java objects with a comparator and preserving the order of equal elements matters.
✅ Pros & ❌ Cons
✅ Advantages
- ✓Stable sort (preserves order)
- ✓Adaptive: O(n) for sorted data
- ✓Very fast on real-world data
- ✓Used by CPython and common Java reference-sorting implementations; runtime details vary
❌ Disadvantages
- ✗O(n) space complexity (not in-place)
- ✗Complex implementation