Loop invariants and termination
Turn an iterative procedure into a proof with preserved assertions, exit conditions, and a well-founded progress measure.
On this page
The question for today
What you’ll understand
- Separate partial correctness from guaranteed termination.
- Prove a lower-bound search using a half-open interval invariant.
- Find a nonnegative integer variant that decreases on every iteration.
A program can pass a thousand tests while mishandling an empty array or repeating forever on two elements. A correctness proof addresses every permitted input by describing what remains true as the machine changes state. The useful assertion usually expresses unfinished work rather than a completed answer. Pair that assertion with a quantity that cannot decrease indefinitely, and a loop becomes a finite inductive argument instead of a collection of plausible updates.
Specify the boundary before choosing the update
Consider a sorted array of length n and a target t. We want its lower-bound index: the first position whose value is at least t, allowing n when every value is smaller. This specification handles missing values and duplicates without exceptional meanings. Maintain indices l and r with the unresolved positions in the half-open interval [l,r). Positions before l are known to be too small; positions from r onward are known to be large enough. The indices satisfy zero ≤ l ≤ r ≤ n.
Initially l is zero and r is n. Both known regions are empty, so their universal claims hold vacuously. When l is smaller than r, choose the midpoint m by rounding their average downward. If the value there is smaller than t, sortedness certifies that every position through m is too small, so set l to m plus one. Otherwise every position from m onward is large enough, so set r to m. Neither update discards an unresolved candidate without a reason.
An invariant proves an implication at exit
Initialization proves the assertion before the first iteration. Preservation assumes it before an arbitrary iteration and derives it afterward. These steps are ordinary induction on the iteration number. At exit the guard l < r is false, while the invariant still gives l ≤ r, hence l equals r. The unresolved region is empty. Its shared boundary is exactly the first index that can satisfy the target comparison, including the legal answer n.
This establishes partial correctness: if the loop exits normally, the returned answer meets the specification. It does not establish that an exit occurs. The deliberately broken update l = m in the smaller-value branch preserves the classification but can stall when r = l + 1. An invariant that merely says the answer has not been discarded cannot detect that failure. Correctness arguments need both a description of the remaining candidates and an explicit explanation of progress.
Make the mathematical machine match the real one
A termination variant must take values in a well-founded order. Positive real values are insufficient: repeatedly halving one gives an infinite strictly decreasing sequence. Here integer interval lengths make the proof work. Each iteration takes constant time in a random-access comparison model with array entries and indices fitting machine words. The interval roughly halves, giving logarithmically many iterations. This resource bound is a separate theorem from correctness and should identify the operations being counted.
Implementation details can invalidate an otherwise sound proof. In a bounded integer type, adding l and r may overflow even when each index is valid. Computing l plus the integer half of r minus l avoids that sum under suitable nonnegative index bounds. Concurrent mutation can destroy sortedness during the search. An expensive comparison changes the time model. Writing assumptions beside the invariant makes these failure modes visible and tells a reviewer which parts belong to mathematics and which belong to the programming environment.
Work it through
Find a boundary through duplicates
Trace lower-bound search for t = 4 in the sorted array [1,4,4,7,9], using zero-based indices and the half-open interval.
Start with l = 0 and r = 5. The midpoint is 2 and its value is 4, so set r = 2. The new unresolved indices are 0 and 1; index 2 and everything after it are certified large enough.
Now m = 1. Its value is again 4, so set r = 1. Then m = 0 has value 1, so set l = 1. The interval lengths have fallen from 5 to 2 to 1 to 0.
At l = r = 1 the loop exits. Index zero has value smaller than 4, while index one and every later position have values at least 4. The invariant supplies the whole boundary claim, not just one equality test.
The answer is index 1, the first of the repeated 4s. Searching for 10 would instead return 5, a valid boundary index outside the array's element positions.
The living notebook
Explore the model
Interactive lab · modular
A preserved property can coexist with an endless cycle
Pause and reset, then set both controls to modulus 5 and additive step 2. Predict that all states remain valid residues but the orbit returns to zero after five steps; use Step to check. For a fresh comparison, pause and reset, then set modulus 5 and step 0: the state stays zero forever. Residue membership is an invariant in both runs, but neither supplies a strictly decreasing nonnegative variant. This finite cyclic model illustrates the logical distinction; it does not execute binary search.
- Residue r0
- 0
- Orbit period · m/gcd(m,a)
- 12
- Distinct residues visited
- 1 of 12
Addition modulo m starts at zero: rₙ = na mod m. Playback advances two additions per second. The orbit has period m/gcd(m,a), including period one for a = 0. This is not a cryptographic security test or a primality algorithm. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forRemaining inside a valid state set proves neither progress nor termination.
Pause & reason
Check your understanding
Your turn
Try it yourself
For an array of length zero, prove that the lower-bound search returns the specified answer without accessing an element. Then explain why replacing the update l = m + 1 by l = m can fail on the one-element array [2] with target 3.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Evaluate the guard immediately for the empty input. For the other input, calculate m before and after the proposed update.
Compare with the solution
For n = 0, initialization gives l = r = 0. The guard is false, both classified-region assertions are vacuous, and the returned index zero is the required boundary. No midpoint or array read occurs. With [2] and target 3, l = 0, r = 1, and m = 0. The broken smaller-value update leaves l unchanged at zero. The next state repeats exactly, so the loop never exits although it has not discarded any possible boundary. The correct update sets l to one and reduces the variant to zero.
Keep the idea
Take it with you
- A loop invariant connects initialization, each iteration, and the exit specification.
- Total correctness also needs a well-founded progress argument.
- Index conventions, integer overflow, and comparison costs are part of the algorithm's assumptions.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.