Room 12 of 14 · about 20 minutes

Insertion sort

Insertion sort shifts rather than swaps, so it is the other sort worth tracing by hand.

Tasks checked0 of 4 XP earned on this path0

What this room checks you can do

Student can produce insertion sort using the shift-and-insert idiom (save current, shift larger elements right with a backward inner loop, drop current into the hole), distinguish the algorithm's outer-loop invariant from selection sort's, and identify the O(n) best case and O(n²) worst case.

Notes

Insertion sort: shift the larger elements right, insert into the hole

Insertion sort sorts in place by walking left-to-right and inserting each element into its correct position among the elements already processed. Where selection sort finds the minimum of the suffix and swaps once per pass, insertion sort takes the next unprocessed element and slides it down through the sorted prefix until it lands in the right place.

public static void insertionSort(final int[] xs) {
    for (int i = 1; i < xs.length; i++) {
        final int current = xs[i];          // save the value to insert
        int j = i - 1;
        while (j >= 0 && xs[j] > current) { // shift larger elements right
            xs[j + 1] = xs[j];
            j--;
        }
        xs[j + 1] = current;                // drop current into the hole
    }
}

Three structural commitments distinguish this from selection sort:

1. The outer loop's i is "the next element to insert." i starts at 1 because xs[0] is trivially a sorted prefix of length 1. At the start of iteration i, the prefix xs[0..i-1] is sorted; at the end, the prefix xs[0..i] is sorted. 2. **The inner loop walks backward through the sorted prefix. Each comparison xs[j] > current checks whether the element at position j should be shifted right to make room for current. The loop continues while the prefix still has larger elements; it stops when j runs off the left edge (j < 0) or when xs[j] is no longer larger than current. 3. The element being inserted is saved in current before the shifting starts.** Without the save, the first shift xs[j+1] = xs[j] would clobber xs[i] (the value to insert): the same destructive-write problem as the swap that skips its temporary variable.

Why no swap

A naive presentation of insertion sort uses repeated swaps: slide current left one position at a time by swapping with its neighbor. That works and is sometimes pedagogically clearer, but it costs three assignments per shift (the temp-swap) versus one (xs[j+1] = xs[j]). For 1 000 000 elements the difference is real. The CSCD 210 lecture style teaches the shift-and-insert form because it is the form java.util.Arrays.sort uses internally (in its small-subarray path).

Best-case vs worst-case

Insertion sort is the rare O(n²) algorithm with a meaningful best case:

  • Best case (already sorted): the inner loop's condition xs[j] > current is false on the very first check, so it makes one comparison and no shifts. Total work: n - 1 comparisons, O(n).
  • Worst case (reverse sorted): every new element has to shift past every prior element. Total work: n(n-1)/2, O(n²).

This is why insertion sort is the right choice for nearly-sorted data, and why hybrid sorts like Timsort fall back to insertion sort for small or partially-sorted subarrays.

In other languages

  • C: identical algorithm; identical shift-and-insert idiom.
  • Python: sorted(xs) uses Timsort which falls back to insertion sort for small subarrays. The pattern is internal.
  • Rust: slice::sort uses pattern-defeating quicksort with insertion-sort fallback.

Tracing insertion sort pass by pass

Insertion sort's trace looks superficially like selection sort's (one row per outer-loop pass, showing the array after each), but the invariant in each row differs. The sorted prefix in insertion sort is the values seen so far, in their sorted order, not "the smallest values in their final positions."

The trace format

For int[] xs = {5, 2, 8, 1, 9, 3}:

| Pass | i | current | Array after the insertion | |------|---:|---:|---------------------------| | (initial) |: |: | {5, 2, 8, 1, 9, 3} | | 1 | 1 | 2 | {2, 5, 8, 1, 9, 3} | | 2 | 2 | 8 | {2, 5, 8, 1, 9, 3} | | 3 | 3 | 1 | {1, 2, 5, 8, 9, 3} | | 4 | 4 | 9 | {1, 2, 5, 8, 9, 3} | | 5 | 5 | 3 | {1, 2, 3, 5, 8, 9} |

Five things to notice:

1. The outer loop starts at i = 1. Slot 0 alone is trivially sorted; pass 1 is the first real insertion. A 6-element array has 5 outer-loop passes (one less than its length). 2. Pass 2 (current = 8) is a no-op for the array. 8 is larger than every element in xs[0..1] (which is {2, 5} after pass 1), so the inner loop terminates immediately and current is written back to its original slot. The array is unchanged. 3. Pass 3 (current = 1) is the most expensive pass. 1 is smaller than every element in xs[0..2] ({2, 5, 8}); the inner loop shifts three elements right before placing current at index 0. 4. **The sorted prefix at the end of pass k is the first k+1 values of the original array, sorted. After pass 3, the prefix is {1, 2, 5, 8}: the four values {5, 2, 8, 1} from the original, sorted. The remaining suffix {9, 3} is still the original. 5. The total number of inner-loop shifts equals the number of inversions in the original array.** This is what makes insertion sort efficient on nearly-sorted inputs.

The trace as design

Two complementary algorithm-design lenses become visible in the comparison:

  • Selection sort answers "where does the smallest unplaced element belong?" then places it. Each pass places one element finally.
  • Insertion sort answers "where does the next-unprocessed element belong in the prefix?" then slots it in. Each pass extends the sorted prefix by one position but does not guarantee the final position of any element until the algorithm completes.

The trace tables make the contrast concrete. CSCD 210 lecture week6-lec4 puts the two traces side-by-side for the same input.

In other languages

  • Python: Timsort's "natural runs" detection is implemented exactly with insertion-sort-style invariants on the small-run path; the trace technique extends.
  • Spreadsheet: the trace fits naturally: each row is one outer pass; each column is i, current, and the array slots.

What this room assumes you already have

Tasks

Do each one, then check the box. Checking a box is you saying you did it. You can uncheck a box if you check it by accident.

  1. trace
    Show the answer

    {1, 2, 3, 5, 8, 9}.

  2. trace
    Show the answer

    i=7, current=6, {1, 1, 2, 3, 4, 5, 6, 9}. The full table is gradable directly.

  3. trace
    Show the answer

    0. Each outer iteration's inner loop terminates after one comparison.

  4. trace
    Show the answer

    10 (1 + 2 + 3 + 4 = n(n-1)/2).

Self check

Type what you think the answer is. Getting it wrong costs nothing and you can try as many times as you want.

Add a count variable that totals the inner-loop shifts. Predict the value for an already-sorted input.

Practice, untimed

Open this whenever you want, before the tasks or after them. Nothing in this section is recorded and nothing here is timed.

  1. Write a complete insertion sort.write
    Show the answer

    outer for (int i = 1; i < xs.length; i++); save current = xs[i]; inner while (j >= 0 && xs[j] > current) shifting xs[j+1] = xs[j]; final xs[j+1] = current.

  2. Identify the bug in for (int i = 0; i < xs.length; i++) (outer loop starting at 0).debug
    Show the answer

    at i=0 the inner loop is skipped (j = -1), so the bug is harmless: but starting at i=1 is the convention because slot 0 alone is trivially sorted.

  3. Given the input {7, 5, 3, 1} (reverse-sorted), report the number of shifts in pass 3.trace
    Show the answer

    3 (shifting 7, 5, and 3 all right by one).

  4. Add a System.out.println(Arrays.toString(xs)) to the outer loop, after the final assignment. Predict what the program prints.write
    Show the answer

    one snapshot per outer pass, matching the trace.

How this room finishes

This room is done when all 4 tasks are checked and the self check is answered.