The invariant and four walks
The whole structure rests on one sentence.
The BST invariant
For every node: every key in the left subtree is smaller than its key, and every key in the right subtree is larger.
Not "the left child is smaller" — the whole subtree. Everything else follows from that.
A node holds a key and two pointers:
node:
key the title
item the record
left pointer to the smaller ones
right pointer to the larger ones
In lesson 10 a node had one pointer (next along the chain). Here there are
two — and step 6 shows what that costs in memory.
Lookup
Every comparison discards an entire subtree:
Get(title):
cur ← root
while cur ≠ nil:
c.Hit()
if title < cur.key: cur ← cur.left
if title > cur.key: cur ← cur.right
else: return cur.item, true
return empty, false
This is lesson 6's binary search with the path written into pointers in advance rather than computed from indices. The same idea: one comparison halves the space that is left.
"Halves" — provided the subtrees are the same size. When they are not, the cost climbs, and step 7 shows how far that can go.
Insertion
Insertion is the same search, ending not at a match but at an empty slot:
Put(item):
search for item.Title exactly as Get does
if found: replace the record (an equal key, as in lesson 10)
if you hit nil: put the new node right there
A new node always becomes a leaf. The tree never rearranges itself — which is why insertion order decides the shape, and why step 7 is possible at all.
In-order traversal
Step 1's answer, in three lines:
walk(n):
if n = nil: return
walk(n.left) // first everything smaller
emit n.key // then me
walk(n.right) // then everything larger
It follows straight from the invariant: the left subtree is every smaller key, so emitting it first, then yourself, then every larger key, produces ascending order. O(n), no extra memory, and not one comparison.
Range query
The same walk with two conditions that cut off the branches that cannot help:
Range(lo, hi, n):
if n = nil: return
if lo < n.key: Range(lo, hi, n.left) // answers may be left
if lo ≤ n.key ≤ hi: emit n.key
if n.key < hi: Range(lo, hi, n.right) // answers may be right
Those two ifs are what turned step 1's 1,000 nodes into 11: once n.key is
already past hi, no answer can exist anywhere in the right subtree, so it is
never entered.
A hash table cannot make that decision. It does not know what "larger" means.