AiTechWorlds
AiTechWorlds
You're playing a card game and the dealer slides cards to you one at a time. You don't wait for all cards to arrive before organizing — you sort as you go. Each new card arrives and you slide it into its correct position among the cards already in your hand.
When you get 7, your hand is: [7]
You get 3 — slide it left of 7: [3, 7]
You get 9 — it's bigger than 7, stays right: [3, 7, 9]
You get 5 — slide it between 3 and 7: [3, 5, 7, 9]
You get 1 — slide all the way left: [1, 3, 5, 7, 9]
At every moment, the cards in your hand are sorted. When the next card arrives, you insert it into the right spot. This is insertion sort — and it's exactly how most people naturally sort things.
The algorithm maintains two logical regions:
[ sorted portion | unsorted portion ]
Visual step-by-step on [12, 11, 13, 5, 6]:
Start: [12 | 11, 13, 5, 6] ← 12 is trivially sorted
Step 1: key = 11
11 < 12 → shift 12 right: [_, 12, 13, 5, 6]
Insert 11 at position 0: [11, 12 | 13, 5, 6]
Step 2: key = 13
13 > 12 → no shift needed
Result: [11, 12, 13 | 5, 6]
Step 3: key = 5
5 < 13 → shift 13 right: [11, 12, _, 13, 6]
5 < 12 → shift 12 right: [11, _, 12, 13, 6]
5 < 11 → shift 11 right: [_, 11, 12, 13, 6]
Insert 5 at position 0: [5, 11, 12, 13 | 6]
Step 4: key = 6
6 < 13 → shift: [5, 11, 12, _, 13]
6 < 12 → shift: [5, 11, _, 12, 13]
6 < 11 → shift: [5, _, 11, 12, 13]
6 > 5 → stop, insert: [5, 6, 11, 12, 13]
Final: [5, 6, 11, 12, 13]
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i] # the element to be inserted
j = i - 1 # start comparing from element just before key
# Shift elements that are greater than key one position to the right
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j] # shift right (not a swap — more efficient)
j -= 1
arr[j + 1] = key # insert key into its correct position
print(f"After inserting {key}: {arr}")
return arr
arr = [12, 11, 13, 5, 6]
result = insertion_sort(arr)
print(f"Sorted: {result}")
Output:
After inserting 11: [11, 12, 13, 5, 6]
After inserting 13: [11, 12, 13, 5, 6]
After inserting 5: [5, 11, 12, 13, 6]
After inserting 6: [5, 6, 11, 12, 13]
Sorted: [5, 6, 11, 12, 13]
Notice: Insertion sort uses shifts, not swaps. When inserting key, elements are moved one position right. This is more efficient than swapping — it touches each element once instead of multiple times.
| Case | Time | Input that causes it |
|---|---|---|
| Best | O(n) | Already sorted array — inner while loop never executes |
| Average | O(n²) | Random data — roughly n²/4 comparisons |
| Worst | O(n²) | Reverse sorted array — every element must slide to position 0 |
| Space | O(1) | In-place, only key and j as extra variables |
Best case trace on [1, 2, 3, 4, 5]:
key = 2: arr[0]=1 < 2 → while loop doesn't execute → 1 comparison
key = 3: arr[1]=2 < 3 → while loop doesn't execute → 1 comparison
key = 4: arr[2]=3 < 4 → while loop doesn't execute → 1 comparison
key = 5: arr[3]=4 < 5 → while loop doesn't execute → 1 comparison
Total: n-1 comparisons = O(n)
This O(n) best case is why insertion sort is used inside more complex algorithms for small or nearly-sorted chunks.
Even though insertion sort is O(n²) in theory, it outperforms merge sort and quicksort for small arrays in practice. Here's why:
1. Cache efficiency Insertion sort accesses memory sequentially — it walks left from the current position. Modern CPUs cache nearby memory, so sequential access is extremely fast. Merge sort and quicksort make scattered memory accesses.
2. Low overhead Complex algorithms have overhead: function calls, pointer arithmetic, stack management. For 10 elements, this overhead often exceeds the cost of the O(n²) comparisons.
3. In-place operation No auxiliary memory allocation needed. For small arrays, memory allocation itself has measurable cost.
Benchmark intuition (approximate):
| Array size | Insertion sort | Merge sort | Winner |
|---|---|---|---|
| n = 5 | ~10 ops | ~15 ops + overhead | Insertion |
| n = 10 | ~25 ops | ~33 ops + overhead | Insertion |
| n = 50 | ~625 ops | ~282 ops | Merge |
| n = 1000 | ~250,000 ops | ~9,966 ops | Merge |
The crossover point is typically around n = 10–20 elements.
Both are O(n²), but insertion sort is meaningfully better:
| Aspect | Insertion Sort | Bubble Sort |
|---|---|---|
| Comparisons (average) | ~n²/4 | ~n²/2 |
| Writes/shifts | ~n²/4 shifts | ~n²/2 swaps |
| Best case | O(n) | O(n) with optimization |
| Stable? | Yes | Yes |
| Cache behavior | Better (sequential) | Similar |
| Online? | Yes — sorts as elements arrive | No |
Online algorithm: Insertion sort can sort a list as it receives elements, without needing the full list first. Bubble sort cannot.
On average, insertion sort does half as many comparisons as bubble sort. For random data with n=1000:
Python's built-in sort() and sorted() use Timsort — a hybrid algorithm invented by Tim Peters in 2002. Timsort splits arrays into small chunks (called "runs") of typically 32–64 elements, then:
This combination is why Python's sort is so fast in practice — it uses the right algorithm for each phase of the work.
# This uses Timsort (insertion sort + merge sort internally):
arr = [5, 2, 8, 1, 9, 3, 7, 4, 6]
arr.sort()
print(arr)
# Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Java's Arrays.sort() for primitive arrays uses a similar hybrid. Understanding insertion sort helps you understand why these production implementations work.
Next lesson: Merge Sort — where we finally break the O(n²) barrier with divide-and-conquer.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises