This deck traces binary search step by step on a concrete sorted array, gives the loop invariant that proves it correct with all three obligations spelled out, explains why halving the range gives logarithmic running time, and states the general rule for counting any loop's running time. It targets the off-by-one that never shrinks the range, trusting binary search on unsorted data, mixing up linear with logarithmic growth, and invariants that are stated but not actually preserved.
Subject: CS3000 Algorithms · 137 slides · symbolic lesson
Open the interactive version of this deck · Homework for this lesson
Title
CS3000 Algorithms
How to search a sorted list in logarithmic time — and prove your loop actually works.
Objectives
Binary search is the first algorithm in CS3000 where you have to PROVE correctness, not just run it and hope. By the end you can:
lo, hi, and mid.lo, hi, and mid correctly, and spot the classic off-by-one bug before it bites you.Warm-up
Discussion prompt
Before we open Binary Search & Analyzing Loops: without looking back, what was the main idea of Proof by Contradiction, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
How to start, run, and close a proof by contradiction: recognizing when it fits, negating a claim correctly (including flipping quantifiers), and reasoning to a genuine impossibility. Covers the irrationality of the square root of 2, the infinitude of primes, and a greedy exchange-argument sketch.
Concept
Before any new material: cover the screen.
You have named 8 reusable moves so far. Say as many as you can out loud, by number, from memory.
Do not advance until you have actually tried. Getting four of eight is information; skipping the exercise is not.
Here they are. Score yourself.
Today adds 2 moves to this list. Everything else you will need is already above.
The question that starts every proof from here on is not how do I begin. It is which of these applies here?
Counterexample
Discussion prompt
You have named 8 reusable moves so far. Say as many as you can out loud, by number, from memory.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Do not advance until you have actually tried. Getting four of eight is information; skipping the exercise is not.
Section
Section 1
Concept
You have a sorted list of values and a target value. You want to find the target's position — or learn that it isn't there — using as few comparisons as possible.
binary search — An algorithm that finds a target value in a sorted array by repeatedly checking the middle of the remaining range and discarding the half that cannot possibly contain the target.
Analogy
Discussion prompt
Explain The problem binary search solves by analogy to something with no CS3000 Algorithms in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
You have a sorted list of values and a target value. You want to find the target's position — or learn that it isn't there — using as few comparisons as possible.
Picture it
Animation
Shows: Each probe throws away half — a rendered Manim animation.
Rendered with Manim.
Takeaway: Seven elements, three probes. Doubling the array adds one probe.
Intuition
Checking every position left to right always works, but in the worst case it can take as many comparisons as there are items in the list.
Because the list is sorted, one comparison in the middle tells you which half the target could possibly be in. You throw away the other half for free, without ever looking at any of it.
Explain it
Discussion prompt
Explain Why binary search beats scanning one by one to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Checking every position left to right always works, but in the worst case it can take as many comparisons as there are items in the list.
Picture it
Animation
Shows: Binary searching the answer itself — a rendered Manim animation.
Rendered with Manim.
Takeaway: One of the most reusable tricks in competitive work.
Concept
The whole trick depends on one fact: the array is arranged in increasing order. Comparing the target to the middle value only tells you which half to keep BECAUSE every value to its left is smaller and every value to its right is larger.
Lose that ordering, and the comparison tells you nothing reliable about where the target might be.
Picture it
Animation
Shows: Sorted input is the entire precondition — a rendered Manim animation.
Rendered with Manim.
Takeaway: Sorting first costs n log n, so one search rarely justifies it.
Concept
Binary search also needs one more thing from the data structure: the ability to jump straight to any position — the first one, the middle one, the last one — in the same small amount of time, no matter how far it is from the start.
random access — The ability to read the value at any index directly, without walking through the elements before it. Arrays give you this for free; some other structures, like linked lists, do not.
Concept
Binary search tracks three positions as it runs: lo, hi, and mid.
lo and hi — The lowest and highest index that might still contain the target. Together they describe the current search range.
mid — The index halfway between lo and hi, computed fresh each iteration. It is the one position binary search actually inspects each time.
Definition probe
Sort into buckets
Every line below is part of the definition of random access or of lo and hi — one or the other, never both. Put each where it belongs.
Intuition
Checking the middle splits the remaining range into two pieces of roughly equal size. Whichever piece you keep, you've cut your remaining work in half.
Checking any other position still splits the range into two pieces — but not necessarily equal ones, so you would not be guaranteed to cut the work in half every single time.
Picture it
Figure (svg): A row of ten boxes numbered 0 through 9, with lo marking box 0, mid marking box 4, and hi marking box 9
Discussion prompt
Read the picture before the words. What is this showing, and what is the one thing it is built to make obvious? Commit to an answer, then read on.
Hint: Name the parts, then say what changes between them — and if nothing changes, say what is being held still.
Answer:
Picture the array as a row of boxes. lo and hi are two pointers marking the left and right edges of the boxes still in play; mid sits between them.
Intuition
Picture the array as a row of boxes. lo and hi are two pointers marking the left and right edges of the boxes still in play; mid sits between them.
Figure (svg): A row of ten boxes numbered 0 through 9, with lo marking box 0, mid marking box 4, and hi marking box 9
Every iteration moves lo right or hi left, so the window between them can only shrink — never grow, and never stay the same size forever.
Step zero
Discussion prompt
Trace binary search: find 40 — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Initialize lo and hi to the array's endpoints
Answer:
Worked example
Search this sorted array of 10 values, indices 0 through 9, for the target 40.
| index | value |
|---|---|
| 0 | 3 |
| 1 | 7 |
| 2 | 12 |
| 3 | 18 |
| 4 | 24 |
| 5 | 31 |
| 6 | 40 |
| 7 | 52 |
| 8 | 68 |
| 9 | 77 |
Initialize lo and hi to the array's endpoints
Why: The search range starts as the entire array — no positions have been ruled out yet.
\[ \text{lo}=0,\ \text{hi}=9 \]
Iteration 1: compute mid and compare
Why: mid is the floor of the average of lo and hi. A[mid] is 24, which is less than the target 40.
\[ \text{mid}=\left\lfloor\frac{0+9}{2}\right\rfloor=4,\quad A[4]=24 < 40 \]
Since A[mid] is too small, discard indices lo through mid
Why: Sortedness guarantees every value at or before mid is at most 24, so none of them can equal 40. Move lo just past mid.
\[ \text{lo}=\text{mid}+1=5 \]
Iteration 2: compute mid and compare
Why: A[mid] is now 52, which is greater than the target 40.
\[ \text{mid}=\left\lfloor\frac{5+9}{2}\right\rfloor=7,\quad A[7]=52 > 40 \]
Since A[mid] is too big, discard indices mid through hi
Why: Every value at or after mid is at least 52, so none of them can equal 40. Move hi just before mid.
\[ \text{hi}=\text{mid}-1=6 \]
Iteration 3: compute mid and compare
Why: A[mid] is 31, still less than the target 40.
\[ \text{mid}=\left\lfloor\frac{5+6}{2}\right\rfloor=5,\quad A[5]=31 < 40 \]
\[ \text{lo}=\text{mid}+1=6 \]
Iteration 4: compute mid and compare
Why: Now lo and hi have both landed on index 6, so mid is 6. A[mid] equals the target exactly.
\[ \text{mid}=\left\lfloor\frac{6+6}{2}\right\rfloor=6,\quad A[6]=40=40 \Rightarrow \text{found} \]
Verify the full trace and the answer
Why: Reading straight down the table, the search took 4 iterations and returned index 6, where A[6] is genuinely 40 — the direct lookup confirms the answer.
| iteration | lo | hi | mid | A[mid] | action |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 24 | 24 < 40, so lo = 5 |
| 2 | 5 | 9 | 7 | 52 | 52 > 40, so hi = 6 |
| 3 | 5 | 6 | 5 | 31 | 31 < 40, so lo = 6 |
| 4 | 6 | 6 | 6 | 40 | 40 = 40, found at index 6 |
Picture it
Animation
Shows: Inclusive or exclusive, but pick one — a rendered Manim animation.
Rendered with Manim.
Takeaway: Mixing the two conventions is where off-by-one bugs breed.
Concept
Sometimes the loop runs out of range before finding the target. That happens when lo moves past hi — there is no room left to check.
When that happens, the array simply does not contain the target. The empty range isn't a bug — it's the loop correctly reporting failure.
Ranking
Put in order
Put the moves of Trace binary search: search for 50 (not present) into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. The first three iterations behave identically to the search for 40, since 50 is also greater than 24 and 31, and less than 52.
Worked example
Search the same 10-value array for 50, a value that does not actually appear anywhere in it.
| index | value |
|---|---|
| 0 | 3 |
| 1 | 7 |
| 2 | 12 |
| 3 | 18 |
| 4 | 24 |
| 5 | 31 |
| 6 | 40 |
| 7 | 52 |
| 8 | 68 |
| 9 | 77 |
Run the same iterations as before
Why: The first three iterations behave identically to the search for 40, since 50 is also greater than 24 and 31, and less than 52.
| iteration | lo | hi | mid | A[mid] | action |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 24 | 24 < 50, so lo = 5 |
| 2 | 5 | 9 | 7 | 52 | 52 > 50, so hi = 6 |
| 3 | 5 | 6 | 5 | 31 | 31 < 50, so lo = 6 |
Iteration 4 also fails to match
Why: A[6] is 40, still less than 50, so lo moves one past hi.
\[ \text{mid}=6,\quad A[6]=40 < 50 \Rightarrow \text{lo}=7 \]
lo is now greater than hi, so the loop stops
Why: With lo = 7 and hi = 6, there is no valid index left between them. The range is empty.
\[ \text{lo}=7 > \text{hi}=6 \Rightarrow \text{report: not found} \]
Verify 50 truly is absent
Why: Scanning the full table of values — 3, 7, 12, 18, 24, 31, 40, 52, 68, 77 — confirms 50 never appears. The loop's conclusion matches reality.
Picture it
Animation
Shows: Finding the FIRST match — a rendered Manim animation.
Rendered with Manim.
Takeaway: The plain version returns any match; duplicates need this variant.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student runs binary search directly on this array, which is NOT sorted, searching for the target 3 (which is actually present, at index 3).
It is wrong. Say what breaks — and say it before you turn the page.
Correct: mid is 2 and A[mid] is 9. Since 3 is less than 9, the algorithm assumes the left side must hold it and discards the right — but nothing here is actually sorted, so that assumption is false.
Sort the array first. The same five values, in increasing order:
Why: mid is 2 and A[mid] is 9. Since 3 is less than 9, the algorithm assumes the left side must hold it and discards the right — but nothing here is actually sorted, so that assumption is false.
Trap
A student runs binary search directly on this array, which is NOT sorted, searching for the target 3 (which is actually present, at index 3).
| index | value |
|---|---|
| 0 | 5 |
| 1 | 1 |
| 2 | 9 |
| 3 | 3 |
| 4 | 7 |
Compare against the middle and trust the sortedness assumption
Why: mid is 2 and A[mid] is 9. Since 3 is less than 9, the algorithm assumes the left side must hold it and discards the right — but nothing here is actually sorted, so that assumption is false.
\[ \text{mid}=2,\ A[2]=9,\ 3<9 \Rightarrow \text{hi}=1\ (\text{discards index 3, where the target actually is}) \]
Finish the (invalid) search
Why: The remaining checks at indices 0 and none left both fail, and the loop reports the target missing.
\[ \text{lo}=0,\text{hi}=1,\ \text{mid}=0,\ A[0]=5>3 \Rightarrow \text{hi}=-1 \Rightarrow \text{lo}>\text{hi}: \text{``not found''} \]
The real, wrong output: binary search reports 3 as missing, even though 3 is sitting right there at index 3 of this array.
Sort the array first. The same five values, in increasing order:
| index | value |
|---|---|
| 0 | 1 |
| 1 | 3 |
| 2 | 5 |
| 3 | 7 |
| 4 | 9 |
Now the middle comparison is trustworthy
Why: mid is 2 and A[mid] is 5. Since the array really is increasing, 3 being less than 5 genuinely means 3 (if present) is in the left half.
\[ \text{mid}=2,\ A[2]=5,\ 3<5 \Rightarrow \text{hi}=1 \]
Finish the search correctly
Why: One more comparison lands exactly on the target.
\[ \text{lo}=0,\text{hi}=1,\ \text{mid}=0,\ A[0]=1<3 \Rightarrow \text{lo}=1 \Rightarrow \text{mid}=1,\ A[1]=3=3: \text{found at index 1} \]
Sorting first (or only ever running binary search on data you already know is sorted) is what makes the middle comparison meaningful in the first place.
Pattern
Step through it
Step through Trap: trusting binary search on unsorted data one row at a time. What is driving the change, and what would the row after the last one be?
Section
Section 2
Concept
A loop invariant is a claim about the program's variables that stays true every single time you check it — before the loop starts, after every iteration, and when the loop finally stops.
loop invariant — A statement that is true right before the loop begins, remains true after every iteration, and — combined with the reason the loop stopped — proves the algorithm's final answer is correct.
Picture it
Animation
Shows: The search invariant, stated — a rendered Manim animation.
Rendered with Manim.
Takeaway: Termination plus the invariant gives correctness with no case analysis.
Intuition
Think of the invariant as a promise you make at the very start: no matter how many times this loop spins, this one fact will still be true.
If even a single iteration could break that promise, the whole argument collapses — you would have no way to know what's true once the loop finally stops.
Concept
Here is the exact promise binary search makes, and keeps, on every single iteration:
\[ \text{If the target is anywhere in the array, it lies at an index between } \text{lo} \text{ and } \text{hi}, \text{ inclusive.} \]
Notice what it does NOT say: it never claims the target IS in the array. It only claims WHERE the target would have to be, if it happens to be there at all.
Concept
Three variables and one loop. The only thing the loop ever does is throw away the half of the range that cannot contain the target.
BINARY-SEARCH(A, target)
lo = 1
hi = A.length
while lo <= hi
mid = floor((lo + hi) / 2)
if A[mid] == target
return mid
else if A[mid] < target
lo = mid + 1
else
hi = mid - 1
return NOT-FOUNDLines 9 and 11 both step past mid rather than onto it. That plus one and that minus one are what guarantee the range strictly shrinks, and a range that strictly shrinks is a loop that must end.
Notation
Every line of BINARY-SEARCH says one thing. Read the line, then read what it does — not the other way round.
Annotate
Invariant
If the target is in the array at all, its index is between lo and hi. Check that sentence at every single step — it is true before the loop, every line preserves it, and at the end it is what makes NOT-FOUND trustworthy.
Step through it
At each step, name the half being discarded and say why it cannot hold the target.
Picture it
Animation
Shows: BINARY-SEARCH executing: the current line of pseudocode is highlighted while the data it touches changes.
Rendered with Manim.
Takeaway: Each pass discards half the remaining range, so a range of n collapses to one in about log n steps.
Picture it
Animation
Shows: The invariant that makes it correct — a rendered Manim animation.
Rendered with Manim.
Takeaway: Correctness and termination, from one sentence.
Concept
Stating an invariant is easy. Proving it takes real work — and every proof of a loop invariant breaks into exactly three obligations.
Concept
Every proof of this kind has the same five or six moves in the same order. The order is not something you rediscover each time.
It is on the right. It will stay on the right through the worked examples that follow.
Why this matters: the structure is now handled. You are not spending working memory on what comes next — you are spending all of it on the one hard step.
Steps 2, 3 and 4 are checkboxes. Step 1 is the whole proof — a badly stated invariant makes steps 2 through 5 impossible, and a well stated one makes them nearly automatic.
Picture it
Animation
Shows: When nested loops really do multiply — a rendered Manim animation.
Rendered with Manim.
Takeaway: The distinction that separates n squared from n plus E.
Concept
Initialization asks one question: before the loop runs even once, is the invariant already true?
For binary search, lo and hi start at the two ends of the whole array. So the range the invariant talks about starts out as the entire array.
Estimation
Predict first
Use the same 10-value array from before.
Commit before you compute: what does Verify initialization on our example array come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Verify the invariant holds before the loop runs
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. If the target is anywhere in the array at all, it is certainly within indices 0 through 9, since that is every index there is.
Worked example
Use the same 10-value array from before.
| index | value |
|---|---|
| 0 | 3 |
| 1 | 7 |
| 2 | 12 |
| 3 | 18 |
| 4 | 24 |
| 5 | 31 |
| 6 | 40 |
| 7 | 52 |
| 8 | 68 |
| 9 | 77 |
Set lo and hi to the array's endpoints
Why: lo starts at the first index, hi at the last — the array has 10 elements, indices 0 through 9.
\[ n=10,\quad \text{lo}=0,\quad \text{hi}=n-1=9 \]
Note exactly what range lo through hi covers
Why: With lo at 0 and hi at 9, the range spans every single index the array has.
\[ \text{lo}..\text{hi} = 0..9 = \text{the entire array} \]
Verify the invariant holds before the loop runs
Why: If the target is anywhere in the array at all, it is certainly within indices 0 through 9, since that is every index there is. Initialization holds — trivially, but genuinely.
Picture it
Animation
Shows: Each line of the worked example "Verify initialization on our example array", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: If the target is anywhere in the array at all, it is certainly within indices 0 through 9, since that is every index there is. Initialization holds — trivially, but genuinely.
Concept
Maintenance asks: assuming the invariant was true right before this iteration, is it still true after the iteration updates lo or hi?
Binary search's loop body only ever does one of two things: move lo up past mid, or move hi down past mid. Each one needs its own justification.
Intuition
Because the array is sorted, comparing the target to the middle value tells you something about EVERY position on one side, not just the middle one.
If the middle value is smaller than the target, every value to its left is even smaller still — none of them can be the target. The entire left half, including the middle itself, is safe to throw away.
Intuition
What move should we make next?
The value at the midpoint is smaller than the target, and the array is sorted:
\[ A[\text{mid}] < \text{target}, \qquad A[0] \le A[1] \le \cdots \le A[n-1] \]
Everyone agrees the left half can go. The question is what licenses that.
Name the move, then name the exact sentence that has to stay true for the discard to be legal.
_Look at your toolkit. Say a move number out loud before this slide advances._ A wrong guess is useful. A silent guess is not.
Missing information
Discussion prompt
Pick up the search for 40 at its very first iteration, where the invariant (assumed true) says: if 40 is in the array, it's within lo through hi.
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
mid lands on index 4, and A[4] is 24 — smaller than the target 40.
Worked example
Pick up the search for 40 at its very first iteration, where the invariant (assumed true) says: if 40 is in the array, it's within lo through hi.
\[ \text{Before: lo}=0,\ \text{hi}=9\quad(\text{invariant assumed true here}) \]
Compute mid and compare to the target
Why: mid lands on index 4, and A[4] is 24 — smaller than the target 40.
\[ \text{mid}=4,\quad A[4]=24 < 40 \]
Rule out every index from lo through mid
Why: Sortedness means indices 0 through 4 hold values at most 24, all strictly less than 40. None of them can equal the target, so the target — if present — cannot be there.
\[ A[0..4] \le 24 < 40 \Rightarrow \text{target} \notin A[0..4] \]
Update lo to mid plus one
Why: Having proven indices 0 through 4 are safe to exclude, the new range starts right after mid.
\[ \text{lo}=\text{mid}+1=5 \]
Verify the invariant still holds after the update
Why: Since 0 through 4 are proven not to hold the target, if the target is in the array at all, it must now be within indices 5 through 9 — exactly the new lo through hi. (And indeed the real target 40 sits at index 6, safely inside 5..9.)
Picture it
Animation
Shows: Each line of the worked example "Verify maintenance: A[mid] smaller than the target", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Since 0 through 4 are proven not to hold the target, if the target is in the array at all, it must now be within indices 5 through 9 — exactly the new lo through hi. (And indeed the real target 40 sits at index 6, safely inside 5..9.)
Step zero
Discussion prompt
Verify maintenance: A[mid] bigger than the target — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Compute mid and compare to the target
Answer:
Worked example
Continue to the search's second iteration, where the invariant now says: if 40 is in the array, it's within lo=5 through hi=9.
\[ \text{Before: lo}=5,\ \text{hi}=9\quad(\text{invariant assumed true here}) \]
Compute mid and compare to the target
Why: mid lands on index 7, and A[7] is 52 — bigger than the target 40.
\[ \text{mid}=7,\quad A[7]=52 > 40 \]
Rule out every index from mid through hi
Why: Sortedness means indices 7 through 9 hold values at least 52, all strictly greater than 40. None of them can equal the target.
\[ A[7..9] \ge 52 > 40 \Rightarrow \text{target} \notin A[7..9] \]
Update hi to mid minus one
Why: Having proven indices 7 through 9 are safe to exclude, the new range ends right before mid.
\[ \text{hi}=\text{mid}-1=6 \]
Verify the invariant still holds after the update
Why: Since 7 through 9 are proven not to hold the target, if the target is in the array at all, it must now be within indices 5 through 6 — exactly the new lo through hi. (Target 40 at index 6 is indeed still inside 5..6.)
Picture it
Animation
Shows: Each line of the worked example "Verify maintenance: A[mid] bigger than the target", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Since 7 through 9 are proven not to hold the target, if the target is in the array at all, it must now be within indices 5 through 6 — exactly the new lo through hi. (Target 40 at index 6 is indeed still inside 5..6.)
Concept
The move: #9 (Loop invariant).
State the invariant first, then the code
Why: If the target is anywhere in the array, it is inside the window from lo to hi. That one sentence is what makes discarding half legal — not the sortedness by itself.
\[ \text{target} \in A \;\Longrightarrow\; \text{target} \in A[\text{lo} \ldots \text{hi}] \]
Every branch is now a maintenance check
Why: Each of the three cases has one job: show the sentence is still true afterwards. You are no longer reasoning about the algorithm — you are checking one sentence, three times.
This is why the invariant is stated before the trace and not derived from it. Written first, it tells you what to check. Written afterwards, it is just a summary.
Notation
Annotate
From The move we just made, named — read this one piece at a time. What is each part doing?
On: \( \text{target} \in A \;\Longrightarrow\; \text{target} \in A[\text{lo} \ldots \text{hi}] \)
Concept
Termination asks: when the loop finally stops, does the invariant — combined with WHY it stopped — actually hand you the correct final answer?
Binary search's loop stops for exactly one of two reasons: it finds the target at mid, or the range becomes empty because lo has moved past hi.
Estimation
Predict first
The search for 40 reaches its fourth iteration with lo and hi both at index 6.
Commit before you compute: what does Verify termination: the found case come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Verify the returned index is correct
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. A[6] literally equals 40 by direct lookup in the table — the loop's found-case termination is trivially correct, since it only ever returns after an exact match.
Worked example
The search for 40 reaches its fourth iteration with lo and hi both at index 6.
\[ \text{lo}=6,\ \text{hi}=6 \]
Compute mid and compare
Why: With lo and hi equal, mid is forced to also be 6.
\[ \text{mid}=6,\quad A[6]=40 \]
A[mid] equals the target, so return mid
Why: The loop's stopping reason here is a direct match — there is nothing left to infer from the invariant, because the comparison already handed us certainty.
\[ A[6]=40=\text{target} \Rightarrow \text{return } 6 \]
Verify the returned index is correct
Why: A[6] literally equals 40 by direct lookup in the table — the loop's found-case termination is trivially correct, since it only ever returns after an exact match.
Picture it
Animation
Shows: Each line of the worked example "Verify termination: the found case", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: A[6] literally equals 40 by direct lookup in the table — the loop's found-case termination is trivially correct, since it only ever returns after an exact match.
Ranking
Put in order
Put the moves of Verify termination: the not-found case into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. There is no index that is at once at least 7 and at most 6 — the range described by lo through hi is empty.
Worked example
The search for 50 (which is not in the array) reaches a point where lo has moved past hi.
\[ \text{lo}=7,\ \text{hi}=6 \]
Notice lo is now greater than hi
Why: There is no index that is at once at least 7 and at most 6 — the range described by lo through hi is empty.
\[ \text{lo}=7 > \text{hi}=6 \Rightarrow \text{lo}..\text{hi} = \varnothing \]
Apply the invariant to the now-empty range
Why: The invariant says: IF the target is in the array, it lies between lo and hi. But there is no index between 7 and 6 at all. The only way that statement can still be true is if its condition is false — the target is simply not in the array.
\[ (\text{target}\in A \Rightarrow \text{target}\in\varnothing) \Rightarrow \text{target}\notin A \]
Verify by scanning the array directly
Why: The full array — 3, 7, 12, 18, 24, 31, 40, 52, 68, 77 — never contains 50. The loop's logical conclusion matches a plain, direct check.
Picture it
Animation
Shows: Each line of the worked example "Verify termination: the not-found case", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: The full array — 3, 7, 12, 18, 24, 31, 40, 52, 68, 77 — never contains 50. The loop's logical conclusion matches a plain, direct check.
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student states the correct invariant, but writes the loop body's maintenance step backwards: whenever A[mid] is less than the target, they shrink from the top instead of moving lo up.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: mid is 4 and A[4] is 24, which is less than 40.
The correct rule moves lo up when A[mid] is too small, discarding the half that's proven too small and keeping the half that might still hold the target.
Why: mid is 4 and A[4] is 24, which is less than 40. The buggy rule shrinks from the top instead of the bottom.
Trap
A student states the correct invariant, but writes the loop body's maintenance step backwards: whenever A[mid] is less than the target, they shrink from the top instead of moving lo up.
\[ \text{Buggy rule: if } A[\text{mid}] < \text{target}: \ \text{hi} = \text{mid} - 1 \]
Trace it on the search for 40, iteration 1
Why: mid is 4 and A[4] is 24, which is less than 40. The buggy rule shrinks from the top instead of the bottom.
\[ \text{mid}=4,\ A[4]=24<40 \Rightarrow \text{hi}=\text{mid}-1=3\ (\text{should have been lo}=5) \]
The invariant is now false
Why: The target 40 actually lives at index 6, but the new range is 0 through 3 — index 6 isn't in it. The claim 'if 40 is in the array, it's between lo and hi' just failed, one iteration in.
\[ \text{target index} = 6 \notin \text{lo}..\text{hi} = 0..3 \]
The buggy search runs to its wrong conclusion
Why: Continuing the same backwards rule shrinks the range down to nothing, and the loop reports 40 as missing — even though it is sitting right there at index 6.
\[ \text{lo}=0,\text{hi}=0 \to \text{hi}=-1 \Rightarrow \text{``not found''}\ (\text{wrong: 40 is at index 6}) \]
The correct rule moves lo up when A[mid] is too small, discarding the half that's proven too small and keeping the half that might still hold the target.
\[ \text{Correct rule: if } A[\text{mid}] < \text{target}: \ \text{lo} = \text{mid} + 1 \]
Trace it on the same iteration
Why: Same comparison, correct direction: since indices 0 through 4 are proven too small, lo moves just past mid.
\[ \text{mid}=4,\ A[4]=24<40 \Rightarrow \text{lo}=\text{mid}+1=5 \]
The invariant survives this update
Why: The target's real index, 6, is still inside the new range 5 through 9. Nothing that could contain the target was discarded.
\[ \text{target index}=6 \in \text{lo}..\text{hi} = 5..9 \]
The correct search reaches the right answer
Why: Continuing this correct rule (as traced in an earlier slide) lands on index 6 and finds A[6] equal to 40 — the true, correct result.
Break the constraint
Discussion prompt
The rule this trap just fixed:
Same comparison, correct direction: since indices 0 through 4 are proven too small, lo moves just past mid.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
mid is 4 and A[4] is 24, which is less than 40. The buggy rule shrinks from the top instead of the bottom.
Pattern
1. Write the invariant precisely
Why: Name exactly what stays true, in terms of the loop's own variables. A vague invariant cannot be checked, let alone proven.
2. Prove Initialization
Why: Show the invariant holds before the loop body ever executes, usually by checking the starting values of the variables directly.
3. Prove Maintenance
Why: Assume the invariant is true right before an arbitrary iteration, then show that the specific update the loop body performs keeps it true — check every branch the loop body can take, separately.
4. Prove Termination
Why: Combine the invariant with the exact condition that stopped the loop, and show together they deliver the result you actually claimed.
Real world
Discussion prompt
Outside this lesson: where does Binary Search & Analyzing Loops actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of The three-obligation recipe for proving any loop invariant is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
That deck traces binary search step by step on a concrete sorted array, gives the loop invariant that proves it correct with all three obligations spelled out, explains why halving the range gives logarithmic running time, and states the general rule for counting any loop's running time. It targets the off-by-one that never shrinks the range, trusting binary search on unsorted data, mixing up linear with logarithmic growth, and invariants that are stated but not actually preserved.
Picture it
Animation
Shows: Counting the work a loop does — a rendered Manim animation.
Rendered with Manim.
Takeaway: Triangular, not rectangular — but still quadratic.
Check
Recall the buggy code from the previous trap: whenever A[mid] is less than the target, it moves hi down to just below mid, instead of moving lo up to just past mid.
Check your understanding
Which obligation does this bug violate?
Answer: A
Why: The bug only affects what happens inside the loop body: when A[mid] is less than the target, discarding indices mid through hi can remove the very half that actually contains the target, since a correct search would know the target could only be at mid + 1 through hi in that case. That is a Maintenance failure — the invariant, true before the iteration, is no longer true right after it.
Section
Section 3
Concept
Mid is computed by averaging lo and hi and rounding down. Rounding down matters: it guarantees mid always lands on a real index that's actually inside the current range.
\[ \text{mid} = \left\lfloor \frac{\text{lo}+\text{hi}}{2} \right\rfloor \]
Concept
After checking A[mid], the loop must move a boundary PAST mid, never back onto mid. Landing on mid again leaves the range exactly the same size, and the loop makes no progress at all.
\[ \text{if } A[\text{mid}] < \text{target}:\ \text{lo}=\text{mid}+1 \qquad \text{if } A[\text{mid}] > \text{target}:\ \text{hi}=\text{mid}-1 \]
Anomaly
Predict first
A student writes this, and it looks reasonable:
A student writes: whenever A[mid] is less than the target, set lo = mid — forgetting the plus one.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: mid is 5, and A[5] is 31, which is less than 40.
The correct rule always moves lo strictly past mid.
Why: mid is 5, and A[5] is 31, which is less than 40. The buggy rule sets lo back to 5 — exactly where it already was.
Trap
A student writes: whenever A[mid] is less than the target, set lo = mid — forgetting the plus one.
\[ \text{Buggy rule: if } A[\text{mid}] < \text{target}:\ \text{lo} = \text{mid}\ (\text{missing } +1) \]
Pick up the search for 40 at lo = 5, hi = 6
Why: mid is 5, and A[5] is 31, which is less than 40. The buggy rule sets lo back to 5 — exactly where it already was.
\[ \text{mid}=\left\lfloor\frac{5+6}{2}\right\rfloor=5,\ A[5]=31<40 \Rightarrow \text{lo}=\text{mid}=5\ (\text{no change}) \]
The very next iteration repeats the exact same state
Why: lo is still 5, hi is still 6, so mid is computed as 5 again, A[5] is still 31, and the buggy rule sets lo to 5 again. Nothing about the range has changed.
\[ \text{lo}=5,\ \text{hi}=6 \to \text{mid}=5 \to \text{lo}=5,\ \text{hi}=6 \to \cdots\ (\text{forever}) \]
The correct rule always moves lo strictly past mid.
\[ \text{Correct rule: if } A[\text{mid}] < \text{target}:\ \text{lo} = \text{mid}+1 \]
Same starting point, correct update
Why: mid is 5, A[5] is 31, still less than 40 — but now lo moves to mid plus one.
\[ \text{mid}=5,\ A[5]=31<40 \Rightarrow \text{lo}=\text{mid}+1=6 \]
The range genuinely shrinks
Why: lo and hi both now sit at 6 — a range of size one — letting the loop reach its found case on the very next iteration, instead of spinning forever.
\[ \text{lo}=6,\ \text{hi}=6 \Rightarrow \text{one step from done} \]
Translation
\( \text{Buggy rule: if } A[\text{mid}] < \text{target}:\ \text{lo} = \text{mid}\ (\text{missing } +1) \)
Draw it
Translate both ways. First write the expression above as a sentence with no symbols in it at all. Then cover it, and write your sentence back as notation. If the two versions disagree, the disagreement is the thing to fix.
Elimination
Eliminate the wrong options
What happens on the very next iteration?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: Here mid equals lo already (both are 4), so setting lo = mid changes nothing at all: lo is still 4. The next iteration recomputes the exact same mid = 4 from the exact same lo = 4, hi = 5 — and if A[mid] is still less than the target, the loop repeats this identical state forever, an infinite loop rather than progress.
Check
Suppose lo = 4 and hi = 5, so mid works out to 4. A[mid] turns out to be less than the target, and the code (buggily) sets lo = mid instead of mid + 1.
Check your understanding
What happens on the very next iteration?
Answer: A
Why: Here mid equals lo already (both are 4), so setting lo = mid changes nothing at all: lo is still 4. The next iteration recomputes the exact same mid = 4 from the exact same lo = 4, hi = 5 — and if A[mid] is still less than the target, the loop repeats this identical state forever, an infinite loop rather than progress.
Concept
An array's valid indices run from 0 up to one less than its length — never up to the length itself. Setting hi to the length would point one step past the last real element.
\[ n = \text{array length} \quad \Rightarrow \quad \text{valid indices: } 0 \text{ through } n-1 \quad \Rightarrow \quad \text{hi}=n-1 \]
Sorting
Sort into buckets
These are the pieces of Binary Search & Analyzing Loops, out of order. Put each one back under the part of the lesson it belongs to.
Missing information
Discussion prompt
Search an array holding just a single value, 9, for the target 9.
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
With only one element, lo and hi both land on the array's only index, 0.
Worked example
Search an array holding just a single value, 9, for the target 9.
| index | value |
|---|---|
| 0 | 9 |
Initialize lo and hi
Why: With only one element, lo and hi both land on the array's only index, 0.
\[ n=1,\quad \text{lo}=0,\quad \text{hi}=n-1=0 \]
Compute mid and compare
Why: With lo equal to hi, mid is forced to be 0, the only index available.
\[ \text{mid}=0,\quad A[0]=9=9 \Rightarrow \text{found at index }0 \]
Verify a different target on the very same array
Why: Searching this same one-element array for 5 instead: A[0] is 9, which is greater than 5, so hi becomes -1, and lo (0) is now greater than hi (-1) — the loop correctly reports 5 as absent, matching the fact that 5 truly isn't in this array.
\[ A[0]=9>5 \Rightarrow \text{hi}=-1 \Rightarrow \text{lo}=0 > \text{hi}=-1: \text{``not found''} \]
Picture it
Animation
Shows: Amortised cost of a doubling array — a rendered Manim animation.
Rendered with Manim.
Takeaway: Expensive rarely, cheap usually — average it over the whole sequence.
Check
Consider the one-element array containing just the value 9, and a search for the target 9.
Check your understanding
How many iterations does binary search run before returning an answer?
Answer: A
Why: With lo = hi = 0, the loop still runs its body once: it computes mid = 0, compares A[0] = 9 to the target 9, finds an exact match, and returns — exactly one iteration, not zero.
Section
Section 4
Concept
To count how long ANY loop takes, multiply two numbers: how much work happens inside one pass through the loop body, and how many times the loop body actually runs.
\[ \text{total time} = (\text{work per iteration}) \times (\text{number of iterations}) \]
Intuition
It's the same arithmetic as timing a chore: if folding one shirt takes a few seconds, and you have a pile of shirts, the total time is seconds-per-shirt times number-of-shirts.
The real work in analyzing a loop is figuring out those two numbers honestly — and the number of iterations is usually the trickier one to pin down.
Intuition
Watch me not know the answer. This is what the first two minutes actually look like.
We want the number of passes binary search makes on an array of n elements.
Try counting passes directly: how many elements get eliminated?
Why: First pass kills about n/2 elements, second kills about n/4, and so on. Add those up and you should get the count.
That sum answers a different question
Why: Those numbers add to roughly n — which is how many elements were eliminated, not how many passes happened. You counted the wrong thing.
Dead end. Not a mistake — a move that was worth trying and did not pay off. This happens in most proofs.
Back up. Count the halvings, not the elements
Why: The window size is what shrinks: n, then n/2, then n/4. Ask how many halvings it takes to reach 1 and you are counting passes directly.
\[ \frac{n}{2^{k}} = 1 \;\Longleftrightarrow\; 2^{k} = n \;\Longleftrightarrow\; k = \log_{2} n \]
Every logarithm in this course arrives this way. Something halves, and you count the halvings.
The expert does not see the whole path in advance. The expert tries something, reads the result, and adjusts. That is the skill.
Concept
Binary search's iteration count comes down to one question: starting from a range of size n, about how many times can you cut that size in half before only one item is left?
\[ n \to \frac{n}{2} \to \frac{n}{4} \to \cdots \to 1 \quad \text{after about } \log_2 n \text{ steps} \]
Step zero
Discussion prompt
Count the halvings for n = 16 — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Halve repeatedly
Answer:
Worked example
Before tying this to binary search's exact iteration count, do the raw arithmetic: how many times can you cut 16 in half before reaching 1?
\[ n=16 \]
Halve repeatedly
Why: Each halving cuts the remaining size roughly in two, rounding down when needed.
\[ 16 \to 8 \to 4 \to 2 \to 1 \]
Count the halvings
Why: That took exactly 4 halvings, which matches log base two of 16 exactly, since 16 is 2 to the 4th power.
\[ \lfloor \log_2 16 \rfloor = 4 \quad (\text{4 halvings shrink 16 down to 1}) \]
Verify what this means for binary search's worst case
Why: After 4 halvings the range has shrunk to size one — but the loop still has to run one more time to actually examine that last remaining element and either match it or come up empty. So a 16-element array takes at most 5 iterations in the worst case, not 4 — confirmed by directly simulating a worst-case search on 16 elements.
\[ 4\ (\text{halvings}) + 1\ (\text{final check}) = 5\ (\text{worst-case iterations}) \]
Picture it
Animation
Shows: Each line of the worked example "Count the halvings for n = 16", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: After 4 halvings the range has shrunk to size one — but the loop still has to run one more time to actually examine that last remaining element and either match it or come up empty. So a 16-element array takes at most 5 iterations in the worst case, not 4 — confirmed by directly simulating a worst-case search on 16 elements.
Intuition
What feels wrong about this?
Two algorithms on a million elements. One does about a million steps. The other does about twenty.
\[ n = 1{,}000{,}000 \qquad \log_{2} n \approx 20 \]
_Plain English only. No notation, no algebra. Just say what bothers you._
The feeling: twenty feels far too small for a million items — it does not feel like it could have even looked at them.
That feeling is the proof. It is not a substitute for the proof — it is the thing the proof writes down.
It did not look at them, and that is the point. The right question is not how many items are there but how many times can this shrink before it is done, and those are wildly different numbers.
Hypothesis
Predict first
Count the halvings for n = 1,000,000 is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.
Correct: Bracket n between two powers of two
Why: 2 to the 19th is 524,288, still under a million; 2 to the 20th is 1,048,576, already over a million.
A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.
Worked example
Now try a much bigger array — one million elements.
\[ n = 1{,}000{,}000 \]
Bracket n between two powers of two
Why: 2 to the 19th is 524,288, still under a million; 2 to the 20th is 1,048,576, already over a million.
\[ 2^{19}=524{,}288 \le 1{,}000{,}000 < 2^{20}=1{,}048{,}576 \]
Read off the raw halving count
Why: This bracketing means 19 halvings (via rounding down at each step) shrink 1,000,000 all the way down to 1.
\[ \lfloor \log_2(1{,}000{,}000) \rfloor = 19 \quad (\text{19 raw halvings}) \]
Verify the binary search worst case
Why: Just as with n = 16, add one more iteration for the loop to actually examine that final size-one range. 19 halvings plus 1 gives 20 — confirmed by directly simulating a worst-case search on one million elements, which indeed takes exactly 20 iterations.
\[ 19 + 1 = 20\ (\text{worst-case iterations for one million elements}) \]
Picture it
Animation
Shows: Each line of the worked example "Count the halvings for n = 1,000,000", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Just as with n = 16, add one more iteration for the loop to actually examine that final size-one range. 19 halvings plus 1 gives 20 — confirmed by directly simulating a worst-case search on one million elements, which indeed takes exactly 20 iterations.
Prediction
Predict first
Taking into account that binary search still needs to examine that final single-element range, what is the worst-case number of iterations for n = 1024?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: 11 — the 10 halvings plus one more iteration to check the last remaining element.
Why: 1024 is exactly 2 to the 10th power, so shrinking a range of size 1024 down to size 1 takes 10 halvings. But the loop still runs once more to actually examine that final size-one range and either confirm a match or come up empty — one more iteration than the halving count. So the worst case is 10 plus 1, which is 11 iterations.
Check
A sorted array has exactly 1024 elements. Since 1024 is exactly 2 to the 10th power, repeatedly halving 1024 down to a range of size 1 takes exactly 10 halvings.
Check your understanding
Taking into account that binary search still needs to examine that final single-element range, what is the worst-case number of iterations for n = 1024?
Answer: A
Why: 1024 is exactly 2 to the 10th power, so shrinking a range of size 1024 down to size 1 takes 10 halvings. But the loop still runs once more to actually examine that final size-one range and either confirm a match or come up empty — one more iteration than the halving count. So the worst case is 10 plus 1, which is 11 iterations.
Concept
Tie the halving-count idea directly to binary search: because each iteration discards about half of whatever range remains, the number of iterations needed is about log base two of n.
\[ \text{number of iterations} \le \lfloor \log_2 n \rfloor + 1 \]
That extra plus-one accounts for the loop's very last check, once the range has shrunk down to a single element. It doesn't change the overall growth rate — logarithmic is logarithmic, with or without one extra step.
Picture it
Animation
Shows: Where binary search goes wrong — a rendered Manim animation.
Rendered with Manim.
Takeaway: A range that fails to shrink is an infinite loop, not a bug in the maths.
Concept
Inside one iteration, binary search does a small, fixed amount of work: compute mid, look up one array value, do one comparison, and update a boundary. None of that grows as the array gets bigger.
constant time — Work that takes the same small amount of time regardless of how large the input is. One array lookup by index is constant time, because the array supports jumping straight to any position.
Concept
Apply the general rule: work per iteration is constant, and the number of iterations is about log base two of n.
\[ \text{total time} = O(1) \times O(\log_2 n) = O(\log_2 n) \]
That earlier plus-one detail, buried inside the exact iteration count, is exactly the kind of small constant Big-O notation is built to absorb — which is why we simply write O(log n).
Fill the middle
Fill in the blanks
From The move behind every logarithm in this course — finish the line. Write what belongs on the right of the equals sign before you look.
c \cdot \log_\Theta(\log n) n = ___
Why: Producing the right-hand side unprompted is the difference between recognising this line and being able to use it. Whenever a quantity is divided by a constant factor each pass, the pass count is a logarithm.
Concept
The move: #10 (Halve and count), then #4 (Replace with the dominant term).
Halve and count
Why: Whenever a quantity is divided by a constant factor each pass, the pass count is a logarithm. Divided by 2 gives log base 2, by 3 gives log base 3 — and the base washes out inside big-O.
Then replace with the dominant term
Why: Constant work per pass times log n passes is c times log n, and move #4 says the constant is not part of the growth rate.
\[ c \cdot \log_{2} n = \Theta(\log n) \]
Contrast this with subtracting one each pass, which gives n passes. Divide gives a log; subtract gives a linear count. That single distinction explains most of the running times in the next four lessons.
Notation
Annotate
From The move behind every logarithm in this course — read this one piece at a time. What is each part doing?
On: \( c \cdot \log_{2} n = \Theta(\log n) \)
Estimation
Predict first
Linear search checks index 0, then 1, then 2, and so on, comparing each value to the target until it finds a match or runs out of array.
Commit before you compute: what does Contrast: linear search's iteration count is linear come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Verify with the 10-element array
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Searching our familiar 10-value array for a missing target with linear search takes up to 10 comparisons — dramatically more than binary search's 4 iterations on the very same array.
Worked example
Linear search checks index 0, then 1, then 2, and so on, comparing each value to the target until it finds a match or runs out of array.
Work per iteration is constant
Why: Each step is just one comparison — the same small amount of work as a binary search iteration.
\[ \text{work per iteration} = O(1) \]
Number of iterations in the worst case is n
Why: If the target is missing (or happens to be the very last element), linear search must check every one of the n positions before it can stop.
\[ \text{worst-case iterations} = n \]
Verify with the 10-element array
Why: Searching our familiar 10-value array for a missing target with linear search takes up to 10 comparisons — dramatically more than binary search's 4 iterations on the very same array. Multiplying constant work by n iterations gives O(n), linear time, not O(log n).
Picture it
Animation
Shows: Each line of the worked example "Contrast: linear search's iteration count is linear", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: Searching our familiar 10-value array for a missing target with linear search takes up to 10 comparisons — dramatically more than binary search's 4 iterations on the very same array. Multiplying constant work by n iterations gives O(n), linear time, not O(log n).
Anomaly
Predict first
A student writes this, and it looks reasonable:
A loop starts with i equal to 1 and doubles i every iteration (i becomes i times 2), stopping once i is at least n = 16. A student reasons: it's one loop with one line inside, and i counts up to 16, so it must take about 16 iterations — linear in n.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The reasoning treats every loop that 'counts up to n' as if it always adds a fixed amount each time.
Trace the loop for real, tracking exactly how i changes on each pass.
Why: The reasoning treats every loop that 'counts up to n' as if it always adds a fixed amount each time. But i here doubles, it doesn't add 1 — and that difference changes everything about how fast it reaches 16.
Trap
A loop starts with i equal to 1 and doubles i every iteration (i becomes i times 2), stopping once i is at least n = 16. A student reasons: it's one loop with one line inside, and i counts up to 16, so it must take about 16 iterations — linear in n.
\[ \text{Wrong claim: iterations} \approx n = 16 \]
This ignores HOW i changes each time
Why: The reasoning treats every loop that 'counts up to n' as if it always adds a fixed amount each time. But i here doubles, it doesn't add 1 — and that difference changes everything about how fast it reaches 16.
Trace the loop for real, tracking exactly how i changes on each pass.
\[ i:\ 1 \to 2 \to 4 \to 8 \to 16\ (\text{stop, since } 16 \text{ is no longer} < 16) \]
Count the actual iterations
Why: The loop body ran exactly 4 times (while i was 1, 2, 4, and 8) before the condition failed at i = 16.
\[ \text{iterations} = 4 = \log_2 16 \]
Generalize correctly
Why: Since i doubles every iteration, the number of iterations needed to reach or exceed n is about log base two of n, not n itself. A doubling (or halving) counter always signals logarithmic running time.
\[ i \text{ doubles each step} \Rightarrow \text{iterations} \approx \log_2 n \quad (\text{not } O(n)) \]
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
Prediction
Predict first
Using the rule 'work per iteration times number of iterations,' how should this loop's running time be classified in terms of n?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: Logarithmic — O(log n), since count shrinks by half each iteration rather than by a fixed amount.
Why: What matters is how count changes: it is halved each iteration, not decreased by a fixed amount. Halving repeatedly reaches 0 in about log base two of n iterations. Work per iteration is constant (one division and one comparison), so total time is O(1) times O(log n), which is O(log n).
Check
A loop starts with count equal to n, and divides count by 2 (rounding down) every iteration, stopping when count reaches 0.
Check your understanding
Using the rule 'work per iteration times number of iterations,' how should this loop's running time be classified in terms of n?
Answer: A
Why: What matters is how count changes: it is halved each iteration, not decreased by a fixed amount. Halving repeatedly reaches 0 in about log base two of n iterations. Work per iteration is constant (one division and one comparison), so total time is O(1) times O(log n), which is O(log n).
Section
Section 5
Concept
Compare two loops that both shrink a range from n down to nothing: one subtracts 1 each iteration, the other halves each iteration. The gap between their iteration counts grows enormous as n gets large.
| n | subtract-1 iterations | halving iterations (worst case) |
|---|---|---|
| 10 | 10 | 4 |
| 1,000,000 | 1,000,000 | 20 |
Comparison
Comparison matrix
From Why halving beats subtracting one, side by side: refill the subtract-1 iterations column from what you know. The rest of the table is as it appeared.
| n | subtract-1 iterations | halving iterations (worst case) |
|---|---|---|
| 10 | 10 | 4 |
| 1,000,000 | 1,000,000 | 20 |
Picture it
Animation
Shows: A halving loop is logarithmic — a rendered Manim animation.
Rendered with Manim.
Takeaway: How the counter changes decides the shape, not how it is written.
Ranking
Put in order
Put the moves of Full end-to-end trace with running time annotated into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. This is the same trace worked through earlier, laid out iteration by iteration.
Worked example
Return to the search for 40 in the familiar 10-value array, this time counting iterations against the predicted bound.
| index | value |
|---|---|
| 0 | 3 |
| 1 | 7 |
| 2 | 12 |
| 3 | 18 |
| 4 | 24 |
| 5 | 31 |
| 6 | 40 |
| 7 | 52 |
| 8 | 68 |
| 9 | 77 |
Recall the full trace
Why: This is the same trace worked through earlier, laid out iteration by iteration.
| iteration | lo | hi | mid | A[mid] | action |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 24 | 24 < 40, so lo = 5 |
| 2 | 5 | 9 | 7 | 52 | 52 > 40, so hi = 6 |
| 3 | 5 | 6 | 5 | 31 | 31 < 40, so lo = 6 |
| 4 | 6 | 6 | 6 | 40 | 40 = 40, found at index 6 |
Compare the iteration count to the predicted bound
Why: For n equal to 10, floor of log base two of 10 is 3, plus 1 gives 4 — exactly matching the 4 iterations actually used.
\[ \lfloor \log_2 10 \rfloor + 1 = 3 + 1 = 4 \]
Verify by counting the rows in the trace table
Why: The table above lists exactly 4 iterations, matching the predicted worst-case bound precisely — the theory and the concrete run agree.
Pattern
Step through it
Step through Full end-to-end trace with running time annotated one row at a time. What is driving the change, and what would the row after the last one be?
Concept
Binary search doesn't strictly require an array of numbers. It only requires two things: a sorted or monotonic structure, and a way to check any single candidate in constant time.
If you can ask 'is this candidate too small, too big, or exactly right?' and the answer only ever flips from 'too small' to 'too big' as you move along, you can binary search over it.
Explain it
Discussion prompt
Explain Recognizing when a problem allows binary search to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Binary search doesn't strictly require an array of numbers. It only requires two things: a sorted or monotonic structure, and a way to check any single candidate in constant time.
Intuition
Think of guessing a person's exact age, where you're only ever told 'higher' or 'lower' after each guess. As long as the true answer flips your yes-or-no verdict exactly once as you scan from low to high, the same halving strategy applies.
This is why binary search shows up far beyond sorted arrays — searching over a whole range of possible answers, not just over a list of stored values.
Picture it
Animation
Shows: Why the cost is logarithmic — a rendered Manim animation.
Rendered with Manim.
Takeaway: A million elements costs twenty probes.
Step zero
Discussion prompt
Apply the pattern to a new array — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Initialize lo and hi
Answer:
Worked example
Search this different, 6-element sorted array for the target 12.
| index | value |
|---|---|
| 0 | 2 |
| 1 | 5 |
| 2 | 9 |
| 3 | 12 |
| 4 | 17 |
| 5 | 23 |
Initialize lo and hi
Why: 6 elements means indices 0 through 5.
\[ \text{lo}=0,\ \text{hi}=5 \]
Iteration 1
Why: mid is 2, and B[2] is 9, less than 12, so lo moves past mid.
\[ \text{mid}=2,\ B[2]=9<12 \Rightarrow \text{lo}=3 \]
Iteration 2
Why: mid is 4, and B[4] is 17, greater than 12, so hi moves before mid.
\[ \text{mid}=4,\ B[4]=17>12 \Rightarrow \text{hi}=3 \]
Iteration 3
Why: lo and hi both land on 3, mid is 3, and B[3] equals 12 exactly.
\[ \text{mid}=3,\ B[3]=12=12 \Rightarrow \text{found at index }3 \]
Verify against the array directly
Why: B[3] is genuinely 12 by direct lookup in the table above, and the search used 3 iterations — matching floor of log base two of 6, which is 2, plus 1, equal to 3.
Picture it
Animation
Shows: Each line of the worked example "Apply the pattern to a new array", appearing one at a time.
The same working the example does, in the order a tutor would write it.
Takeaway: B[3] is genuinely 12 by direct lookup in the table above, and the search used 3 iterations — matching floor of log base two of 6, which is 2, plus 1, equal to 3.
Concept
Every binary search you write should be defended by two separate arguments: the loop invariant proves the answer is correct, and the halving argument proves it is fast.
Skip the invariant, and you cannot be sure the algorithm is right. Skip the halving argument, and you cannot be sure it's actually earning its logarithmic reputation.
Analogy
Discussion prompt
Explain Putting the invariant and the running time together by analogy to something with no CS3000 Algorithms in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Every binary search you write should be defended by two separate arguments: the loop invariant proves the answer is correct, and the halving argument proves it is fast.
Elimination
Eliminate the wrong options
What happens to this version's running time?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: Running time is work per iteration times number of iterations. The per-iteration work is still constant either way, but now each iteration removes only ONE index from the range instead of about half of it. So the number of iterations needed to empty a range of size n becomes about n, not log base two of n — that is linear, not logarithmic.
Check
A classmate writes their own binary search, but instead of computing mid as the true midpoint, they always set mid equal to lo plus one — moving through the range one index at a time. The rest of the comparisons and updates are otherwise correct.
Check your understanding
What happens to this version's running time?
Answer: A
Why: Running time is work per iteration times number of iterations. The per-iteration work is still constant either way, but now each iteration removes only ONE index from the range instead of about half of it. So the number of iterations needed to empty a range of size n becomes about n, not log base two of n — that is linear, not logarithmic.
Concept
Moves added today:
Moves you reused today:
Maintenance is induction wearing work clothes: it held before the pass, so it holds after is peel one off plus substitute the hypothesis, with the pass number playing the role of n.
Full toolkit so far: #1 through #10.
Next session opens with you naming every one of these from memory, before any new material.
Counterexample
Discussion prompt
Maintenance is induction wearing work clothes: it held before the pass, so it holds after is peel one off plus substitute the hypothesis, with the pass number playing the role of n.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Next session opens with you naming every one of these from memory, before any new material.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Meeting Binary Search · The Loop Invariant · Boundary Handling · Counting Loop Running Time · Bringing It Together. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You now have the full toolkit: trace binary search precisely, state and prove its invariant, and count any loop's running time.
lo and hi.| Piece | What to check |
|---|---|
| Initialization | lo and hi start at the array's edges |
| Maintenance | the correct half is discarded, using the sorted order |
| Termination | found returns mid; an empty range means truly absent |
| Running time | constant work per iteration times about log base two n iterations |
Want this taught 1-on-1? Alexander tutors CS3000 Algorithms — $55/session, free consultation.