Begin Algo
Sorting · 03 / 09

Insertion SortInsertion sort

Like sorting a hand of cards; O(n) when nearly sorted.

Used for: Small or nearly sorted arrays — built-in sorts switch to it for short runs

Time complexityO(n²)
Space complexityO(1)
DifficultyIntro
PrerequisitesArray & Dynamic Array, Bubble Sort

01Why it exists

The last mile of a standard-library sort

To sort a million integers, quicksort and merge sort keep halving the range, and near the bottom that leaves tens of thousands of runs holding barely a dozen elements each. Recursing again on a run that small — picking a pivot, allocating scratch space — burns more time on bookkeeping than on the comparisons themselves.

Why this fitsWhen n is small, constants matter more than growth rates. Insertion sort has no recursion, moves data only between neighbouring slots, and is kind to the CPU cache, so at a dozen elements it beats any O(n log n) algorithm. LLVM libc++'s std::sort switches to insertion sort once a range drops below 24 elements, and Python's Timsort uses binary insertion sort to pad short runs out to 32-64 elements before merging them.

Sensor time series: readings that land a few slots late

A temperature sensor on a factory floor sends 10 timestamped readings per second, 864,000 a day. Network jitter makes a handful of packets arrive late, but no reading is ever more than 5 slots away from where it belongs. The stream has to be ordered by timestamp before it is archived.

Why this fitsInsertion sort costs O(n + inversions). Each reading moves back at most 5 slots, so a full day takes under 4.32 million shifts and roughly 5.18 million comparisons — and it can sort as the data arrives. Quicksort and heapsort, which never look at how tidy the input already is, sit at n log₂ n ≈ 17 million comparisons and have to wait for the whole day's data first.

A race leaderboard as runners cross the line

A road race starts in waves, and 60 runners cross the line one after another. Each runner's net time is their finish time minus the start of their own wave, so finishing later does not mean being slower. The board has to update the standings the instant a runner crosses.

Why this fitsInsertion sort's invariant — the first i entries are always in order — makes it an online algorithm by nature: a new time comes in, you shift it back from the end into place, and nobody else's relative order changes. All 60 runners cost at most 1,770 shifts, and because the sort is stable, runners with identical times stay in the order they finished.

Reach for it when you see:Small arrays (a few dozen at most), nearly sorted data, every element close to its final position, items arriving one at a time that must stay ordered, a stable in-place sort, the small runs inside a hybrid sort.

02The core idea

Insertion sort is how you tidy a hand of playing cards: your left hand holds a pile that is already in order, and each new card you draw gets slotted in by scanning right to left for the spot it belongs. On an array, that means splitting it into a sorted region a[0..i-1] on the left and the untouched part on the right. Each round saves a[i] into key, which leaves a "hole" where it used to sit. Looking left, every element larger than key shifts one slot right and the hole follows it leftwards, until you hit the first element that is not larger than key (or run off the left end) and drop key into the hole.

Correctness rests on a loop invariant: before round i, a[0..i-1] is exactly the first i original elements in sorted order. Everything shifted during the round is larger than key, and the whole block moves right together, so their relative order is untouched. The position j where the loop stops satisfies a[j] ≤ key, and everything to its left is ≤ a[j], so once key lands at j+1 the region a[0..i] is still sorted. Run that through i = n−1 and the whole array is sorted. Because it shifts only on a strictly greater comparison, key never crosses an element equal to it, which makes insertion sort stable; and since it needs just one extra temporary, it sorts in place.

For complexity, counting the shifts is enough. Every shift removes exactly one inversion (the pair of key and the element it just passed), so the total shift count equals the array's inversion count I. Each round adds at most one more comparison, the one that fails and stops the loop, so comparisons fall between I and I + (n−1) and the total time is O(n + I). On sorted input I = 0 and only n−1 comparisons happen, the best case of O(n); on fully reversed input I = n(n−1)/2, the worst case of O(n²); on random data the expected inversion count is n(n−1)/4, so the average is still O(n²). If no element sits more than k slots from its final position, no element shifts left more than k times, and the time becomes O(nk). Extra space is O(1).

Three mistakes come up again and again. The loop condition must include j >= 0, and it must come before a[j] > key: in C++, dropping it or swapping the order reads out of bounds at a[-1], while in Python a[-1] is a perfectly legal "last element", so a missing check may raise an IndexError or, worse, silently scramble the data. Writing >= instead of > lets equal elements cross each other and throws away stability. Swapping instead of shifting gives the same answer, but turns one write per step into three — and what you have built is really bubble sort's adjacent swapping. Against the neighbours: bubble sort performs exactly as many swaps as there are inversions, but insertion sort writes only once per step; selection sort always spends n(n−1)/2 comparisons no matter what the data looks like, whereas insertion sort speeds up on its own as the input gets tidier (it is adaptive). It is also an online algorithm, keeping the data ordered as items arrive one at a time. Merge sort and quicksort both switch to it on small runs, and generalising "shift by one" into "shift by gap" gives you shell sort.

03The algorithm

  1. 1The outer loop runs i from 1 to n−1. At the start of round i, a[0..i-1] is already sorted; it begins as just a[0], and a single element is sorted by itself.
  2. 2Save a[i] into key and set j = i − 1. Slot a[i] is now a hole, so overwriting it loses nothing.
  3. 3while j >= 0 and a[j] > key: a[j+1] = a[j], j -= 1. Test j >= 0 first, and shift only on a strictly greater comparison — that is what keeps the sort stable.
  4. 4The loop stops with a[j] ≤ key or j = −1. Write key into a[j+1], and the sorted region grows to a[0..i].
  5. 5When comparisons are expensive, use bisect_right (upper_bound) to locate the insertion point inside a[0..i-1] and shift the block in one go. To run it on a hybrid sort's small runs, replace the left bound 0 with the run's start lo.

04Interactive demo

The shared array is [5, 2, 9, 1, 7, 3, 8, 4]. Each round lifts one element into the hand, leaving a dashed hole behind it. Green is the sorted region, amber is the element just compared with the card in hand (if it is larger it shifts one slot right and the hole moves left), and blue is where the card was just inserted. Notice that inserting 9 stops after a single comparison, while inserting 1 shifts all the way to the left end. The run ends with 13 shifts, exactly this array's inversion count. There are 18 comparisons: the 13 shifts plus the 5 comparisons that stopped the loop (inserting 2 and 1 ran off the left end, so those rounds need no stopping comparison), which sits right inside the range I to I + (n−1) = 20.

Start[5, 2, 9, 1, 7, 3, 8, 4] · ascending
Array (a dashed cell is the hole)
52917384
Green is the sorted region, amber is the element just compared with the card in hand, and blue is where the card was inserted.
Card in hand
empty
Comparisons: 0
Shifts: 0
Step 0/33Think of the array as a hand of cards. The single card on the left is trivially sorted; each turn you pick up the next card and walk left until you find the spot it belongs in.

05Code

The basic version plus two variants. Binary insertion sort cuts comparisons to O(n log n) while the moves stay O(n²), which pays off for expensive strings or objects — it is what Timsort uses. In C++ that means std::upper_bound to find the slot and one std::rotate to shift the block. The range version sorts only a[lo..hi], the shape a hybrid sort calls on its small runs.

from bisect import bisect_right


# Insertion sort: a[0..i-1] is already sorted. Lift a[i], shift everything larger one slot right, drop it in the hole.
def insertion_sort(a):
    for i in range(1, len(a)):
        key = a[i]                          # the card in hand; a[i] is now a hole
        j = i - 1
        while j >= 0 and a[j] > key:        # strictly greater, so equals never cross: stable
            a[j + 1] = a[j]                 # shift one slot right, the hole moves left
            j -= 1
        a[j + 1] = key                      # drop it into the hole
    return a


# Variant 1: binary insertion sort.
# Binary search finds the slot, so comparisons drop to O(n log n); the moves are still O(n²). Good when comparing is expensive.
def binary_insertion_sort(a):
    for i in range(1, len(a)):
        key = a[i]
        pos = bisect_right(a, key, 0, i)    # land to the right of equal elements: stable
        a[pos + 1:i + 1] = a[pos:i]         # shift the whole block one slot right
        a[pos] = key
    return a


# Variant 2: sort only a[lo..hi] (both ends inclusive).
# A hybrid sort calls this once a run gets small, instead of recursing further.
def insertion_sort_range(a, lo, hi):
    for i in range(lo + 1, hi + 1):
        key = a[i]
        j = i - 1
        while j >= lo and a[j] > key:       # the left bound is lo, not 0
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = key


if __name__ == "__main__":
    print(insertion_sort([5, 2, 9, 1, 7, 3, 8, 4]))                  # [1, 2, 3, 4, 5, 7, 8, 9]
    print(binary_insertion_sort(["pear", "fig", "apple", "kiwi"]))  # ['apple', 'fig', 'kiwi', 'pear']
    b = [9, 8, 7, 6, 5, 4, 3, 2, 1]
    insertion_sort_range(b, 2, 6)                                    # sorts indices 2..6 only
    print(b)                                                         # [9, 8, 3, 4, 5, 6, 7, 2, 1]

06Practice

  • LeetCode 2418Sort the People (hand-write insertion sort, moving names along with heights)Easy
  • LeetCode 147Insertion Sort List (a linked list needs no shifting, but you scan from the head)Medium
  • LeetCode 57Insert Interval (find the slot in a sorted list, then merge the overlaps)Medium
  • LeetCode 775Global and Local Inversions (when nothing is more than one slot off, every inversion is adjacent)Medium
  • LeetCode 1649Create Sorted Array through Instructions (the cost of each insertion, sped up with a BIT)Hard