Quiz 2

05. Selection Sort & Insertion Sort

1817 words
9 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# 05. Selection Sort & Insertion Sort > **What problem does this solve?** You have an unordered list.

05. Selection Sort & Insertion Sort

What problem does this solve? You have an unordered list. You need elements in ascending order. These are the two most intuitive sorting algorithms — but both are (O(n^2)) and only practical for small lists (<10,000 elements).

1. Selection Sort

Mental Model

You're sorting exam papers by score. You scan the entire pile, find the highest score, move it to a new pile. Repeat with the remaining papers. After (n) scans, the new pile is sorted.

How It Works

(Diagram)

Step-by-Step Trace

Sorting [64, 25, 12, 22, 11]:
pseudo
Initial: [64, 25, 12, 22, 11]
Pass 1: Find min in [64, 25, 12, 22, 11] → 11 at index 4
        Swap 64 and 11 → [11, 25, 12, 22, 64]
Pass 2: Find min in [25, 12, 22, 64] → 12 at index 2
        Swap 25 and 12 → [11, 12, 25, 22, 64]
Pass 3: Find min in [25, 22, 64] → 22 at index 3
        Swap 25 and 22 → [11, 12, 22, 25, 64]
Pass 4: Find min in [25, 64] → 25 at index 3
        Already in place → [11, 12, 22, 25, 64]
Sorted! [11, 12, 22, 25, 64]

Implementation

python
# runnable
def selection_sort(arr):
    """Sort arr in-place. Returns sorted array.
    Invariant: After i passes, first i elements are sorted and
    are the i smallest elements in the entire list.
    Time: O(n²) always
    Space: O(1)
    """
    n = len(arr)
    for i in range(n - 1):
        # Find the minimum element in the unsorted portion
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        # Swap the found minimum with the first unsorted position
        if min_idx != i:
            arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr
# Test
arr = [64, 25, 12, 22, 11]
result = selection_sort(arr.copy())
print(f"Sorted: {result}")  # [11, 12, 22, 25, 64]
# Swaps vs Comparisons analysis
def selection_sort_analysis(arr):
    """Return sorted array plus comparison and swap counts."""
    n = len(arr)
    comparisons = 0
    swaps = 0
    for i in range(n - 1):
        min_idx = i
        for j in range(i + 1, n):
            comparisons += 1
            if arr[j] < arr[min_idx]:
                min_idx = j
        if min_idx != i:
            arr[i], arr[min_idx] = arr[min_idx], arr[i]
            swaps += 1
    return arr, comparisons, swaps
arr = [64, 25, 12, 22, 11]
sorted_arr, comps, swps = selection_sort_analysis(arr)
print(f"Sorted: {sorted_arr}, Comparisons: {comps}, Swaps: {swps}")
# Sorted: [11, 12, 22, 25, 64], Comparisons: 10, Swaps: 4

Complexity Analysis

MeasurementValue
Comparisons(n(n-1)/2 = O(n^2)) always
Swaps(n-1 = O(n))
Best case(O(n^2)) — even if already sorted, still scans everything
Worst case(O(n^2))
Average case(O(n^2))
Space(O(1)) in-place
Stable?No — the swap can move equal elements past each other

2. Insertion Sort

Mental Model

You're sorting a hand of playing cards. You pick up cards one by one and insert each into its correct position among the already-sorted cards in your hand.

How It Works

(Diagram)

Step-by-Step Trace

Sorting [64, 25, 12, 22, 11]:
pseudo
Initial: [64, 25, 12, 22, 11]
Pass 1: key = 25, compare with 64, 64 > 25 → shift 64 right
        [64, 64, 12, 22, 11] → insert 25 → [25, 64, 12, 22, 11]
Pass 2: key = 12, compare with 64, 64 > 12 → shift
        [25, 64, 64, 22, 11] → compare with 25, 25 > 12 → shift
        [25, 25, 64, 22, 11] → insert 12 → [12, 25, 64, 22, 11]
Pass 3: key = 22, compare with 64 → shift
        [12, 25, 64, 64, 11] → compare with 25 → shift
        [12, 25, 25, 64, 11] → compare with 12 → stop, insert
        [12, 22, 25, 64, 11]
Pass 4: key = 11, shift all 4 elements right, insert at front
        [11, 12, 22, 25, 64]

Implementation

python
# runnable
def insertion_sort(arr):
    """Sort arr in-place. Returns sorted array.
    Invariant: After i passes, first i elements are sorted
    (but not necessarily the smallest i elements).
    Time: O(n²) worst, O(n) best
    Space: O(1)
    """
    n = len(arr)
    for i in range(1, n):
        key = arr[i]  # Element to be inserted
        j = i - 1
        # Shift elements greater than key to the right
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key  # Insert key in correct position
    return arr
# Test
arr = [64, 25, 12, 22, 11]
result = insertion_sort(arr.copy())
print(f"Sorted: {result}")  # [11, 12, 22, 25, 64]
# Analysis with counters
def insertion_sort_analysis(arr):
    """Return sorted array plus comparison and shift counts."""
    n = len(arr)
    comparisons = 0
    shifts = 0
    for i in range(1, n):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            comparisons += 1
            arr[j + 1] = arr[j]
            shifts += 1
            j -= 1
        if j >= 0:
            comparisons += 1  # The comparison that failed
        arr[j + 1] = key
    return arr, comparisons, shifts
# Test on different inputs
arr1 = [1, 2, 3, 4, 5]  # Already sorted
arr2 = [5, 4, 3, 2, 1]  # Reverse sorted
arr3 = [64, 25, 12, 22, 11]  # Random
for label, arr in [("Already sorted", arr1), ("Reverse sorted", arr2), ("Random", arr3)]:
    _, comps, shifts = insertion_sort_analysis(arr.copy())
    print(f"{label}: {comps} comparisons, {shifts} shifts")
# Already sorted: 4 comparisons, 0 shifts
# Reverse sorted: 14 comparisons, 10 shifts
# Random: 11 comparisons, 7 shifts

Recursive Insertion Sort

python
# runnable
def insertion_sort_recursive(arr, n=None):
    """Recursive insertion sort."""
    if n is None:
        n = len(arr)
    if n <= 1:
        return arr
    # Sort first n-1 elements
    insertion_sort_recursive(arr, n - 1)
    # Insert last element in its correct position
    last = arr[n - 1]
    j = n - 2
    while j >= 0 and arr[j] > last:
        arr[j + 1] = arr[j]
        j -= 1
    arr[j + 1] = last
    return arr
arr = [64, 25, 12, 22, 11]
print(f"Recursive sort: {insertion_sort_recursive(arr)}")

Complexity Analysis

MeasurementValue
Best case (already sorted)(O(n)) — only 1 comparison per element
Worst case (reverse sorted)(O(n^2)) — each element shifts all previous ones
Average case(O(n^2)) — about half the elements shift
ComparisonsBest: (n-1), Worst: (n(n-1)/2)
ShiftsSame as comparisons in worst case
Space(O(1)) in-place
Stable?Yes — equal elements keep original order

3. Comparison: Selection Sort vs Insertion Sort

FeatureSelection SortInsertion Sort
Best case(O(n^2))(O(n))
Worst case(O(n^2))(O(n^2))
Average case(O(n^2))(O(n^2))
Swaps/Shifts(n-1) swaps (few)(O(n^2)) shifts (many)
StableNoYes
Online (sort as data arrives)NoYes
Adaptive (fast on nearly sorted)NoYes
ComparisonsAlways (n(n-1)/2)Varies: (n-1) to (n(n-1)/2)
When to use selection sort: When swapping is expensive (e.g., swapping large structures) — it never makes more than (n-1) swaps. When to use insertion sort: When data is nearly sorted or arrives online (one element at a time). It's also used as the base case in some hybrid sorts (like Timsort).

4. Common Bugs

python
# BUG 1: Off-by-one in selection sort range
def buggy_sel_sort(arr):
    n = len(arr)
    for i in range(n):  # Should be range(n-1)
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr
# Works but makes an unnecessary extra pass (swapping with itself)
# BUG 2: Not using <= for stability in insertion sort
def unstable_insertion(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:  # Using > makes it stable
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
# Using >= would make it unstable (equal elements reversed)
# BUG 3: Forgetting to decrement j in insertion sort
def infinite_insertion(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            # Missing j -= 1 → infinite loop!

Practice Questions

Q1. Trace selection sort on [3, 1, 4, 1, 5, 9, 2, 6]. Show the array after each pass. Q2. How many comparisons does selection sort make on an array of 100 elements? How many swaps? Q3. What input causes insertion sort to run in O(n) time? What input causes O(n²)? Q4. Prove that after i iterations of selection sort's outer loop, the first i elements are the i smallest elements in the entire array. Q5. After 3 iterations of insertion sort on [7, 2, 1, 9, 5, 3], what does the array look like? Q6. Why is insertion sort preferred over selection sort for nearly sorted data? Q7. Write a modified selection sort that sorts in descending order. Q8. You have 5 numbers to sort. Which sort would you use and why? Q9. Count the number of comparisons in insertion sort for input [1, 2, 3, 4, 5, 6, 7, 8]. Q10. If swap costs 10x more than comparison, which algorithm wins? What if comparison costs 10x more?
Answers
A1.
pseudo
[1, 3, 4, 1, 5, 9, 2, 6]  (min=1 at idx 3, swap 3↔1)
[1, 1, 4, 3, 5, 9, 2, 6]  (min=1 at idx 3, swap 3↔1)
[1, 1, 2, 3, 5, 9, 4, 6]  (min=2 at idx 6, swap 4↔2)
[1, 1, 2, 3, 5, 9, 4, 6]  (min=3 at idx 3, already placed)
[1, 1, 2, 3, 4, 9, 5, 6]  (min=4 at idx 6, swap 5↔4)
[1, 1, 2, 3, 4, 5, 9, 6]  (min=5 at idx 6, swap 9↔5)
[1, 1, 2, 3, 4, 5, 6, 9]  (min=6 at idx 7, swap 9↔6)
A2. Comparisons = 100×99/2 = 4,950. Swaps = at most 99.
A3. Best: Already sorted → O(n). Worst: Reverse sorted → O(n²).
A4. In each iteration i, we scan arr[i:] for the minimum. We then swap it into position i. After the swap, arr[i] contains the minimum of arr[i:], which is ≤ everything after it, and all previous positions already contain the global minimums from earlier passes.
A5. After 3 iterations (i=1,2,3): [1, 2, 7, 9, 5, 3]. Elements [1,2,7,9] are sorted among themselves.
A6. Insertion sort runs in O(n) on nearly sorted data because the inner while loop terminates almost immediately. Selection sort always takes O(n²).
A7. Change if arr[j] < arr[min_idx] to if arr[j] > arr[min_idx].
A8. Either. For n=5, both are O(25) operations — trivial. But insertion sort is slightly better for nearly sorted data.
A9. 7 comparisons (one per element, since the while loop terminates immediately when arr[j] <= key).
A10. If swaps are 10× more expensive: Selection sort wins (n-1 swaps vs O(n²) shifts). If comparisons are 10× more expensive: Both make ~n²/2 comparisons, but insertion sort may do fewer if data is nearly sorted. Join Discord Previous04. Searching Algorithms — Linear Search & Binary SearchNext06. Merge Sort — The O(n log n) Breakthrough
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.