Insertion Sort
BasisPrerequisites
Think of sorting a hand of playing cards. You pick cards one at a time from the table and slide each new card left through your hand until it sits behind the next smaller card. That is insertion sort: simple, on nearly-sorted input, and fast enough that real-world hybrid sorts like Timsort use it for small subarrays.
The algorithm
Insertion sort maintains a sorted prefix arr[0..i]. At each step it takes the element at position i (call it key) and shifts every larger element in the sorted prefix one step to the right, then drops key into the gap.
fn insertionSort(arr: []i64) void {
var i: usize = 1;
while (i < arr.len) : (i += 1) {
const key = arr[i];
var j = i;
// Shift elements larger than key one position to the right.
while (j > 0 and arr[j - 1] > key) : (j -= 1) {
arr[j] = arr[j - 1];
}
arr[j] = key;
}
}
The inner while loop exits as soon as it finds an element that is ≤ key, or when it reaches the start of the array. arr[j] = key then places key in the correct position.
Correctness via loop invariant
Invariant: at the start of iteration , arr[0..i] is sorted.
- Before iteration 1 ():
arr[0..1]is a single element — trivially sorted. - Maintenance: the inner loop finds the correct insertion position by shifting all elements in
arr[0..i]that are greater thankeyone step right. Afterarr[j] = key, the prefixarr[0..i+1]is sorted, re-establishing the invariant for . - Termination: increases each iteration and reaches
arr.len, at which point the invariant states thatarr[0..arr.len]— the full array — is sorted.
Stability
Insertion sort is stable: the inner loop shifts only elements that are strictly greater than key. Elements equal to key stop the shift, so key is inserted immediately after them, preserving their original relative order.
Complexity
The worst case: reverse-sorted input
When the array is sorted in reverse order, every new element must travel all the way to position 0. Iteration performs shifts:
The best case: already-sorted input
When the array is already sorted, every key satisfies arr[j - 1] <= key immediately — the inner loop does zero iterations. Total work: comparisons, .
The general case: inversions
An inversion is a pair of indices with and arr[i] > arr[j]. Each shift in the inner loop resolves exactly one inversion. Insertion sort therefore performs exactly as many shifts as there are inversions in the input.
- Sorted input: 0 inversions → shifts.
- Nearly-sorted input (at most inversions per element): shifts.
- Reverse-sorted: inversions → shifts.
This inversion-based view explains why insertion sort is the right choice when input is nearly sorted.
| Case | Comparisons | Shifts |
|---|---|---|
| Best (sorted) | ||
| Worst (reverse sorted) | ||
| Average |
Space complexity is : only key and j are needed beyond the array.
Why insertion sort is practical
Despite its worst case, insertion sort beats merge sort and quick sort for small arrays. It has:
- No recursion overhead — no call stack frames beyond the outer loop.
- No auxiliary memory — merge sort needs extra space.
- Excellent cache behaviour — the inner loop reads and writes consecutive memory addresses.
The standard library sort in many languages (Go, Java, Python’s Timsort) switches from algorithms to insertion sort when a sub-array shrinks below roughly 10–32 elements.
Summary
- Insertion sort maintains a sorted prefix by inserting each new element into its correct position via rightward shifts.
- It is stable: equal elements are never reordered.
- Best-case time is (already-sorted input); worst-case is (reverse-sorted).
- The number of shifts equals the number of inversions in the input, making it for nearly-sorted data with at most inversions per element.
- Space is ; there is no recursion.
- Used as the small-array sub-routine inside practical hybrid sorts such as Timsort.