Context:FIT1008_MOC, FIT2004_MOC · the decrease-and-conquer search · contrast with Linear Search · powers index in SortedArrayListFIT2004 emphasis: the canonical best = worst example — an early return exists, so Ω(1) / O(logN) cannot be collapsed into one Θ; it is the locate half of Output-Sensitive Complexity; and W2 makes it the unit’s termination counter-example ➔ Invariant.
Quick Revision
🎯 Objective: find a target in a sorted array by halving the window ➔ Θ(logn), exponentially faster than linear search.
⚡ Key Constraint: needs order + O(1) random access (no LinkList); and the window must strictly shrink — the obvious lo = mid formulation loops forever.
Preconditions ➔ order (so “too high/low” is meaningful) + O(1) random access to the midpoint.
2. Why Θ(logn) (the Invariant)
Halving ➔ each pass is O(1) and halves the window ⟹ n/2b=1⇒b=log2n passes.
Loop invariant ➔ if the key exists in array[0…N], it exists in array[lo…hi] — the if/else is exactly what preserves it, and it is deliberately the weakest statement that implies the postcondition.
Best = worst, and why ➔ the return mid short-circuits ⟹ best Θ(1) (target is the first midpoint), worst Θ(logn) (target absent, window shrinks to empty). Contrast Linear Search’s recursive form, which has no early exit on a miss.
3. The Termination Bug (why lo = mid hangs)
The naive form ➔ while lo < hi: mid = (lo+hi)//2; if key >= array[mid]: lo = mid else: hi = mid — no early return, because lo is reused as the answer index.
The stall ➔ at lo=5,hi=6: mid=⌊11/2⌋=5, so lo = mid sets lo=5 ⟹ no measure decreases, the guard stays true, the loop spins forever.
The fix ➔ while lo < hi - 1: since hi is exclusive (initialised to len(array)), the search space is allowed to shrink to size 1 and then exit, with lo holding the answer index.
Why it matters beyond the exam ➔ a non-terminating branch is input-dependent, so it survives testing; in FIT2004 assignments the marking harness kills the thread on timeout and the mark is lost.
4. Boundary Search (the range-reporting entry point)
Search for the boundary, not the value ➔ to report everything in (X,Y), binary-search the smallest element >X — X itself need not be in the array.
Then scan ➔ walk forward printing until an element ≥Y ⟹ Θ(logn+W) for W reported items ➔ Output-Sensitive Complexity.
⚙️ Core Implementation
🔹 index via binary search (inclusive high, early return)
SortedArrayList.index
def index(self, item: T) -> int: low, high = 0, len(self) - 1 while low <= high: mid = low + (high - low) // 2 # avoids (low+high) overflow in fixed-width ints if self.array[mid] > item: high = mid - 1 # discard right half -- STRICT shrink elif self.array[mid] == item: return mid # found else: low = mid + 1 # discard left half -- STRICT shrink raise ValueError("item not in list") # low > high => absent
💡 Common Mistake:Use mid = low + (high-low)//2 ➔ (low+high)//2 can overflow in fixed-width ints; and the data must be sorted or discarding a half is unjustified.
🔹 Boundary form (exclusive hi, no early return)
binary_search — the lecture's version, with the terminating guard
def binary_search(array, key): # hi is EXCLUSIVE; we do not exit early because lo IS the answer index lo, hi = 0, len(array) while lo < hi - 1: # NOT `lo < hi` -- that never terminates mid = (lo + hi) // 2 if key >= array[mid]: lo = mid # keep [mid, hi) else: hi = mid # keep [lo, mid) return lo if len(array) > 0 and array[lo] == key else -1
💡 Common Mistake:lo = mid with guard lo < hi ➔ at hi=lo+1 the midpoint islo, so the assignment is a no-op ⟹ infinite loop. mid + 1 or the hi - 1 guard is what restores the strictly-decreasing variant.
⚖️ Core Decision Matrix
Aspect
Complexity
Why
Time — best
O(Comp)
target is the first midpoint
Time — worst/avg
Θ(logn)⋅Comp
window halves each pass
Space (iterative)
O(1)
three indices
Space (recursive)
O(logn)
call stack
When It Flips: vs Linear Search (O(n)) always better; vs Hash Table (O(1) expected but unordered) binary search keeps order for predecessor/successor/range. A balanced Binary Tree is "binary search made dynamic" — O(logn) search and insert/delete, which a sorted array can't.
📊 Exam Execution Trace
Manual Execution Trace
Search 15 in [2,5,8,12,15,23,42,50]:
Step / State
Trigger Op
[lo, hi]
mid / array[mid]
Action
0 (Init)
init
[0, 7]
—
—
1
probe
[0, 7]
3 / 12
12 < 15 → lo = 4
2
probe
[4, 7]
5 / 23
23 > 15 → hi = 4
3
probe
[4, 4]
4 / 15
match → return 4
Applied Exercise
Problem: Derive the Θ(logn) bound.
Derivation Proof / Hand-Calculation Walkthrough:
after b passes: 2bn candidates⇒2bn=1⇒b=log2n⇒Θ(logn)
Final Extracted Output: halving ⟹ log2n passes of O(1) ⟹ Θ(logn) — vs linear search’s O(n).
🧠 Active Recall
Why is binary search Θ(logn), and why is that exponentially better than linear search?
Hint: Halving vs decrementing the candidate set.
Answer
Short answer: Each pass discards half ⟹ n/2b=1 gives b=log2n passes of O(1).
Why:Halve vs decrement ➔ Linear Search eliminates one element per step (O(n)) — ~20 vs 1,000,000 passes for a million elements.
Binary search needs sorted data in an array — why each requirement, and what structure relaxes them while keeping O(logn)?
Hint: Order justifies discarding; array gives the midpoint.
Answer
Short answer:Sorted lets you discard a half; array gives O(1) midpoint access.
Why:Dynamic version ➔ a balanced Binary Tree keeps O(logn) search andO(logn) insert/delete, which a sorted array (O(n) inserts) cannot.
Binary search is Θ(logn); a hash table is O(1) expected — when would you still choose binary search?
Hint: Ordered operations.
Answer
Short answer: When you need predecessor/successor, range queries, or in-order iteration.
"The window shrinks every iteration, so it terminates." Where does that argument fail?
Hint: Shrinks, or strictly shrinks?
Answer
Short answer: With lo = mid and hi=lo+1, the midpoint equals lo, so the window does not shrink and the loop never exits.
Why:A variant must strictly decrease ➔ termination needs hi−lo to drop by at least 1 every iteration; use lo = mid + 1, or guard with while lo < hi - 1 given an exclusivehi ➔ Invariant.