DSA Sorting
Insertion Sort
A courier app receives GPS pings one by one. Each new reading should land in an already ordered window without reshuffling the whole buffer.
Insertion sort grows a sorted prefix by saving the next key, shifting larger prefix values right, and writing the key into the gap. It is and when the shift uses a strict comparison, and naturally .
Algorithm journey
Keep a sorted prefix
a[0:i] holds the first i records in order.
Save the next key
Pull a[i] into a register so shifts can overwrite safely.
Shift larger values right
Open the unique stable slot for the key.
Insert once
Write the key; the prefix grows by one.
What actually happens
Naive idea: for each new value, rebuild order from scratch. Fine for n = 20. At n = 10^5 reverse-sorted keys, shifts alone approach n^2/2 writes.
Better idea: reuse the sorted prefix. Each insertion costs work proportional to how far the key must travel.
It still fails as a general large-n sort: reverse order is Theta(n^2). Comparison sorting has an Omega(n log n) worst-case lower bound on arbitrary keys.
Invariant
At the start of pass i, a[0:i] contains exactly the first i input records in stable sorted order (not necessarily their final global positions).
How to choose insertion sort
1. Tiny or nearly sorted ranges? It is often the best elementary choice and a finishing phase inside hybrids.
2. Need stability and constant extra memory? Yes, with a strict > shift. Prefer merge/TimSort when n is large and stability matters.
3. Online arrivals? Each new key can be inserted without waiting for the full batch.
Quantitative check
Already sorted n = 10^5: about n - 1 comparisons and zero shifts. Reverse order: roughly n(n - 1)/2 shifts. A random permutation averages about n^2/4 inversions.
The sorted-prefix invariant
At the start of pass i, the prefix a[0:i] contains exactly the first i input records in stable sorted order. It is sorted, but its values are not necessarily in their final global positions; a later smaller key may shift the whole prefix right.
Initialization
Before i = 1, the one-element prefix a[0:1] is sorted and contains exactly the first input record.
Maintenance
Shifting only prefix values greater than key opens the unique stable slot; inserting key preserves order and adds exactly one input record.
Termination
When i reaches n, the prefix contains every input record in sorted order, so it is the required output.
a[j] > key, not while a[j] >= key. The key is inserted after existing equal keys, so their input order is retained.Interactive: key, gap, and sorted prefix
The key register makes the saved value explicit. Each shift performs one array write and moves the gap left; the final insertion performs one more write. Compare nearly sorted and reverse inputs to see adaptivity in the counters.
Insertion shift laboratory
Lift one key, shift larger prefix values right, then fill the gap.
The letters preserve original identity: watch whether 4a stays before 4b.
Pass 0 · start
Step 1 / 17
What changed
A one-element prefix is already sorted.
Invariant
Before pass 1, index 0 contains exactly the first input item in sorted order.
Counting convention: one key comparison compares two values; one algorithmic assignment into an array cell is one write. During insertion, clearing the displayed source is only a conceptual gap animation, not an extra assignment. A swap uses two array writes (the temporary local variable is not counted).
Original visualizer · preserved
Interactive insertion sort demo
Watch the active key move left until it reaches its position in the sorted prefix.
Reference implementation
Save the key before shifting. Otherwise the first write overwrites it. Shifts are preferable to repeated adjacent swaps because one displaced value costs one array write instead of two.
Python
def insertion_sort(a):
for i in range(1, len(a)):
key = a[i]
j = i - 1
# Shift only strictly larger values: this preserves stability.
while j >= 0 and a[j] > key:
a[j + 1] = a[j]
j -= 1
a[j + 1] = key
return aJava
final class InsertionSort {
static int[] sort(int[] a) {
for (int i = 1; i < a.length; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
return a;
}
}Pass-by-pass trace
The bar marks the sorted-prefix boundary for [5, 2, 4, 6, 1, 3]. Every row inserts the first value to the right of the bar, then moves the boundary one position right.
| Key | Before | Action | After |
|---|---|---|---|
| 2 | [5 | 2, 4, 6, 1, 3] | shift 5; insert 2 | [2, 5 | 4, 6, 1, 3] |
| 4 | [2, 5 | 4, 6, 1, 3] | shift 5; insert 4 | [2, 4, 5 | 6, 1, 3] |
| 6 | [2, 4, 5 | 6, 1, 3] | no shift | [2, 4, 5, 6 | 1, 3] |
| 1 | [2, 4, 5, 6 | 1, 3] | shift four values | [1, 2, 4, 5, 6 | 3] |
| 3 | [1, 2, 4, 5, 6 | 3] | shift 6, 5, 4 | [1, 2, 3, 4, 5, 6] |
Inversions explain the running time
An inversion is a pair i < j with a[i] > a[j]. When key a[i] is inserted, the algorithm shifts once for each earlier value that forms an inversion with it. Across the whole run, the number of shifts is therefore exactly the original inversion count I.
Best: sorted
Θ(n)
I = 0. Exactly n − 1 key comparisons, no shifts, and n − 1 final key writes in the reference code.
Average: random
Θ(n²)
For distinct keys in random order, E[I] = n(n − 1)/4, so expected shifts and time are quadratic.
Worst: reversed
Θ(n²)
I = n(n − 1)/2. Every key moves to the front; shifts equal comparisons under the usual short-circuit loop.
A more precise bound than “quadratic”
II + F, where 0 ≤ F ≤ n − 1 counts failed stopping comparisonsI + n − 1 for the shown codeIncluding loop overhead, time is Θ(n + I). This is genuine adaptivity: if only O(n) inversions exist, insertion sort runs in linear time even if they are spread throughout the array.
Why it works well on small or nearly sorted ranges
Low overhead
Tight loops, sequential memory access, and no recursion or allocation give it small constant factors.
Natural runs help
A key already after a no-greater predecessor needs one comparison and no shifts. Existing order is reused directly.
Hybrid-sort base case
Practical merge- and quicksort-family implementations often switch to insertion sort for short subarrays. The cutoff is implementation- and hardware-dependent.
Online processing
After each arriving item is inserted, everything seen so far is sorted. Maintaining a large array this way can still make each arrival expensive.
Binary insertion: fewer comparisons, same movement
Because the prefix is sorted, binary search can find the insertion position in Θ(log i) comparisons. But an array must still shift up to i values, so worst-case time remains Θ(n²). This variant matters when comparisons are expensive: long strings or computed keys, but writes are comparatively cheap.
Python
def binary_insertion_sort(a):
for i in range(1, len(a)):
key = a[i]
# upper_bound: insert after existing equal keys for stability
lo, hi = 0, i
while lo < hi:
mid = (lo + hi) // 2
if a[mid] <= key:
lo = mid + 1
else:
hi = mid
a[lo + 1:i + 1] = a[lo:i] # conceptual right shift
a[lo] = keyJava
final class BinaryInsertionSort {
static void sort(int[] a) {
for (int i = 1; i < a.length; i++) {
int key = a[i];
// upper_bound: insert after equals for stability
int lo = 0;
int hi = i;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (a[mid] <= key) {
lo = mid + 1;
} else {
hi = mid;
}
}
for (int j = i; j > lo; j--) {
a[j] = a[j - 1];
}
a[lo] = key;
}
}
}The slice assignment is concise Python, not an in-place O(1)-space teaching implementation: Python materializes a temporary slice. Use an explicit backward shift when auxiliary-space accounting matters.
Shifts versus adjacent swaps
Shift-based insertion
Save one key, write each larger value one cell right, then place the key. The reference implementation uses I + n − 1 array writes and one local key variable.
Swap-based insertion
Repeatedly swap the key with its predecessor. It is easy to animate and still performs exactly Iswaps, but that means 2I array writes and usually more assignment overhead.
Variants and data-structure nuances
Sentinel insertion sort
Place a smallest sentinel at index 0 so the inner loop need not check j ≥ 0 each iteration. It can reduce branching, but finding or creating a safe sentinel complicates generic code.
Shell sort
Run insertion sorts over elements separated by a shrinking gap. Early passes move far-away values quickly; the final gap-1 pass finishes the array. Complexity depends on the gap sequence.
Linked-list insertion
Insertion itself can relink nodes in O(1), but locating each position is still linear without extra structure, so total time remains Θ(n²). It avoids array shifting.
Descending and custom keys
Reverse the comparison for descending order. For records, cache an expensive key once per outer pass and preserve strict tie handling if stability matters.
Edge cases and reasoning
Implementation checklist
- Empty and one-element arrays need no special branch; the loop starts at index 1.
- Save
keybefore the first shift and scan backward so unread values are not overwritten. - After the loop, insert at
j + 1, notj. - All-equal input is linear and stable when the condition is strictly
>. - Define behavior for incomparable values such as
NaNor inconsistent custom comparators.
Checks that sharpen the proof
- Why is the prefix sorted but not necessarily final?
- Why do shifts equal the inversion count exactly?
- What makes the implementation stable, and how can one operator break it?
- Why does binary search not improve the Θ(n²) worst-case time?
- Why do hybrid algorithms use insertion sort only below a tuned cutoff?
Exercises
- Prove that shifting while
a[j] >= keycan reorder equal tagged keys. Give a three-element counterexample. - For n = 8 x 10^3 with I = 50 inversions, estimate comparisons under a Theta(n + I) model. When does merge sort still win on constants?
- Why can the sorted prefix values move again later? How does that differ from selection sort's final prefix?
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): insertion sort and loop invariants.
- Knuth, The Art of Computer Programming, Vol. 3: insertion sorting and inversion tables.
- Python list.sort / TimSort notes: insertion as a run extender for small runs.
Takeaway
Insertion sort is stable, in place, online, and genuinely adaptive: its work is Θ(n + I), where I is the input's inversion count. Quadratic worst-case movement prevents it from scaling alone, but its low overhead and excellent behavior on short or nearly sorted ranges make it an important building block inside faster hybrid sorts.