Theory

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.