SwadhyAI Notes

All Java notes

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

1

Find runs

Detect ascending or reversible descending sequences.

2

Extend to minrun

Insertion-sort short runs up to a computed minimum.

3

Push and merge

Maintain stack invariants so merges stay balanced.

4

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

1

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.

2

Merge Runs

Push runs onto a stack. Merge runs together using the Merge Sort logic to maintain stability and balance.

3

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]

  1. Find ascending run [1, 2, 3, 7].
  2. Find descending run [6, 5] and reverse it to [5, 6].
  3. 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

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

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.

TimSort run stack example
Stack before pushPushViolationScheduling response
[64, 40]2464 is not greater than 40+24Merge an adjacent pair selected by the collapse policy
[…, B, C]new short runB ≤ CCollapse 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

  1. Why must descending runs be reversed carefully for stability?
  2. What goes wrong if minrun is always 1 on random data?
  3. 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

  1. Why must descending runs be reversed carefully for stability?
  2. What goes wrong if minrun is always 1 on random data?
  3. 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

  1. Why must descending runs be reversed carefully for stability?
  2. What goes wrong if minrun is always 1 on random data?
  3. 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

TimSort complexity by input structure
Input structureNatural runs rExpected work shapeWhy
Already monotone1Θ(n)One scan; a descending run is reversed once
Few long runsr ≪ nOften described as O(n log r)Merge work depends on run structure, not only n
Random/adversarialmany short runsO(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