Shell Sort
Insertion sort over elements h apart with a shrinking gap sequence, finishing with a plain insertion sort.
Overview
Shell sort generalizes Insertion Sort: instead of moving elements one slot at a time, it first sorts elements that are h positions apart (an "h-sort"), then repeats with smaller and smaller h, ending with h = 1. Early passes with large gaps move far-away elements long distances cheaply; the final h = 1 pass is a regular insertion sort on an array that is already nearly sorted.
Its running time depends on the gap sequence: Shell's original n/2, n/4, … gives O(n²) worst case; Knuth's (3^k - 1)/2 gives O(n^1.5); Sedgewick's sequences reach O(n^{4/3}); Ciura's empirical sequence 1, 4, 10, 23, 57, 132, 301, 701, … is fastest in practice. It is in-place, not stable, and adaptive. It is used where code size matters (embedded systems, the Linux kernel's early sort(), uClibc qsort) because it is short, needs no recursion or extra memory, and is decent for n in the thousands.
Intuition
A mental model before the formal terms.
Insertion sort is slow because a small element at the far right shuffles left one step at a time. Shell sort lets elements take big strides first: sort every 5th element among themselves, so wildly misplaced items jump across the array in a few moves. Then stride 2, then stride 1 — by then nothing has far to go.
How it works
- Choose a gap sequence ending in
1, e.g. Ciura's[701, 301, 132, 57, 23, 10, 4, 1](extended by×2.25for largen), using only gaps< n. - For each gap
h: fori = h … n - 1, insertion-sorta[i]into the subsequence…, a[i - 2h], a[i - h]by shifting elementshapart while they are larger. - Each gap pass leaves the array
h-sorted; a key property is that an array that ish-sorted staysh-sorted after beingg-sorted. - The final
h = 1pass is plain insertion sort and guarantees correctness.
Why it works
Correctness is trivial: the last pass is insertion sort, which always sorts. The earlier passes are only an optimization.
Efficiency: after h-sorting, the number of inversions left is greatly reduced; insertion sort runs in O(n + I), so later passes are cheap. The h-sortedness preservation property means the work of large gaps is not undone.
The exact bound depends on the gap sequence and is an open problem in general; good sequences avoid gaps sharing common factors so that subsequences interleave.
Recognition
How to tell a problem wants this.
- A sub-quadratic sort with tiny code, no recursion, and
O(1)space is required (embedded, bootloaders, interpreters). - Explicitly asked about gap sequences or how to improve insertion sort.
- Moderate
n(a few thousand) whereO(n^1.3)in practice is fine and library sorts are unavailable.
Interactive visualization
Play, step, change the input. ← → and space work too.
1gap = n // 22while gap > 0:3 for i in gap .. n-1:4 key = a[i]; j = i5 while j >= gap and a[j-gap] > key:6 a[j] = a[j-gap]; j -= gap7 a[j] = key8 gap = gap // 2Pseudocode
1for h in gaps (descending, ending with 1):2 for i from h to n - 1:3 key = a[i]; j = i4 while j >= h and a[j - h] > key:5 a[j] = a[j - h]; j -= h6 a[j] = keyImplementations
1def shell_sort(a: list[int]) -> None:2 n = len(a)31 · Build the Ciura gap sequence, extended by x2.254 gaps = [1, 4, 10, 23, 57, 132, 301, 701]5 while gaps[-1] * 2.25 < n:6 gaps.append(int(gaps[-1] * 2.25))72 · One pass per gap, largest first (skip gaps >= n)8 for h in reversed(gaps):9 if h >= n:10 continue113 · Gapped insertion sort: shift larger elements h apart12 for i in range(h, n):13 key = a[i]14 j = i15 while j >= h and a[j - h] > key:16 a[j] = a[j - h]17 j -= h18 a[j] = key- Sorts in place and returns
None, following thelist.sort()convention (returning the list would suggest a copy was made). gaps[-1] * 2.25mixes int and float;int(...)truncates back — for these magnitudes the float math is exact enough, and Python ints themselves never overflow.reversed(gaps)iterates largest gap first without copying the list.- The while loop shifts elements
hapart; tuple-free single assignments (a[j] = a[j - h]) do one write per shifted element. - The final
h = 1pass is plain insertion sort, so correctness never depends on the earlier gaps.
Pure-Python loops mean sorted() / list.sort() (TimSort in C) win by orders of magnitude at any n — shell sort in Python is for understanding, not speed.
list.sort()andsorted()are stable TimSort implemented in C; there is no scenario where hand-written shell sort beats them in CPython.reversed(gaps)returns a lazy iterator;gaps[::-1]would copy the list.- For NumPy arrays,
np.sort(kind="stable")orkind="quicksort"(introsort) are the vectorized answers; element-wise Python loops over ndarrays are slower still.
- Halving gaps with
h //= 2fromn— the classic O(n²) sequence. - Writing
while j >= 0 and a[j - h] > key— negative indices wrap in Python, so this silently compares against the array tail instead of raising. - Expecting stability from shell sort when replacing a
sorted(key=...)call — equal keys can reorder.
- Gap arithmetic: C++ extends the sequence with integer math (
* 9LL / 4) to avoid float rounding andintoverflow; JS/TS and Python use2.25directly — safe because JS numbers are exact integers below 2^53 and Python ints are unbounded. - Negative index guard:
j >= hprotectsa[j - h]in every language, but the failure mode differs — C++ is undefined behaviour, JS readsundefined(comparison false, silently "works"), Python wraps around to the tail and corrupts the sort. - None of the four standard libraries uses shell sort: C++
std::sortis introsort, Pythonsorted()and V8'sArray.prototype.sortare TimSort — shell sort survives where code size and O(1) space matter (embedded, bootloaders). - In-place mutation style: C++ takes
std::vector<int>&, JS/TS mutate and returnvoidlikeArray.prototype.sort, Python returnsNonelikelist.sort().
Complexity
Depends on gap sequence: Shell n/2^k is O(n²) worst; Knuth (3^k−1)/2 is O(n^1.5); Sedgewick O(n^4/3); Ciura best empirically. In-place, not stable, adaptive.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Small-footprint environments where a recursion-free, allocation-free, sub-quadratic sort is needed.
- Medium-sized arrays (hundreds to low tens of thousands) when a library sort is unavailable.
- Nearly sorted data where it approaches
O(n log n)behaviour.
- Large arrays —
O(n log n)sorts (Merge Sort, Quick Sort, Heap Sort) win clearly. - Stability is required — gap passes move equal elements past each other.
- When a provable tight bound is needed; shell sort's analysis is sequence-dependent and partly open.
Alternatives
Common mistakes
- Using gaps
n/2, n/4, …, 1— powers of two share factors and never mix even and odd positions until the last pass, givingO(n²)worst case. - Forgetting the final
h = 1pass, leaving the array only partially sorted. - Assuming stability.
- Off-by-one in the inner loop bound
j >= h(must not reada[j - h]forj < h).
Interview patterns
- Explain why a gap of 1 must be last and why powers of two are a poor sequence.
- Compare against Insertion Sort on nearly sorted vs random data.
- Rarely asked to implement; usually a discussion question about improving simple sorts.
- Recognizing the approach from an array and a targetIntermediate
- When space complexity mattersIntermediate
- Two pointers or hash map?Intermediate
- Convincing me your algorithm is correctExpert
- Two SumBeginner
Example problems
No linked problems yet.