skip to content

Binary insertion sort cuts comparisons to O(n log n) — so why isn't its running time O(n log n)?

level: middleimportance: nice to knowfreq 34%

answer

  1. Two budgets, not one
  2. Search got cheaper — what did not?
  3. Contiguous storage still needs a gap
  4. Sum of shifts is unchanged
  5. Also check the already-ordered case

basics

~20 s

Finding the insertion point is not the expensive part — making room is. Binary search cuts comparisons to O(n log n), but each insertion still shifts elements one slot at a time, so total data movement stays quadratic.

solid answer

~50 s

Insertion sort has two costs: locating the insertion point and opening a gap for the element. Binary search over the already-sorted prefix drops the first cost from `O(n^2)` comparisons to `O(n log n)`. The second cost is untouched — the prefix is stored contiguously, so inserting at position `p` still means moving `i - p` elements right, one at a time, and in the worst case that sums to `n(n-1)/2` moves. Running time is `Θ(n^2)` either way. There are two further catches. The linear best case is lost: on already-ordered input, plain insertion sort does `n - 1` comparisons, while the binary variant does `Θ(n log n)` because binary search cannot exit early. And stability now depends on the search: it must return the position *after* the last equal element, not the first match it happens to land on.

code

pseudocode · 15 lines
pseudocode
for i in 1..n-1
    key = a[i]
    lo = 0
    hi = i
    while lo < hi                  // binary search, floor division
        mid = (lo + hi) / 2
        if a[mid] <= key
            lo = mid + 1
        else
            hi = mid
    j = i - 1
    while j >= lo                  // still one slot at a time
        a[j + 1] = a[j]
        j = j - 1
    a[lo] = key

go deeper

for a junior

Be ready to say that insertion has two costs — finding the spot and making room — and that only the first one gets cheaper. Recognising the claim as a trap is what is being tested here.

for a middle

Derive both sums out loud: logarithmic search over each prefix gives Θ(n log n) comparisons, while worst-case shifting still sums to n(n-1)/2. Mention that the linear best case disappears.

for a senior

Show the reviewing instinct: when a change claims a speedup, ask which cost it reduced and whether that cost dominates the workload. Be able to name the narrow case where the variant pays off, and the stability requirement on the search.

for a principal

Own the standard that performance claims arrive with a named dominant cost and a measurement, not an asymptotic argument about one operation. Be ready to explain why a micro-optimisation that worsens the common case is a poor default.

## Two different costs wearing one name Every insertion into a sorted contiguous region has two parts: 1. **Search** — where does this element belong? 2. **Move** — make a hole there. Plain insertion sort fuses them: the backward scan compares and shifts in the same step, so both costs are `O(i)` on pass `i`. Binary insertion sort separates them, replacing the linear search with a binary search over the sorted prefix and then doing the shifting as a separate loop. The improvement is real but partial, and the trap is to price only the half that got cheaper. ## The arithmetic - **Comparisons.** Binary search over a prefix of size `i` costs `⌈log2(i + 1)⌉` comparisons. Summed over `i = 1..n-1` this is `Θ(n log n)`, and — importantly — it is `Θ(n log n)` on *every* input, best case included. - **Moves.** The element inserted at position `p` of a prefix of size `i` requires `i - p` shifts. Worst case (reversed input) `p = 0` every time, giving `0 + 1 + 2 + ... + (n-1) = n(n-1)/2` moves. - **Total time.** `Θ(n log n) + Θ(n^2) = Θ(n^2)`. So the improvement is a constant-ish win on the comparison side only. It is worth something when comparisons are genuinely expensive relative to moves — comparing long text keys or calling a costly comparison function — and worth nothing when the elements are small and the moves dominate, which is the common case. ## The best case gets worse, not better This is the part candidates almost never mention, and it is the sharpest point in the topic. On already-ordered input: | Variant | Comparisons | Moves | Time | |---|---|---|---| | Plain insertion sort | `n - 1` | 0 | `Θ(n)` | | Binary insertion sort | `Θ(n log n)` | 0 | `Θ(n log n)` | Binary search has no early exit — it always runs its full logarithmic descent, even when the answer is "right where it already is". So the variant that looks strictly better on paper destroys the single property that makes insertion sort worth keeping: adaptivity on nearly-ordered data. If your reason for choosing insertion sort in the first place is that your input arrives almost in order, binary insertion is a downgrade. ## Stability needs the right search Binary search finds *a* valid position, and when the prefix contains elements equal to the key there is a range of valid positions. To preserve stability the search must return the position **after the last equal element** (the upper bound), so the newly inserted element ends up behind equal elements that arrived earlier. A search that returns the first match, or whatever midpoint it happens to hit, will sometimes place the key ahead of an equal predecessor and quietly break stability — a defect no ordering assertion will catch, because the output is still sorted. ``` for i in 1..n-1 key = a[i] lo = 0 hi = i while lo < hi mid = (lo + hi) / 2 if a[mid] <= key lo = mid + 1 else hi = mid ... ``` The `<=` in that test is what pushes the position past equal elements; with `<` it would stop at the first equal one and the sort would no longer be stable. ## The general lesson The reason this question is asked is not that anyone ships binary insertion sort. It is that it catches a habit: pricing an algorithm by the one operation named in the analysis you remember. Comparison counts and move counts are separate budgets, and an optimisation that improves one can leave the total untouched — or, as here, make a different case worse. The same discipline applies whenever you propose a change: name every cost the workload actually pays, and check which one dominates before claiming an improvement. Cutting the cheap half of the work in half is not a speedup. A genuinely different way to escape the quadratic move cost is to change the storage so that insertion does not require shifting a contiguous block — but that is a data-structure change, not an insertion-sort optimisation, and it gives up the in-place, cache-friendly behaviour that is the algorithm's remaining advantage.

  • When is binary insertion sort actually worth using?
    When comparisons are far more expensive than moves and the data is small: long string keys, a comparison that consults several fields, or a comparison callback with real overhead. You trade `Θ(n^2)` comparisons for `Θ(n log n)` while keeping the same move cost. On small elements with cheap comparisons the moves dominate, and the extra index arithmetic makes it a net loss.
  • Why can't the shifting loop be made logarithmic too?
    Because the sorted prefix is a contiguous block, and opening a gap in the middle of a contiguous block requires physically relocating every element after the gap. That is `i - p` moves, with no algorithmic shortcut. Avoiding it means changing the storage layout — a different structure with different tradeoffs — not a smarter loop.
  • Which property of plain insertion sort does the binary variant give up?
    The linear best case. Binary search always performs its full logarithmic descent, so ordered input costs `Θ(n log n)` comparisons instead of `n - 1`. Since near-ordered adaptivity is the main reason to reach for insertion sort at all, the variant undermines its own use case on exactly the data insertion sort was chosen for.

saying these in an interview costs you the question

  • Claims binary search makes insertion sort O(n log n) overall
  • Counts comparisons and ignores element moves
  • Says the best case is still linear
  • Assumes any binary search preserves stability
  • Thinks the shifting loop can be made logarithmic

context