Insertion Sort
Build a sorted prefix by inserting each new element into its correct place among the ones before it.
Overview
Insertion sort takes elements one at a time and inserts each into the already-sorted prefix by shifting larger elements one slot right. It is stable, in-place, online (can sort a stream as it arrives), and strongly adaptive: the running time is O(n + I) where I is the number of inversions, so nearly sorted input sorts in near-linear time.
It is the fastest sort in practice for small arrays (roughly n ≤ 16–32) because the inner loop is a tight shift with excellent cache and branch behaviour. Every serious library sort — TimSort in Python and Java, introsort in C++ std::sort, pdqsort in Rust and Go — switches to insertion sort for small subranges.
Intuition
A mental model before the formal terms.
Sorting playing cards in your hand: pick up the next card from the table and slide it leftwards past any bigger cards until it sits between a smaller card and a bigger one. The cards already in your hand are always in order.
How it works
- For
i = 1 … n - 1, takekey = a[i]; everything ina[0..i-1]is already sorted. - Set
j = i - 1. Whilej >= 0anda[j] > key, shifta[j]toa[j + 1]and decrementj. - Place
keyata[j + 1]. - Optionally use Binary Search to find the insertion point in
O(log n)comparisons — the shifts remainO(n).
Why it works
Invariant: before step i, a[0..i-1] is a sorted permutation of the original first i elements. The shift loop stops at the first a[j] ≤ key, so all elements right of j are > key and moved right by one; inserting key at j + 1 keeps the prefix sorted.
Using a[j] > key (strict) means equal elements are never moved past key, giving stability.
Each shift removes exactly one inversion, so total shifts equal the inversion count I, giving O(n + I).
Recognition
How to tell a problem wants this.
- Input is nearly sorted or has few inversions ("each element is at most k positions from its sorted place").
- Elements arrive online and the collection must stay sorted after each arrival.
- Small
n(< 32), or the base case of a recursive sort. - A linked list must be sorted in place with
O(1)extra space.
Interactive visualization
Play, step, change the input. ← → and space work too.
1for i in 1 .. n-1:2 key = a[i]3 j = i - 14 while j >= 0 and a[j] > key:5 a[j+1] = a[j]6 j = j - 17 a[j+1] = keyPseudocode
1for i from 1 to n - 1:2 key = a[i]; j = i - 13 while j >= 0 and a[j] > key:4 a[j + 1] = a[j]; j -= 15 a[j + 1] = keyImplementations
1def insertion_sort(a: list[int]) -> None:21 · Walk each element after the first3 for i in range(1, len(a)):42 · Take the key out5 key = a[i]6 j = i - 173 · Shift larger elements one slot right8 while j >= 0 and a[j] > key: # strict '>' keeps equal keys in order9 a[j + 1] = a[j]10 j -= 1114 · Drop the key into the gap12 a[j + 1] = keyrange(1, len(a))is empty for lists of length 0 or 1, so no guard is needed.key = a[i]copies the reference before the slot is overwritten.while j >= 0 and a[j] > key—andshort-circuits; without the guarda[-1]would silently read the last element.- Each shift is one list write; the final assignment places the key.
Python indexing is slow; bisect.insort does binary search + list.insert (memmove in C) and is much faster despite the same O(n) shift.
bisect.insort(a, x)is the stdlib "insert into sorted list" and keeps stability (inserts after equal keys).- Negative indices wrap in Python: forgetting
j >= 0readsa[-1]instead of raising. - CPython's TimSort uses binary insertion sort for runs shorter than minrun.
- Dropping
j >= 0— Python wraps toa[-1]and the sort silently corrupts. - Using
>=which breaks stability. - Using
a.insert(j, key)per element while also shifting — O(n) each and double work.
- Reading index -1: C++ is undefined behaviour, JS/TS return
undefined(comparison silentlyfalse), Python wraps to the last element — thej >= 0guard must come first in every language. - Library insertion helpers: C++
std::upper_bound+std::rotate, Pythonbisect.insort; JS/TS have none (spliceis O(n) with allocation). - Every major library sort (introsort in
std::sort, TimSort in Python and V8) falls back to insertion sort for short ranges. - JS/TS default
sort()is lexicographic; the insertion sort above compares numerically.
Complexity
Exactly O(n + I) where I = number of inversions. Stable, in-place, adaptive, online.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Small arrays (
n ≤ 32) and as the base case of merge/quick/introsort. - Nearly sorted data or data with few inversions.
- Online insertion into a small sorted buffer.
- Sorting linked lists in place with a stable result.
- Large random inputs —
O(n²)shifts; use Merge Sort, Quick Sort, or Heap Sort. - When the number of comparisons is the bottleneck on large
n(binary insertion helps comparisons but not shifts).
Alternatives
Common mistakes
- Using
a[j] >= key— moves equal elements pastkeyand breaks stability. - Swapping instead of shifting; correct but roughly 3× the writes.
- Forgetting
j >= 0in the loop guard (or writing it aftera[j]in a language that evaluates both). - Assuming binary insertion makes it
O(n log n)— the shifts still dominate.
Interview patterns
- Sort a nearly sorted array where each element is at most
kaway: insertion sort inO(nk), or a size-kheap inO(n log k). - Insertion sort on a linked list (LeetCode 147) — splice nodes into a sorted dummy list.
- Explain why library sorts fall back to insertion sort for short ranges.
- Merge a new element into a sorted array in place (the single-step version).
- Recognizing the approach from an array and a targetIntermediate
- When space complexity mattersIntermediate
- Two pointers or hash map?Intermediate
- Convincing me your algorithm is correctExpert
- Two SumBeginner