Tim Sort

Hybrid of merge sort and insertion sort. Finds natural runs, extends them, and merges with a sophisticated merge strategy. Used by Python and Java.

Best O(n) Avg O(n log n) Worst O(n log n) Space O(n) Stable Yes In-place No Comparison-based

How it works

Hybrid of merge sort and insertion sort. Finds natural runs, extends them, and merges with a sophisticated merge strategy. Used by Python and Java.

Implementation

function timSort(arr, stats) {
  const n = arr.length;
  if (n < 2) return;
  const MIN_MERGE = 32;
  const MIN_GALLOP = 7;
  let minGallop = MIN_GALLOP;
  let tmp = new Array(n < 2 * MIN_MERGE ? n >>> 1 : 256);
  let tmpLen = tmp.length;
  const stackLen = n < 120 ? 5 : n < 1542 ? 10 : n < 119151 ? 24 : 49;
  const runBase = new Array(stackLen);
  const runLen = new Array(stackLen);
  let stackSize = 0;
  const metrics = stats.educational.metrics;
  const recordMetric = (name, delta = 1) => {
    metrics[name] = (metrics[name] || 0) + delta;
  };
  metrics.minMerge = MIN_MERGE;
  metrics.initialMinGallop = MIN_GALLOP;
  const compareValues = (left, right) => {
    if (left < right) return -1;
    if (left > right) return 1;
    return 0;
  };
  function writeValue(index, value) {
    arr[index] = value;
    recordMetric('writeOperations');
    write(index, value);
  }
  function swapValues(i, j) {
    const value = arr[i];
    arr[i] = arr[j];
    arr[j] = value;
    recordMetric('pairSwaps');
    swap(i, j);
  }
  function copyArray(src, srcPos, dest, destPos, length) {
    if (src === dest && srcPos < destPos && srcPos + length > destPos) {
      for (let i = length - 1; i >= 0; i--) {
        dest[destPos + i] = src[srcPos + i];
      }
      return;
    }
    for (let i = 0; i < length; i++) {
      dest[destPos + i] = src[srcPos + i];
    }
  }
  function copyTmpToArr(tmpPos, destPos, length) {
    for (let i = 0; i < length; i++) {
      writeValue(destPos + i, tmp[tmpPos + i]);
    }
  }
  function copyArrToArr(srcPos, destPos, length) {
    if (srcPos < destPos && srcPos + length > destPos) {
      for (let i = length - 1; i >= 0; i--) {
        writeValue(destPos + i, arr[srcPos + i]);
      }
      return;
    }
    for (let i = 0; i < length; i++) {
      writeValue(destPos + i, arr[srcPos + i]);
    }
  }
  function reverseRange(lo, hi) {
    hi--;
    while (lo < hi) {
      swapValues(lo, hi);
      lo++;
      hi--;
    }
    recordMetric('reversedRuns');
  }
  function binarySort(lo, hi, start) {
    if (start === lo) start++;
    for (; start < hi; start++) {
      const pivot = arr[start];
      let left = lo;
      let right = start;
      while (left < right) {
        const mid = left + right >>> 1;
        compare(start, mid);
        if (compareValues(pivot, arr[mid]) < 0) right = mid; else left = mid + 1;
      }
      for (let i = start; i > left; i--) {
        writeValue(i, arr[i - 1]);
      }
      writeValue(left, pivot);
      recordMetric('binaryInsertionPlacements');
    }
  }
  function countRunAndMakeAscending(lo, hi) {
    let runHi = lo + 1;
    if (runHi === hi) return 1;
    compare(runHi, lo);
    if (compareValues(arr[runHi], arr[lo]) < 0) {
      runHi++;
      while (runHi < hi) {
        compare(runHi, runHi - 1);
        if (!(compareValues(arr[runHi], arr[runHi - 1]) < 0)) break;
        runHi++;
      }
      reverseRange(lo, runHi);
    } else {
      runHi++;
      while (runHi < hi) {
        compare(runHi, runHi - 1);
        if (compareValues(arr[runHi], arr[runHi - 1]) < 0) break;
        runHi++;
      }
    }
    recordMetric('runsDetected');
    return runHi - lo;
  }
  function minRunLength(value) {
    let r = 0;
    while (value >= MIN_MERGE) {
      r |= value & 1;
      value >>= 1;
    }
    return value + r;
  }
  function pushRun(base, length) {
    runBase[stackSize] = base;
    runLen[stackSize] = length;
    stackSize++;
    recordMetric('stackPushes');
  }
  function gallopLeft(key, source, base, len, hint, keyIndex) {
    recordMetric('gallopLeftCalls');
    let lastOfs = 0;
    let ofs = 1;
    if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint);
    if (compareValues(key, source[base + hint]) > 0) {
      const maxOfs = len - hint;
      while (ofs < maxOfs) {
        if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint + ofs);
        if (!(compareValues(key, source[base + hint + ofs]) > 0)) break;
        lastOfs = ofs;
        ofs = (ofs << 1) + 1;
        if (ofs <= 0) ofs = maxOfs;
      }
      if (ofs > maxOfs) ofs = maxOfs;
      lastOfs += hint;
      ofs += hint;
    } else {
      const maxOfs = hint + 1;
      while (ofs < maxOfs) {
        if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint - ofs);
        if (!(compareValues(key, source[base + hint - ofs]) <= 0)) break;
        lastOfs = ofs;
        ofs = (ofs << 1) + 1;
        if (ofs <= 0) ofs = maxOfs;
      }
      if (ofs > maxOfs) ofs = maxOfs;
      const tmpOfs = lastOfs;
      lastOfs = hint - ofs;
      ofs = hint - tmpOfs;
    }
    lastOfs++;
    while (lastOfs < ofs) {
      const mid = lastOfs + (ofs - lastOfs >>> 1);
      if (keyIndex >= 0 && source === arr) compare(keyIndex, base + mid);
      if (compareValues(key, source[base + mid]) > 0) lastOfs = mid + 1; else ofs = mid;
    }
    return ofs;
  }
  function gallopRight(key, source, base, len, hint, keyIndex) {
    recordMetric('gallopRightCalls');
    let lastOfs = 0;
    let ofs = 1;
    if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint);
    if (compareValues(key, source[base + hint]) < 0) {
      const maxOfs = hint + 1;
      while (ofs < maxOfs) {
        if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint - ofs);
        if (!(compareValues(key, source[base + hint - ofs]) < 0)) break;
        lastOfs = ofs;
        ofs = (ofs << 1) + 1;
        if (ofs <= 0) ofs = maxOfs;
      }
      if (ofs > maxOfs) ofs = maxOfs;
      const tmpOfs = lastOfs;
      lastOfs = hint - ofs;
      ofs = hint - tmpOfs;
    } else {
      const maxOfs = len - hint;
      while (ofs < maxOfs) {
        if (keyIndex >= 0 && source === arr) compare(keyIndex, base + hint + ofs);
        if (!(compareValues(key, source[base + hint + ofs]) >= 0)) break;
        lastOfs = ofs;
        ofs = (ofs << 1) + 1;
        if (ofs <= 0) ofs = maxOfs;
      }
      if (ofs > maxOfs) ofs = maxOfs;
      lastOfs += hint;
      ofs += hint;
    }
    lastOfs++;
    while (lastOfs < ofs) {
      const mid = lastOfs + (ofs - lastOfs >>> 1);
      if (keyIndex >= 0 && source === arr) compare(keyIndex, base + mid);
      if (compareValues(key, source[base + mid]) < 0) ofs = mid; else lastOfs = mid + 1;
    }
    return ofs;
  }
  function ensureCapacity(minCapacity) {
    if (tmpLen >= minCapacity) return;
    let newSize = minCapacity;
    newSize |= newSize >> 1;
    newSize |= newSize >> 2;
    newSize |= newSize >> 4;
    newSize |= newSize >> 8;
    newSize |= newSize >> 16;
    newSize++;
    if (newSize < 0) newSize = minCapacity; else newSize = Math.max(minCapacity, Math.min(newSize, n >>> 1));
    tmp = new Array(newSize);
    tmpLen = newSize;
    recordMetric('tmpResizes');
  }
  function mergeLo(base1, len1, base2, len2) {
    recordMetric('mergeLoCalls');
    ensureCapacity(len1);
    copyArray(arr, base1, tmp, 0, len1);
    let cursor1 = 0;
    let cursor2 = base2;
    let dest = base1;
    writeValue(dest++, arr[cursor2++]);
    len2--;
    if (len2 === 0) {
      copyTmpToArr(cursor1, dest, len1);
      return;
    }
    if (len1 === 1) {
      copyArrToArr(cursor2, dest, len2);
      writeValue(dest + len2, tmp[cursor1]);
      return;
    }
    let localMinGallop = minGallop;
    while (true) {
      let count1 = 0;
      let count2 = 0;
      let exitOuter = false;
      do {
        if (compareValues(arr[cursor2], tmp[cursor1]) < 0) {
          writeValue(dest++, arr[cursor2++]);
          count2++;
          count1 = 0;
          len2--;
          if (len2 === 0) {
            exitOuter = true;
            break;
          }
        } else {
          writeValue(dest++, tmp[cursor1++]);
          count1++;
          count2 = 0;
          len1--;
          if (len1 === 1) {
            exitOuter = true;
            break;
          }
        }
      } while ((count1 | count2) < localMinGallop);
      if (exitOuter) break;
      do {
        count1 = gallopRight(arr[cursor2], tmp, cursor1, len1, 0, cursor2);
        if (count1 !== 0) {
          copyTmpToArr(cursor1, dest, count1);
          dest += count1;
          cursor1 += count1;
          len1 -= count1;
          if (len1 <= 1) {
            exitOuter = true;
            break;
          }
        }
        writeValue(dest++, arr[cursor2++]);
        len2--;
        if (len2 === 0) {
          exitOuter = true;
          break;
        }
        count2 = gallopLeft(tmp[cursor1], arr, cursor2, len2, 0, -1);
        if (count2 !== 0) {
          copyArrToArr(cursor2, dest, count2);
          dest += count2;
          cursor2 += count2;
          len2 -= count2;
          if (len2 === 0) {
            exitOuter = true;
            break;
          }
        }
        writeValue(dest++, tmp[cursor1++]);
        len1--;
        if (len1 === 1) {
          exitOuter = true;
          break;
        }
        localMinGallop--;
      } while (count1 >= MIN_GALLOP || count2 >= MIN_GALLOP);
      if (exitOuter) break;
      if (localMinGallop < 0) localMinGallop = 0;
      localMinGallop += 2;
    }
    minGallop = localMinGallop < 1 ? 1 : localMinGallop;
    metrics.finalMinGallop = minGallop;
    if (len1 === 1) {
      copyArrToArr(cursor2, dest, len2);
      writeValue(dest + len2, tmp[cursor1]);
    } else {
      copyTmpToArr(cursor1, dest, len1);
    }
  }
  function mergeHi(base1, len1, base2, len2) {
    recordMetric('mergeHiCalls');
    ensureCapacity(len2);
    copyArray(arr, base2, tmp, 0, len2);
    let cursor1 = base1 + len1 - 1;
    let cursor2 = len2 - 1;
    let dest = base2 + len2 - 1;
    writeValue(dest--, arr[cursor1--]);
    len1--;
    if (len1 === 0) {
      copyTmpToArr(0, dest - (len2 - 1), len2);
      return;
    }
    if (len2 === 1) {
      dest -= len1;
      cursor1 -= len1;
      copyArrToArr(cursor1 + 1, dest + 1, len1);
      writeValue(dest, tmp[cursor2]);
      return;
    }
    let localMinGallop = minGallop;
    while (true) {
      let count1 = 0;
      let count2 = 0;
      let exitOuter = false;
      do {
        if (compareValues(tmp[cursor2], arr[cursor1]) < 0) {
          writeValue(dest--, arr[cursor1--]);
          count1++;
          count2 = 0;
          len1--;
          if (len1 === 0) {
            exitOuter = true;
            break;
          }
        } else {
          writeValue(dest--, tmp[cursor2--]);
          count2++;
          count1 = 0;
          len2--;
          if (len2 === 1) {
            exitOuter = true;
            break;
          }
        }
      } while ((count1 | count2) < localMinGallop);
      if (exitOuter) break;
      do {
        const gallopRightOffset = gallopRight(tmp[cursor2], arr, base1, len1, len1 - 1, -1);
        count1 = len1 - gallopRightOffset;
        if (count1 !== 0) {
          dest -= count1;
          cursor1 -= count1;
          len1 -= count1;
          copyArrToArr(cursor1 + 1, dest + 1, count1);
          if (len1 === 0) {
            exitOuter = true;
            break;
          }
        }
        writeValue(dest--, tmp[cursor2--]);
        len2--;
        if (len2 === 1) {
          exitOuter = true;
          break;
        }
        const gallopLeftOffset = gallopLeft(arr[cursor1], tmp, 0, len2, len2 - 1, cursor1);
        count2 = len2 - gallopLeftOffset;
        if (count2 !== 0) {
          dest -= count2;
          cursor2 -= count2;
          len2 -= count2;
          copyTmpToArr(cursor2 + 1, dest + 1, count2);
          if (len2 <= 1) {
            exitOuter = true;
            break;
          }
        }
        writeValue(dest--, arr[cursor1--]);
        len1--;
        if (len1 === 0) {
          exitOuter = true;
          break;
        }
        localMinGallop--;
      } while (count1 >= MIN_GALLOP || count2 >= MIN_GALLOP);
      if (exitOuter) break;
      if (localMinGallop < 0) localMinGallop = 0;
      localMinGallop += 2;
    }
    minGallop = localMinGallop < 1 ? 1 : localMinGallop;
    metrics.finalMinGallop = minGallop;
    if (len2 === 1) {
      dest -= len1;
      cursor1 -= len1;
      copyArrToArr(cursor1 + 1, dest + 1, len1);
      writeValue(dest, tmp[cursor2]);
    } else {
      copyTmpToArr(0, dest - (len2 - 1), len2);
    }
  }
  function mergeAt(index) {
    recordMetric('mergeAtCalls');
    let base1 = runBase[index];
    let len1 = runLen[index];
    const base2 = runBase[index + 1];
    let len2 = runLen[index + 1];
    runLen[index] = len1 + len2;
    if (index === stackSize - 3) {
      runBase[index + 1] = runBase[index + 2];
      runLen[index + 1] = runLen[index + 2];
    }
    stackSize--;
    const k = gallopRight(arr[base2], arr, base1, len1, 0, base2);
    base1 += k;
    len1 -= k;
    if (len1 === 0) return;
    len2 = gallopLeft(arr[base1 + len1 - 1], arr, base2, len2, len2 - 1, base1 + len1 - 1);
    if (len2 === 0) return;
    if (len1 <= len2) mergeLo(base1, len1, base2, len2); else mergeHi(base1, len1, base2, len2);
  }
  function mergeCollapse() {
    while (stackSize > 1) {
      let index = stackSize - 2;
      if (index > 0 && runLen[index - 1] <= runLen[index] + runLen[index + 1] || index - 1 > 0 && runLen[index - 2] <= runLen[index - 1] + runLen[index]) {
        if (runLen[index - 1] < runLen[index + 1]) index--;
        mergeAt(index);
      } else if (runLen[index] <= runLen[index + 1]) {
        mergeAt(index);
      } else {
        break;
      }
    }
  }
  function mergeForceCollapse() {
    while (stackSize > 1) {
      let index = stackSize - 2;
      if (index > 0 && runLen[index - 1] < runLen[index + 1]) index--;
      mergeAt(index);
    }
  }
  let lo = 0;
  let remaining = n;
  if (remaining < MIN_MERGE) {
    const initRunLen = countRunAndMakeAscending(lo, n);
    binarySort(lo, n, lo + initRunLen);
    checkpoint(n, n);
    for (let i = 0; i < n; i++) markSorted(i);
    return;
  }
  const minRun = minRunLength(remaining);
  metrics.minRun = minRun;
  do {
    let currentRunLen = countRunAndMakeAscending(lo, n);
    if (currentRunLen < minRun) {
      const forcedRunLen = remaining <= minRun ? remaining : minRun;
      binarySort(lo, lo + forcedRunLen, lo + currentRunLen);
      currentRunLen = forcedRunLen;
      recordMetric('forcedRuns');
    }
    pushRun(lo, currentRunLen);
    mergeCollapse();
    lo += currentRunLen;
    remaining -= currentRunLen;
    checkpoint(lo, n);
  } while (remaining !== 0);
  mergeForceCollapse();
  checkpoint(n, n);
  for (let i = 0; i < n; i++) markSorted(i);
}