Question 1 · easy
In a binary search tree the minimum key is found by:
Answer: option B. Smaller keys always lie to the left.
Guess-the-number, the phone book trick, and why log n barely grows.
How do you find something in a million items with only 20 looks?
Each failed comparison removes the middle item and one side. Without sorted order, the discarded side might contain the target.
def binary_search(a, target):
low, high = 0, len(a) - 1
while low <= high:
mid = (low + high) // 2
if a[mid] == target:
return mid
if a[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1Empty input makes high = −1, so the loop never runs. With duplicates, this version returns a matching index, not necessarily the first. The +1 and −1 prevent checking the same middle forever.
| q queries on initially unsorted n values | Total work model |
|---|---|
| Scan each time | O(qn) |
| Sort once, then binary search | O(n log n + q log n) |
| Build a set once, membership only | O(n + q) average |
For one query, sorting may cost more than scanning. A set is suitable for membership; sorted order also supports ranges. Constants and required outputs decide which model fits.
from bisect import bisect_left, bisect_right
a = [2, 4, 4, 7, 9]
count = bisect_right(a, 7) - bisect_left(a, 4)
print(count) # 3 values in the inclusive range [4, 7]bisect_left finds the first allowed position; bisect_right goes after equal values. Finding a position is logarithmic; inserting into a list can still shift n items.
10 test questions · 10 written questions · 20 total
Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.
Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.
Question 1 · easy
In a binary search tree the minimum key is found by:
Answer: option B. Smaller keys always lie to the left.
Question 2 · easy
An in-order traversal of a BST prints the keys:
Answer: option C. Left subtree, node, right subtree.
Question 3 · easy
Insert distinct keys 1, 2, …, n into a plain, unbalanced BST, for n ≥ 3. Counting edges from root to deepest leaf, its height is:
Answer: option B. Each key becomes the right child of the previous one: a chain.
Question 4 · medium
Using the successor-based deletion method, a BST node with two children is replaced by:
Answer: option C. The successor has at most one child, so it is easy to remove from its old place.
Question 5 · medium
The successor of a BST node that has a right child is:
Answer: option A. The next larger key is the smallest key that is larger.
Question 6 · medium
Which structure gives the best guaranteed worst-case bound on all seven dictionary operations?
Answer: option C. A balanced BST supports each ordered-dictionary operation in O(log n) worst-case time. An ordinary hash table does not maintain key order, and its worst-case lookup can be linear.
Question 7 · medium · course question
A sorted list has 15 items. Binary search checks one middle item per iteration and stops on equality. What is the maximum number of checks needed for a successful search?
Answer: option C. A search with four checks can cover up to 2⁴−1=15 items. Three checks cover at most seven.
Question 8 · medium · course question
Binary search uses inclusive bounds lo and hi. After comparing a middle value smaller than the target in an ascending list, what update is correct?
Answer: option A. The middle value and everything before it are too small. Excluding the checked middle position also guarantees progress.
Question 9 · medium · course question
With inclusive bounds, which condition means a binary search has exhausted all candidates?
Answer: option D. When lo exceeds hi, the candidate interval is empty. With lo == hi there is still one candidate to check.
Question 10 · medium · course question
You will perform one search in an unsorted list. Which statement compares a scan with sorting first correctly?
Answer: option B. For a single query, linear scanning takes O(n), while comparison sorting plus binary search generally costs O(n log n). Repeated queries may justify preprocessing.
Read each question together with its explanation, trace or proof. Numbering continues from the test questions.
Question 11 · easy
In a binary search tree, where is the largest key? The smallest?
largest: start at the root and keep going right until you cannot. Smallest: keep going left.
Question 12 · easy
Insert 50, 30, 70, 20, 40, 60, 80 in that order into an empty BST. Describe the tree and give its height.
50 is the root; 30 is its left child and 70 its right child; 20 and 40 hang under 30; 60 and 80 hang under 70. Three levels, height 2. It is perfectly balanced.
Question 13 · easy
What does an in-order traversal of the tree in exercise 38 print?
Exercise 38 uses the BST obtained by inserting 50, 30, 70, 20, 40, 60, 80 in that order.
20 30 40 50 60 70 80, the keys in sorted order.
Question 14 · easy
Insert 1, 2, 3, 4, 5 in that order into an empty BST. What is its height, and what does a search cost?
each key goes to the right of the previous one, giving a chain of height 4; search costs O(n), no better than a linked list.
Question 15 · medium
In a BST, node x has no right child. Where is its successor?
walk up from x until you arrive at an ancestor from its left child; that ancestor is the successor (the next key in in-order). If you reach the root without ever coming up from a left child, x is the maximum.
Question 16 · medium
When deleting a BST node with two children, which node takes its place, and why is that node guaranteed to have at most one child?
its successor, the minimum of the right subtree (the predecessor works symmetrically). Being a minimum, it cannot have a left child, so it has at most a right child and can be removed with the one-child case.
Question 17 · medium
How can you check in O(n) time whether a given binary tree with keys satisfies the BST property?
For distinct keys and a strict BST ordering rule, traverse in order and check that successive keys are strictly increasing. Visit every node once: O(n) time. If duplicate keys are allowed, specify which subtree may contain them and enforce that policy with lower and upper bounds during traversal; a nondecreasing traversal alone cannot enforce a one-sided duplicate policy.
Question 18 · medium
Binary search on a sorted array costs O(log n). Why does the same idea not work on a sorted linked list?
binary search needs to jump to the middle element in O(1); a linked list can only reach the middle by walking n/2 pointers, which already costs O(n).
Question 19 · medium
What is the height of a perfectly balanced BST holding 1,023 keys?
9. A full tree of height h has 2ʰ⁺¹ − 1 nodes, and 2¹⁰ − 1 = 1,023, so there are 10 levels and 9 edges from root to leaf.
Question 20 · hard
Prove that an in-order traversal of any BST outputs its keys in sorted order.
In simpler words: Explain why left–root–right prints a search tree in order.
Starting hint: Assume each smaller subtree already prints correctly.
induction on the number of nodes. An empty tree prints nothing, which is sorted. For a tree with root r, by the induction hypothesis the left subtree prints its keys sorted and so does the right subtree. Every left key is < r < every right key by the BST property, so “left output, then r, then right output” is sorted.
Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.
Search for 30 in [3, 7, 9, 14, 21, 28, 33, 40]. Record middle values.
14 → 28 → 33; then low = 6, high = 5, so return −1.
What is the worst-case number of middle checks for 1,023 sorted items?
⌊log₂ 1023⌋ + 1 = 10.
In the range example, change the interval to [4, 4]. Predict the answer.
bisect_right(a, 4) − bisect_left(a, 4) = 3 − 1 = 2. Both copies count.
Use the steps value returned by binary_search to fill a
table for n = 1 000, 10 000, 100 000, 1 000 000, 10 000 000 (worst case: search for a
missing value). What is the pattern?
For a missing target larger than every item: 10, 14, 17, 20, 24. Other missing targets can take fewer steps. Each ten-fold increase in data adds roughly three or four steps. That is what "logarithmic" feels like from the inside.
Run binary_search on an unsorted list and find a value it fails
to locate even though it is present. Then explain in two sentences why no error was
raised.
For a list of 1 000 000 unsorted items, compare total time for q searches, q = 1, 10, 100, 1 000, 10 000, under three strategies: (a) linear each time, (b) sort once then binary, (c) build a set once then look up. At what q does each strategy win?
For a single search, linear scanning avoids preparation and is often preferable; the actual winner depends on the input and implementation. The set overtakes very quickly (usually within a handful of queries) because building it is only one pass. Sorting pays off later than the set for pure membership, but it is the only one of the three that also answers range questions.
Given a sorted list of a million timestamps, use bisect to count how many
fall between two given values, in O(log n). Then try to do the same with a set and
describe why it degrades to O(n).
import bisect
count = bisect.bisect_right(data, high) - bisect.bisect_left(data, low)Two O(log n) lookups and a subtraction. A set has no notion of order, so the only option is to examine every element — O(n). This is the reason sorted structures still exist in a world with hash tables.
By hand, trace binary_search([3, 7, 9, 14, 21, 28, 33, 40], 33), writing
low, high and middle at each step. How many steps?
Then trace a search for 10 (absent) the same way.
Finding 33 (indices 0–7): mid 3 → data[3]=14, 33>14, low=4; mid 5 → data[5]=28, 33>28, low=6; mid 6 → data[6]=33, match. 3 steps. Searching for 10: mid 3 → 14, 10<14, high=2; mid 1 → 7, 10>7, low=2; mid 2 → 9, 10>9, low=3; now low 3 > high 2, return −1. 3 steps, then the crossing.
Write your own binary search and run it against all six boundary cases from §12.5: an empty list, a one-item list, a two-item list, the first element, the last element, and a missing value. Fix anything that fails. Which case caught the most bugs for you?
The two-item list and the "missing value just past the end" case are the usual
culprits — they are exactly where a stray < instead of <=,
or a missing −1, shows itself. If all six pass, the loop condition and the
two narrowing steps are consistent.
Binary search on 1 000 000 sorted items needs at most about:
You have unsorted data and need exactly one search. The right choice is:
Binary search on unsorted data:
When is a sorted list with bisect better than a set?
A binary search that writes high = middle instead of high = middle - 1 risks:
low and high never cross and the while loop never ends. Always move past the middle.Why is the loop condition while low <= high rather than <?
<, the last remaining candidate is never checked, so a value sitting there is missed. The trace in §12.4 ends exactly when low passes high.In week 1 you guessed a number between 1 and 100 by always choosing the middle of the remaining range, and it took at most seven guesses. That is binary search, and it is the second most useful algorithm a beginner can learn (the first is "put it in a dictionary").
| Range size | Guessing one by one | Halving |
|---|---|---|
| 100 | up to 100 | 7 |
| 1 000 | up to 1 000 | 10 |
| 1 000 000 | up to 1 000 000 | 20 |
| 1 000 000 000 | up to a billion | 30 |
| the world's population | 8 billion | 33 |
Thirty-three questions to find one person on Earth. That column is why
O(log n) is treated as "practically free": between a thousand items and a
billion, it grows from 10 to 30.
def linear_search(data, target):
"""Return the position of target, or -1. Works on any list."""
for i in range(len(data)):
if data[i] == target:
return i
return -1
Worst case n comparisons: O(n). Its virtues are that it needs no
preparation and works on unsorted data, which is exactly what you usually have.
The rule: look in the middle; if the target is smaller, throw away the right half; if larger, throw away the left half; repeat.
def binary_search(data, target):
low = 0
high = len(data) - 1
steps = 0
while low <= high:
steps += 1
middle = (low + high) // 2
if data[middle] == target:
return middle, steps
elif target < data[middle]:
high = middle - 1 # drop the right half
else:
low = middle + 1 # drop the left half
return -1, stepsdata = list(range(1000000)) # already sorted
print(binary_search(data, 999999)) # (999999, 20)
print(binary_search(data, 0)) # (0, 19)
print(binary_search(data, -5)) # (-1, 19)At most twenty middle inspections in this million-item list. These exact calls use twenty for the far-end hit and nineteen for the near-end hit and the miss. Compare with linear search, which needs up to a million.
while low <= high, and middle ± 1 when narrowing. Get either wrong and you loop forever or miss the last item. Test with a 1-item list, a 2-item list, the first element, the last, and a missing value.
The code is short, but short code hides its behaviour. The cure is to trace it once by
hand, writing down low, high and middle at every
turn. Take this ten-item sorted list (indices along the top) and search for
23:
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| value | 2 | 5 | 8 | 12 | 16 | 23 | 38 | 56 | 72 | 91 |
| step | low | high | middle | data[middle] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 16 | 23 > 16 → search right, low = 5 |
| 2 | 5 | 9 | 7 | 56 | 23 < 56 → search left, high = 6 |
| 3 | 5 | 6 | 5 | 23 | match → return (5, 3) |
Three steps, and each one threw away roughly half of what was left: ten candidates, then five, then two. Now trace a miss — search for 40, which is not in the list:
| step | low | high | middle | data[middle] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 16 | 40 > 16 → low = 5 |
| 2 | 5 | 9 | 7 | 56 | 40 < 56 → high = 6 |
| 3 | 5 | 6 | 5 | 23 | 40 > 23 → low = 6 |
| 4 | 6 | 6 | 6 | 38 | 40 > 38 → low = 7 |
| 5 | 7 | 6 | — | — | low > high → return (−1, 4) |
The loop ends the instant low overtakes high: the window has
closed to nothing, so the value cannot be present. That crossing is the whole reason the
condition is while low <= high and not < — with
<, a one-element window (low == high) is never examined, and a
value sitting there is missed.
Each mismatch at least halves the remaining candidate count. For this inclusive-boundary implementation, the maximum number of middle inspections is floor(log₂ n) + 1 for n > 0, and zero for n = 0. This is 4 at n = 8 or 10 and 20 at n = 1 000 000. Actual hits and misses can use fewer steps; reaching one candidate is not the same as checking it.
Binary search is famous for being easy to describe and hard to get exactly right. Almost every bug is one of three kinds. Here is a version with the classic mistake planted in it — predict what it does before reading on:
def buggy_search(data, target):
low, high = 0, len(data) - 1
while low <= high:
middle = (low + high) // 2
if data[middle] == target:
return middle
elif target < data[middle]:
high = middle # BUG: should be middle - 1
else:
low = middle + 1
return -1
Run it on [10, 20] searching for 5. Step 1: low 0, high 1,
middle 0, data[0] is 10, and 5 < 10, so high = middle = 0. Step
2: low 0, high 0, middle 0, still 5 < 10, so high = 0 again. Nothing has
changed. It loops forever. The fix — high = middle - 1 —
guarantees the window shrinks on every branch, so low and high
must eventually cross.
| Mistake | Symptom | The rule |
|---|---|---|
while low < high | Misses a value sitting in a one-element window | Use <= so low == high is still checked |
high = middle (no −1) | Infinite loop when the window will not shrink | Always move past the middle: middle − 1 / middle + 1 |
| Searching unsorted data | Wrong answer, no error | Sort first, or use a set — precondition, not optional |
The defence is not cleverness, it is a fixed set of tests. Any binary search you write should be tried on: an empty list, a one-item list, a two-item list, the very first element, the very last element, and a value that is absent. Passing these tests is useful evidence, not proof. Justify correctness by showing that all possible answers stay inside the window and that every mismatch strictly shrinks it.
A useful cousin returns not "is it here?" but "where would it go?" — the
insertion point. This is what powers bisect in the next
section, and it never needs the equality branch at all:
def insertion_point(data, target):
"""First index i such that everything left of i is < target."""
low, high = 0, len(data) # note: high starts at len, not len-1
while low < high:
middle = (low + high) // 2
if data[middle] < target:
low = middle + 1
else:
high = middle
return low
data = [10, 20, 30, 40, 50]
print(insertion_point(data, 30)) # 2 — 30 belongs at index 2
print(insertion_point(data, 35)) # 3 — between 30 and 40
print(insertion_point(data, 99)) # 5 — past the end
Notice the deliberately different shape: high starts at len(data)
and the loop is while low < high with high = middle (no −1).
The two idioms are not interchangeable — each is internally consistent, and mixing pieces of
one into the other is how the bugs above are born. Learn one of each and keep them separate.
Binary search costs O(log n) — if the data is already sorted. Sorting costs O(n log n), which is more than a single linear scan. So the arithmetic depends entirely on how many searches you will do.
| Situation | Best choice | Total cost |
|---|---|---|
| One search, unsorted data | Linear search | O(n) — sorting first would cost more |
| Many searches, data stays put | Sort once, then binary search | O(n log n) once + O(log n) each |
| Many searches, you only ask "is it there?" | Build a set | O(n) once + O(1) each |
| Data already sorted for other reasons | Binary search | O(log n) each |
| You need neighbours, ranges, "next larger" | Sorted list + bisect | O(log n) to locate boundaries or a neighbour; returning k range items adds O(k) |
Notice row three: for repeated plain membership, a set offers average O(1) query time, while binary search offers O(log n); measured constants and preparation still matter. Binary search earns its place when you need order — the nearest value, everything between two dates, the next larger measurement. That is a question a hash table cannot answer at all.
The crossover is worth doing on the back of an envelope. Say sorting a list costs about
n log₂ n and each linear scan costs n. Sorting-then-searching for
q queries costs roughly n log₂ n + q log₂ n; scanning q times costs
q n. For n = 1 000 000, log₂ n ≈ 20, so sorting pays for itself once q climbs
past about 20 — after that the scan is throwing away the same million items over and over,
while binary search never looks at more than twenty.
Python ships with a tested implementation. Use it in real work; write your own only to understand it, as you did above.
import bisect
data = [10, 20, 30, 40, 50]
i = bisect.bisect_left(data, 30)
print(i, i < len(data) and data[i] == 30) # 2 True; boundary check also handles empty/missing inputs
print(bisect.bisect_left(data, 35)) # 3 — where 35 would go
bisect.insort(data, 35) # insert and keep sorted
print(data) # [10, 20, 30, 35, 40, 50]
bisect_left is O(log n). insort finds the spot in O(log n) but
still has to shift items, so the insertion itself is O(n) — the week 10 lesson has not
gone away.
When the value is already present, bisect_left returns the index of the
first copy and bisect_right the index just after the last. Their difference,
bisect_right(data, x) - bisect_left(data, x), is exactly how many copies of
x the list holds — an O(log n) count, no scanning. You will use this in
Task 4.
import time, random, bisect
def bench(n, queries=1000):
data = list(range(n))
targets = [random.randrange(n) for _ in range(queries)]
start = time.perf_counter()
for t in targets:
linear_search(data, t)
t_lin = time.perf_counter() - start
start = time.perf_counter()
for t in targets:
bisect.bisect_left(data, t)
t_bin = time.perf_counter() - start
lookup = set(data)
start = time.perf_counter()
for t in targets:
t in lookup
t_set = time.perf_counter() - start
print(f"n={n:>8} linear {t_lin:8.4f}s binary {t_bin:7.5f}s set {t_set:7.5f}s")
for n in [10000, 100000, 1000000]:
bench(n)Three completely different behaviours in one table: linear multiplies by ten each row, binary creeps up by a fifth (that is log n at work), and the set does not move. Everything in this course is visible in those three columns.
From the beginner notes · Lectures 3, 4, 5
Binary search repeatedly discards half the positions that might still contain the answer. Its O(log n) search cost requires reaching the middle position in constant time. A sorted array supports that access; a linked list has to walk through links to reach the middle.
With one candidate, a search may still need a comparison. For a conventional successful binary search on n ≥ 1 array items, the worst comparison count is floor(log₂ n) + 1. “About log n” is useful for growth, but an exact count needs an implementation and a rule for what counts as one comparison.
A balanced search tree offers a different route: follow one link per level, keeping the number of levels proportional to log n. An ordinary unbalanced tree can become a chain. Sorted data, random insertion order and guaranteed balance are not interchangeable conditions.
Engineering use. Choose between an array of calibration points and a linked record chain by examining the actual access operations.
low, high and middle;bisect module instead of writing your own.00:00–01:00: launch, predict–run–explain cycle, questions · 01:00–01:10: break · 01:10–02:00: worked variation, peer instruction, questions · 02:00–02:10: break · 02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.
Ask at any point. Weekly self-checks stay private; optional extensions are not collected.
Need a slower explanation? Open the English + Türkçe reference guide.
AA_Week12.ipynb and complete Tasks 1–6.| Term | Meaning in plain words |
|---|---|
| linear search | Check items one at a time. O(n), no preparation needed. |
| binary search | Halve the sorted range each step. O(log n). |
| precondition | Something that must be true for an algorithm to be correct — here, "the data is sorted". |
| off-by-one | A boundary error — a stray ±1 or the wrong comparison — that misses an item or loops forever. |
| insertion point | The index where a value would go to keep the list sorted; what bisect returns. |
| preprocessing | Work done once up front (sorting, building a set) to make later queries cheap. |
| bisect | Python's built-in binary search, including "where would this go?". |
| range query | "Everything between x and y" — answerable with order, not with hashing. |
Binary search runs on logarithms, so this is the natural week to meet the log rules and a
cluster of Skiena's puzzles that all turn on the same idea: halving, and the number
of times you can do it. Throughout, lg means log base 2.
Give an accessible argument for each identity: (a) loga(xy) = loga x + loga y; (b) loga(xy) = y · loga x; (c) the change-of-base rule loga x = logb x / logb a; and (d) xlogb y = ylogb x.
The one fact behind all four: a logarithm is just "the exponent you need". If
loga x = p, that is only shorthand for x = ap.
Keep swapping between those two ways of writing the same thing and the rules fall out.
(a) loga(xy) = loga x + loga y. Write x = ap and y = aq. When you multiply powers of the same base you add the exponents: xy = ap+q. So the exponent for xy is p + q, i.e. loga(xy) = p + q = loga x + loga y. (This is exactly why old slide rules turned multiplication into addition.)
(b) loga(xy) = y · loga x. With x = ap, raising to the y-th power multiplies the exponent: xy = apy. So the exponent for xy is py = y · loga x.
(c) change of base. Start from x = ap where p = loga x. Take log base b of both sides and use rule (b): logb x = p · logb a. Divide across: p = logb x / logb a. That is the rule.
(d) xlogb y = ylogb x. Two things are equal when their logs are equal. Take log base b of each side (rule b): the left gives logb y · logb x, the right gives logb x · logb y. Same product, so the two sides are equal.
import math
x, a, b = 50, 2, 10
print(math.log(x, a)) # log base 2 of 50
print(math.log(x, b) / math.log(a, b)) # change of base — same value
# identity (d): x**(log_b y) should equal y**(log_b x)
print(3 ** math.log(8, 10)) # 2.6968...
print(8 ** math.log(3, 10)) # 2.6968... — equalAnswer: all four hold, and each is just the exponent rules read through the "log = exponent" lens. Rule (c) is the one you will use most: it says any two bases differ only by the constant factor 1 / logb a. That constant vanishes inside Big-O — which is why the base of the log never matters for complexity, and why we can write O(log n) without ever naming a base.
Show that the ceiling of lg(n+1) equals the floor of lg n, plus one, for every integer n ≥ 1.
Both sides are secretly counting the same thing: how many bits n needs (equivalently, how many times you halve n before reaching 1). The interesting action is all at the powers of two, so tabulate a few values and watch the two columns agree:
| n | lg n | ⌊lg n⌋ + 1 | lg(n+1) | ⌈lg(n+1)⌉ |
|---|---|---|---|---|
| 1 | 0.00 | 1 | 1.00 | 1 |
| 2 | 1.00 | 2 | 1.58 | 2 |
| 3 | 1.58 | 2 | 2.00 | 2 |
| 4 | 2.00 | 3 | 2.32 | 3 |
| 5 | 2.32 | 3 | 2.58 | 3 |
| 6 | 2.58 | 3 | 2.81 | 3 |
| 7 | 2.81 | 3 | 3.00 | 3 |
| 8 | 3.00 | 4 | 3.17 | 4 |
Why it works: for any n with 2k−1 ≤ n < 2k, the floor ⌊lg n⌋ is k−1, so the left-hand side is k. On the same range n+1 sits in (2k−1, 2k], so lg(n+1) is just above k−1 and at most exactly k, and its ceiling is also k. Both sides are k. The only place to be careful is the boundary n = 2k: there lg n is exactly k so the left side is k+1, and n+1 = 2k+1 pushes lg(n+1) just above k so its ceiling is k+1 too — they still agree (look at the n = 2, 4, 8 rows).
Answer: both sides count the number of bits of n, so ⌈lg(n+1)⌉ = ⌊lg n⌋ + 1 for all n ≥ 1, the two ways of writing "how many halvings to reach 1".
Prove that the binary representation of an integer n ≥ 1 has exactly ⌊lg n⌋ + 1 bits.
The largest number you can write with b bits is 2b − 1 (all ones):
1 bit reaches 1, 2 bits reach 3, 3 bits reach 7, 8 bits reach 255. So a number n needs
exactly b bits when it fits in b bits but not in b−1:
2b−1 ≤ n < 2b
Take lg of that double inequality (lg is increasing, so it preserves order): b − 1 ≤ lg n < b. The only integer in the half-open interval [lg n, lg n + 1) whose floor sits at the bottom is b − 1 = ⌊lg n⌋, so b = ⌊lg n⌋ + 1. A small table confirms it:
| n | binary | bits | ⌊lg n⌋ + 1 |
|---|---|---|---|
| 1 | 1 | 1 | 0 + 1 = 1 |
| 2 | 10 | 2 | 1 + 1 = 2 |
| 4 | 100 | 3 | 2 + 1 = 3 |
| 255 | 11111111 | 8 | 7 + 1 = 8 |
| 256 | 100000000 | 9 | 8 + 1 = 9 |
Answer: ⌊lg n⌋ + 1 bits. This is the same quantity as problem 2-40, and it is exactly why binary search takes about lg n steps — each comparison learns one bit about where the target sits, and a number in a list of n needs ⌊lg n⌋ + 1 bits to pin down.
Someone shows you a sorting algorithm that runs in O(n log √n). Sorting is known to need Ω(n log n) comparisons in the worst case. How can both be true?
It looks like a contradiction until you simplify the log. Using rule (b) from 2-39, √n = n1/2, so:
log √n = log(n1/2) = ½ · log n
Therefore O(n log √n) = O(n · ½ log n) = O(½ · n log n) = O(n log n) — the one-half is a constant factor, and Big-O throws constant factors away.
Answer: nothing is broken. O(n log √n) is the very same class as O(n log n), just written in a disguise. It does not beat the Ω(n log n) lower bound — it sits right on it. A tidy reminder that "log √n" and "½ log n" are the same thing, and that constant factors never change the class.
You want the lowest floor of a 100-storey building from which a dropped marble breaks. How few drops do you need (a) with a plentiful supply of identical marbles, and (b) with only two marbles?
(a) Plenty of marbles → binary search. A broken marble is no loss, so you can gamble on the middle floor every time: drop from 50. If it breaks, the answer is below 50; if not, above. Each drop halves the range, so you need ⌈lg 100⌉ = 7 drops — the same halving as every other week-12 idea.
(b) Only two marbles → you cannot binary search. If your first marble breaks at floor 50, you have one marble left and no choice but to walk it up one floor at a time from where you last knew it was safe — a linear scan. So a reckless first jump risks up to 49 further drops. The cure is to make the first marble take smaller and smaller steps, so that whichever gap it breaks in, the linear finish with the second marble is short. Drop the first marble at floors 14, then 27, 39, 50, 60, 69, 77, 84, 90, 95, 99, 100 — gaps of 14, 13, 12, … shrinking by one each time.
Why 14? If the first marble breaks on drop number d, you have already spent d drops and the remaining gap to scan is 14 − d floors, for a total of about 14 every time. You want the first gap k big enough that k + (k−1) + … + 1 = k(k+1)/2 covers all 100 floors: the smallest such k is 14, because 14 × 15 / 2 = 105 ≥ 100 while 13 × 14 / 2 = 91 falls short.
Answer: 7 drops with unlimited marbles (binary search); about 14 drops worst case with just two, using the shrinking-gap (triangular- number) strategy. Two marbles cost you the logarithm — you drop from lg n to roughly √n.
You have 10 bags of coins. Nine hold genuine 10-gram coins; one bag holds only 9-gram forgeries. With a digital scale that reads exact grams, identify the bad bag in a single weighing.
The trick is to make each bag contribute a different, recognisable amount to one
pile. Number the bags 1 to 10, and take 1 coin from bag 1, 2 coins from bag 2, 3 from bag
3, …, 10 from bag 10 — 55 coins in all. If every coin were genuine the pile would weigh
55 × 10 = 550 grams. Each forged coin is 1 gram light, and bag k contributed exactly k
coins, so a light bag k makes the pile read 550 − k grams. The number of
grams missing is the guilty bag's number.
weights = [10] * 10 # ten bags, all 10-gram coins to start
weights[6] = 9 # bag number 7 secretly holds 9-gram coins
# take (i+1) coins from bag i (0-indexed) and weigh them all together
total = sum((i + 1) * weights[i] for i in range(10))
print(550 - total) # 7 -> the light bag is number 7
Answer: take k coins from bag k, weigh all 55 together, and read off
550 − (reading) — that difference is the light bag's number. One weighing,
no searching: a positional-counting trick that packs ten answers into a single number.
You have 8 balls that look identical, but one is slightly heavier. Using a balance scale only twice, find the heavy one.
A balance has three outcomes each time — left heavier, right heavier, or level — so two weighings can distinguish up to 3 × 3 = 9 possibilities, and 9 ≥ 8. That count tells you it must be doable; here is how. Split the 8 balls into groups of 3, 3 and 2.
Answer: split 3 / 3 / 2 and you always finish in two weighings. It works because each weighing yields three outcomes, so two weighings carry 3² = 9 distinguishable results — comfortably more than the 8 balls you must tell apart. (The same reasoning shows you could handle up to 9 balls in two weighings, but not 10.)
Binary search only works on sorted data. Week 13 asks what sorting itself costs, and why the sort you invent is so much slower than the built-in one.