DSA DSA In Depth
0%
lesson sortinginsertion-sortshellsort

Insertion, Selection & Shellsort: Inversions & Diminishing Increments

7 min read Module 2.2: Sorting Mechanics, Hybrid Sorts & Linear-Time Order Statistics Updated Oct 23, 2026
📄 Cheat Sheet
Shell Sort: Diminishing Increment Insertion & h-Sorting Invariants
RobEdwards (SDSU) Shell Sort: Diminishing Increment Insertion & h-Sorting Invariants Watch on YouTube ↗

Srini Devadas establishes the prefix-sorted loop invariant of Insertion Sort, contrasts comparison cost with element movement cost, and explains why small subarrays favor Insertion Sort on modern hardware.

0:00 / --:--

The Combinatorics of Disorder: Inversions

Given an array $A[0 \dots n-1]$, an inversion is any ordered pair of indices $(i, j)$ such that $i < j$ and $A[i] > A[j]$. The total inversion count $I(A)$ quantifies exactly how far $A$ is from sorted order:

  • A sorted array has $I(A) = 0$ inversions.
  • A strictly reverse-sorted array has every pair inverted: $I(A) = \binom{n}{2} = \frac{n(n-1)}{2}$ inversions.
  • Expected Inversions of a Random Permutation: By linearity of expectation over all $\binom{n}{2}$ pairs, each pair $(i, j)$ has probability exactly $1/2$ of satisfying $A[i] > A[j]$ in a uniformly random permutation of distinct keys. Thus: $$\mathbb{E}[I(A)] = \frac{1}{2}\binom{n}{2} = \frac{n(n-1)}{4} = \Theta(n^2)$$
Theorem (Adjacent-Swap Lower Bound & Insertion Sort Exact Cost)

Swapping two adjacent elements $A[k]$ and $A[k+1]$ changes the relative order of only the pair $(k, k+1)$ while leaving their relative order with all other elements $m \notin \{k, k+1\}$ invariant. Consequently, a single adjacent swap decreases $I(A)$ by at most $1$. Any sorting algorithm that only exchanges adjacent elements (Insertion Sort, Bubble Sort, Cocktail Shaker Sort) must perform at least $I(A)$ swaps—requiring $\ge \frac{n(n-1)}{4} = \Omega(n^2)$ operations in expectation and $\frac{n(n-1)}{2}$ in the worst case.

Furthermore, in Insertion Sort, inserting $A[i]$ into the sorted prefix $A[0 \dots i-1]$ shifts rightwards every element $A[j]$ ($j < i$) satisfying $A[j] > A[i]$. Thus the number of element shifts equals $I(A)$ exactly, and total comparisons equal $I(A) + O(n)$.

Algorithm Best Time Average Time Worst Time Aux Space Stable? Adaptive to $I(A)$?
Selection Sort $\Theta(n^2)$ $\Theta(n^2)$ $\Theta(n^2)$ $O(1)$ No (array swap) No (always $\binom{n}{2}$ cmps, $\le n$ swaps)
Insertion Sort (Guarded) $\Theta(n)$ $\Theta(n^2)$ $\Theta(n^2)$ $O(1)$ Yes ($A[j] > v$) Yes: $\Theta(n + I(A))$
Shellsort (Knuth $3^k - 1$) $\Theta(n \log n)$ $O(n^{1.25})$ empirical $\Theta(n^{3/2})$ $O(1)$ No (strided jumps) Yes (fast on nearly sorted)
Shellsort (Pratt $2^p 3^q$ / Ciura) $\Theta(n \log n)$ $O(n (\log n)^2)$ $\Theta(n (\log n)^2)$ (Pratt) $O(1)$ No Yes

Shellsort: Breaking the $\Omega(n^2)$ Barrier via $h$-Sorting

Donald Shell (1959) observed that moving an element across $h$ positions in a single swap can eliminate up to $2h - 1$ inversions simultaneously. An array is called $h$-sorted if every strided subsequence $A[i], A[i+h], A[i+2h], \dots$ is independently sorted via Insertion Sort with stride $h$. Shellsort executes $h$-Insertion Sort for a diminishing increment sequence $h_t > h_{t-1} > \dots > h_1 = 1$.

  • The $h$-Sort Preservation Theorem: If an array is already $g$-sorted and we subsequently $h$-sort it, the array remains $g$-sorted! This nontrivial combinatorial lemma (proven via Gale-Ryser / Frobenius coin problem duality by Pratt and Knuth) guarantees that work done at coarse strides is never undone by finer strides.
  • Why Powers of 2 Fail ($h = 2^k$): Shell's original sequence $N/2, N/4, \dots, 2, 1$ never compares elements at odd indices with elements at even indices until the very final $h = 1$ pass. Interleaving the largest $n/2$ elements at odd indices and the smallest $n/2$ elements at even indices forces the $h = 1$ pass to do $\Omega(n^2)$ work.
  • Coprime Diminishing Increments:
    • Hibbard ($2^k - 1$): Adjacent increments are coprime; worst-case $\Theta(n^{3/2})$ (Papernov & Stasevich, 1965).
    • Knuth ($\frac{3^k - 1}{2} = 1, 4, 13, 40, 121, \dots$): Easy recurrence $h = 3h + 1$, worst-case $\Theta(n^{3/2})$.
    • Sedgewick ($4^k + 3 \cdot 2^{k-1} + 1$) / Tokuda / Ciura ($1, 4, 10, 23, 57, 132, 301, 701, 1750$): Achieves $O(n^{4/3})$ worst-case and matches $O(n \log n)$ algorithms in wall-clock time up to $n \approx 10^4$.
Shellsort Strided Subsequences (h = 4): Each Color Chain Is Insertion-Sorted Independently A[0]=89 A[1]=42 A[2]=71 A[3]=15 A[4]=23 A[5]=19 A[6]=11 A[7]=08 1. Swapping A[0]=89 with A[4]=23 jumps 4 slots, removing up to 2h - 1 = 7 inversions in a single exchange. 2. By the time h = 1 runs, every element is within 1–2 positions of its final destination: Insertion Sort finishes in O(N) time!
flowchart LR A["Unsorted Array I(A) ≈ N²/4"] --> B["h = 701, 301, 132, 57, 23, 10, 4 Coarse Strided Insertion Passes"] B --> C["Nearly Sorted Array Max displacement ≤ O(1)"] C --> D["Final h = 1 Pass Linear O(N) Insertion Sort"]
Sorting Arena: 3-Way QuickSort, MergeSort & LSD RadixSort
O(N log N) vs O(d·N)

Step through Bentley-McIlroy 3-way partition QuickSort, Bottom-Up MergeSort inversion counting, and non-comparison LSD RadixSort with live comparison and swap counters.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

3-Way QuickSort (Dijkstra Dutch National Flag): partitions subarray into < pivot, == pivot, and > pivot.
🌳 Live 2D Hierarchical Tree Topology — Sorting Arena: 3-Way QuickSort, MergeSort & LSD RadixSort Visited/Active Pivot/Root Resolved
38 [0] 27 [1] 43 [2] 3 [3] 9 [4] 82 [5] 10 [6] 27 [7]
[0]38
[1]27
[2]43
[3]3
[4]9
[5]82
[6]10
[7]27
Comparisons
0
Swaps
0
Locked Keys
0 / 8
Array Size
8
[Step 1] Starting 3-Way QuickSort on [38, 27, 43, 3, 9, 82, 10, 27].
Hardware Reality: Why Insertion Sort Beats $O(N \log N)$ for $N \le 32$

Every production standard library sort—std::sort (libstdc++/libc++), Rust's pdqsort, and Python/V8's TimSort—switches to Insertion Sort (specifically unguarded insertion sort once the minimum element sits in $A[0]$ as a sentinel) when subarray size drops below $16\text{–}32$ elements:

  • Single Cache Line Residency: $16$ 32-bit integers ($64\text{ B}$) or $16$ 64-bit integers ($128\text{ B}$) fit entirely within 1–2 L1d cache lines ($4$-cycle latency) or even CPU registers.
  • Predictable Loop Branches & Zero Recursion Overhead: Function call frames, pivot selection, and 3-way partition branches incur 15-cycle branch misprediction penalties, whereas a tight linear scan with unguarded inner condition (while (v < *--j) without checking j >= first) executes at 1–2 cycles per element.

Production C++20 Implementation: Unguarded Insertion Sort & Ciura Shellsort

#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <utility>
#include <vector>

// Guarded + Unguarded Insertion Sort (used as base-case leaf in Introsort / pdqsort)
template <std::totally_ordered T>
void insertion_sort_unguarded(T* first, T* last) noexcept {
    if (last - first <= 1) return;

    // 1. Move the minimum of the first step, or guard only when touching A[0]
    for (T* i = first + 1; i != last; ++i) {
        T val = std::move(*i);
        // Fast path: if val < *first, it belongs at the very front (shift whole prefix)
        if (val < *first) {
            std::move_backward(first, i, i + 1);
            *first = std::move(val);
        } else {
            // Unguarded inner loop: *first <= val acts as a natural sentinel!
            // Eliminates the j > first bounds check branch on every iteration.
            T* j = i;
            while (val < *(j - 1)) {
                *j = std::move(*(j - 1));
                --j;
            }
            *j = std::move(val);
        }
    }
}

// Shellsort using Marcin Ciura's empirically optimal increment sequence (extended by *2.25)
template <std::totally_ordered T>
void shellsort_ciura(std::vector<T>& a) noexcept {
    const std::size_t n = a.size();
    if (n <= 1) return;

    // Ciura's sequence + geometric 2.25x extension for large N
    std::vector<std::size_t> gaps = {1, 4, 10, 23, 57, 132, 301, 701, 1750};
    while (gaps.back() < n / 2) {
        gaps.push_back(static_cast<std::size_t>(gaps.back() * 2.25));
    }

    for (auto it = gaps.rbegin(); it != gaps.rend(); ++it) {
        const std::size_t h = *it;
        for (std::size_t i = h; i < n; ++i) {
            T temp = std::move(a[i]);
            std::size_t j = i;
            while (j >= h && temp < a[j - h]) {
                a[j] = std::move(a[j - h]);
                j -= h;
            }
            a[j] = std::move(temp);
        }
    }
}
Strict Weak Ordering & Stability in Insertion Sort

Never write the inner comparison of Insertion Sort as while (val <= a[j - 1]). Using <= instead of strict < causes equal keys to leapfrog past one another, destroying sorting stability and performing unnecessary shifts on duplicate keys. Furthermore, if someone passes a comparator that returns true on equal elements in an unguarded loop, the sentinel at *first will fail to stop the pointer j, causing an out-of-bounds memory corruption before first!

Check yourself Question 1 of 4
Concept Check Expected Inversions in a Uniform Random Permutation recall

An array $A[0 \dots n-1]$ contains a uniformly random permutation of $n$ distinct integers.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

What precondition makes omitting the `j > first` bounds check memory-safe, and how can a buggy user comparator cause an out-of-bounds segfault?

Key Takeaways

An inversion is an out-of-order pair (i < j, A[i] > A[j]); a random permutation has n(n-1)/4 inversions on average, and any adjacent-swap sort requires at least I(A) swaps.
Insertion Sort runs in exact Θ(n + I(A)) time—making it O(n) on k-nearly-sorted arrays where each element is within k slots of its sorted destination.
Unguarded Insertion Sort uses A[0] as a sentinel whenever val >= A[0], eliminating the bounds-check branch (j > 0) from the hot inner loop.
Shellsort h-sorts strided subsequences using coprime increments (Knuth 3^k-1, Sedgewick, or Ciura) so single swaps remove up to 2h-1 inversions; an h-sort preserves prior g-sorted order.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue