◈ python mapVol 4 · Ch 56/62
Volume 4 Python, from the metal up · chapter 56

56The binary tree — hierarchy, two children at a time

In Chapter 55 we flattened a grid into one straight line of memory. That was the last easy one. Every structure so far has lived in a row: array, list, stack, queue, even the matrix, all of it laid end to end. A tree breaks the line. It's the shape of anything that contains things that contain things. Think of a folder of folders, a web page of elements, a company of departments. Here we build the plainest one from its scattered bytes upward: the binary tree, one root, up to two children per node, no cycles. The whole way through we keep asking the one question that decides everything downstream. If nowhere in RAM is there a thing shaped like a tree, what is actually holding the hierarchy together? By the end you'll see any hierarchy as nodes joined by two pointers. You'll know why a single number, its height, is the difference between lightning and disaster. And you'll be able to walk one four different ways, each for a different job.

iolinked · chapter 56 — the checkpoints6 steps
$ sections covered in The binary tree — hierarchy, two children at a time
01A tree is scattered boxes joined by pointers
02Height is the whole game
03The pointer tax, paid again at runtime
04Recursion is a tree's native tongue
05Four walks, four jobs
06When to reach for a tree

01A tree is scattered boxes joined by pointers

Let's start with the vocabulary, and let's pin each word down the moment it lands. A node is one box holding a value. The single node at the top is the root. A node's children are the nodes hanging directly below it; that node is their parent; the link between them is an edge. A node with no children is a leaf. Binary means each node has at most two children, and they are named — a left child and a right child; the position carries meaning, so left and right are not interchangeable. One rule makes it a tree and not a tangle: no cycles — there is exactly one path from the root down to any node.

Make it concrete with the little tree we'll carry all chapter: A at the top, with children B and C. B holds D and E, and C holds only a right child, F. So A is the root, and its depth is 0. D, E, and F are leaves, because none of them has a child. B is the parent of D and E, and the edge from A to B is one hop down. Notice that C has a right child but no left one. That empty left slot is real, and the position is part of the data. That is exactly why left and right are not interchangeable.

The cleanest way to say all of that is to say it recursively. A binary tree is either empty, or a node holding a value plus two smaller binary trees (its left and right subtrees). The definition contains itself. Hold that thought, because it is the whole reason trees are easy to walk.

Why does “the definition contains itself” matter so much? It means every question about a tree has the same shape as a question about a smaller tree. Ask for the height of A, and the answer is one more than the taller of B's tree and C's tree. Ask for the node count, and it is one plus the count on the left plus the count on the right. The no-cycles rule is what keeps this recursion from ever looping forever. Because there is exactly one path from the root to any node, walking down can never circle back to a box you have already visited. You always shrink toward the empty tree, so you always stop.

★ YOU ALREADY RUN THIS · the-knockout-bracketthe sheet on the pub wall was a binary tree, and you read it with your finger
World Cup, round of sixteen. The sheet is taped to the wall and you have been reading it all week without once calling it a diagram. Sixteen names down the outer edge, one empty box in the middle waiting for a trophy. Someone asks who knocked Brazil out, so you put a finger on Brazil and slide inward — beat Korea, lost to Croatia, and Croatia lost to Argentina. Three boxes, one path, and no way to skip a box. Then look at that middle box: it cannot be filled in until both semi-finals have finished. The final is the last thing the sheet learns, never the first. Every box on that sheet is fed by exactly two boxes, and you have never once mixed up which was which.
sixteen names on the outer edge, one trophy box in the middleleaves and root — a bracket is this chapter's tree, drawn inward (the sheet's arrows just run the other way)
every box is fed by exactly two, and the top feeder is not the bottom feederat most two children, and left/right is a position, not decoration — swap them and you have changed the data
the final cannot be filled in until both semi-finals have finishedthe post-order rule: a parent's answer waits on both children returning. That is literally 1 + max(height(L), height(R))
16 teams take 4 rounds; 32 teams take 5double the field, add one round — that is log₂n, and why height, not node count, sets what a lookup costs
“who knocked Brazil out?” is a finger-walk, box by boxO(height) — there is no index to jump with; the only way through a tree is to follow the links, one level at a time
pin it: a knockout bracket is a binary tree you have already read with your finger — the trophy is the root, the teams are the leaves, and doubling the field costs exactly one more round.

Now the memory picture, which is this volume's obsession. Nowhere in RAM is there a thing shaped like a tree. On the heap a tree is a scatter of node objects. Each one is a tiny record with three fields: a reference to its value, a reference to its left child, and a reference to its right child (or None where a child is missing). The root is nothing but a name holding one 8-byte reference (Volume 1). Follow the left and right references from box to box, and you are the tree — the shape exists only as pointers.

One number per arrow — that is the whole trick. A reference is just an address, the 8-byte number of where a box sits in RAM. When node A stores its left child, it is not holding B inside itself; it is holding B's address, a single integer like 0x7f3a…. Follow that integer and the machine loads the bytes parked there, and now you are standing on B. The hierarchy you picture as a shape is, underneath, nothing but a handful of these addresses pointing at one another. Lose the addresses and the boxes are just unrelated litter on the heap.

each node = a heap object: [ value · left-ptr · right-ptr ] — addresses are scattered, the shape is only pointers root a name → ref 0x7fa0 node A A L● R● 0x7f3c node B B L● R● 0x7fc8 node C C R● 0x7f14 node D D 0x7f9e node E E 0x7f57 node F F
Fig — the same tree as it truly sits in memory: six node objects at scattered heap addresses. Blue = the value reference; amber = a child pointer; = a null (None) child. The hierarchy is nothing but arrows.
⚠ MOST BEGINNERS THINK…the shape in that diagram is guaranteed
Look again at what the figure actually shows. Six boxes, each with a value cell and two pointer cells, every arrow running downward, the missing children drawn as — and section 01's rule sitting underneath it: no cycles, exactly one path from the root down to any node. Every word of that is true, which is why nobody arrives at the wrong conclusion carelessly. The wrong conclusion is that the rule is kept for you — that a node type with three fields is a tree type, so wiring nodes together is what makes the result a tree, and that an arrow no tree could have would be refused, or at the very least complained about. Build the smallest class that figure describes, then hand it two such arrows, and watch what stops you.
TYPE THIS — 10 SECONDS
class N:
    __slots__ = ('v', 'left', 'right')
    def __init__(self, v, left=None, right=None):
        self.v, self.left, self.right = v, left, right

def walk(n):                          # everything on my left, me, everything right
    return [] if n is None else walk(n.left) + [n.v] + walk(n.right)

shared = N('B')
root = N('A', shared, shared)         # ONE node object, hung in TWO places
print("objects:", len({id(root), id(shared)}), " walk says:", walk(root))
root.left = root                      # now a child that points at its parent
try:
    walk(root)
except RecursionError as e:
    print("the cycle ->", type(e).__name__ + ":", e)
objects: 2  walk says: ['B', 'A', 'B']
the cycle -> RecursionError: maximum recursion depth exceeded
A node is three references and nothing more, so this is a box type and not a tree type — no line of Python anywhere checks that the boxes you wired up form a tree: “tree” is a promise your own code keeps about where the arrows point, and when the promise lapses the arrows keep working perfectly, they simply stop describing a tree. Read the two failures in order, because they break differently and only one of them makes a sound.

Line 1 is the shared node. root.left and root.right hold the same 8-byte address, so there are two objects on the heap and the walk reports three visits: ['B', 'A', 'B']. Nothing raised, because a traversal never asks “have I been here before?” — it only ever asks what is my left, what is my right, and both answers were honest. What you built has a name: a DAG, a directed acyclic graph (Volume 3, chapter 44), and it is a perfectly respectable structure. It is simply not the one your three-line recursions are counting. Measured on a diamond — one 3-node subtree worth 13, hung under both sides of a root holding 100 — a plain recursive node count answered 7 when there are only 4 distinct objects, and a plain recursive sum answered 126 when the real total is 113. Two clean numbers, no warning, each wrong by exactly the shared subtree counted twice.

Line 2 is the cycle. root.left = root gives a child that points at its own ancestor, so the descent never shrinks toward the empty tree, the base case is never reached, and the walk dies with RecursionError. Here is the sharp part: that is the same exception raised by a tree that is merely very deep — the degenerate spine two sections from here — so the message cannot tell you which of the two you are holding. One is a real shape you repair by balancing; the other is an arrow that should never have been written. Only the count separates them, and drop the try and the traceback prints [Previous line repeated 996 more times]: a number that is the recursion limit, not anything about your data.

Three labelled refinements, so none of this reads as a half-truth. First, this is not Python being careless. C, Java, and Rust will all let you assign one pointer into two child slots; a reference is a number, and no language inspects the shape a pile of numbers happens to make. Wherever enforcement exists, somebody wrote it by hand. Second, writing it by hand is three lines, and they are the graph move: carry a seen set and return False the moment id(n) is already in it — id, not ==, because two different nodes may hold equal values. Measured, that checker returned True on the six-node tree above, False on the diamond, and False on the cycle, where the naive count had raised instead of answering. It costs one extra O(n) pass and a set of n entries, which is exactly why nobody runs it on every walk: you enforce treeness where the arrow is written, at insertion, not where it is followed. Third, sharing is not a defect in general — very often it is the entire point. A build system's dependency graph, a Git history, and an expression that reuses a common subexpression all deliberately aim two parents at one child, and they store it in these same three fields. The boxes never change. Only the promise does, which is the thing worth carrying out of this chapter: a tree is a discipline, not a type.
The recursive definition is the whole trick
“A node plus two smaller trees” means a tree is defined in terms of itself. That self-similarity is the human insight this structure rests on — the moment someone realized a box could point at more boxes of its own kind, hierarchy became a data type. Everything hard about trees (walking, measuring, balancing) turns out to be three lines of code, because the data is recursive so the code can be too.

How heavy is one node? The honest answer is to measure, not guess, and the machine will tell us exactly what it costs. We give the class __slots__ so its three fields pack straight into the object, and each node becomes a lean record with no room left for hidden weight.

memory.pypython
import sys

class Node:
    __slots__ = ('value', 'left', 'right')   # pack the 3 refs INTO the object — no per-instance dict
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left  = left
        self.right = right

root = Node('A', Node('B'), Node('C'))       # a 3-node tree, all on the heap
print(sys.getsizeof(root))                    # 56  → 16-byte header + three 8-byte references

Line 4 is the money line: __slots__ tells CPython to store value, left, and right inside the node itself instead of in a separate instance dictionary. Ran on CPython 3.12.7, sys.getsizeof(root) returns 56 bytes: a 16-byte object header plus the three 8-byte references. Drop __slots__ and each node also drags a 296-byte instance __dict__ behind it, for 344 bytes per node, measured. Either way, that is the pointer tax. You pay two extra references per element (16 bytes) purely to encode the shape. Compare a Python list, a contiguous array of references (Volume 1): about 8 bytes per element plus one header. I measured 100,000 slotted nodes at 5,600,000 bytes against the same count in a list at 800,056 bytes. The tree is roughly 7× heavier. A tree is not a light structure. You buy shape with bytes.

Put real numbers on our six-node tree, then. Slotted, it is 6 × 56 = 336 bytes of node objects, plus the one 8-byte name that holds the root. Drop __slots__ and those same six nodes swell to 6 × 344 = 2,064 bytes, more than six times the memory for the identical shape. That gap is pure overhead — not one extra value stored, just the instance __dict__ that __slots__ deletes. On a tree of a million nodes the same multiplier turns roughly 56 MB into 344 MB. The pointer tax you cannot avoid; the dictionary tax is a choice you get to decline.

Put __slots__ on node classes by reflex
A tree can have millions of nodes, and the 288-byte-per-node difference between a slotted node (56 B) and a dict-backed one (344 B) is the difference between 56 MB and 344 MB for a million-node tree. When a class exists to be created by the million, __slots__ is not a micro-optimization — it's the design.
Wait —
if six nodes weigh only ~340 bytes, the bytes aren't the interesting cost. So why can two trees with the identical number of nodes differ by a factor of a hundred in how fast you find something inside them?

The answer isn't in the bytes. It's in one number that measures how tall the scatter of boxes stacks up — the tree's height. →

02Height is the whole game

Two more words, precisely. The depth of a node is how many edges you cross to reach it from the root (the root has depth 0). The height of a tree is the depth of its deepest node, the length of the longest root-to-leaf path. Height is the number that governs cost, and here is exactly why. To reach any node, you start at the root and follow one child pointer per level down. So finding, inserting, or deleting a node costs O(height) pointer hops. It is never more than the height, because that's the longest any path can be. Derive the cost from the layout and it's forced: the work is the walk, and the walk is bounded by the height.

Make that cost concrete on our little tree. To reach leaf F, you start at the root A, follow one child pointer down to C, then one more to F: two hops, exactly its depth. There is no index to jump with and no shortcut to take — the only way down is to follow the pointers, one level at a time. So the deepest node fixes the worst case, and the worst case is the height. That is all O(height) is really saying: the work is a walk, and the walk is as long as the tree is tall.

That turns the whole question into one line: how tall does n nodes have to stack? There are two answers, and they could not sit further apart. The very same node count can give you a shape that flies or a shape that crawls.

Balanced. Fill the tree level by level and each level holds twice the one above: 1 root, then 2, then 4, then 8, …, 2k at level k. So k levels hold up to 2k+1−1 nodes. Invert that — n nodes need only about log₂n levels. This is the same doubling that makes binary search O(log n) in Volume 3, now standing up in three dimensions. Measured: a balanced tree of 1,000 nodes has height 9; of 1,000,000 nodes, height 19. Nineteen hops to reach any node among a million.

Let's check that height-19 claim by hand, because it is the whole promise of the shape. A completely filled tree of height h holds up to 2h+1−1 nodes. Height 9 gives 210−1 = 1,023 nodes, so 1,000 items fit with a level to spare. Height 19 gives 220−1 = 1,048,575 nodes, just past a million. So a million items really do sit within 19 hops of the root. Add one more level, height 20, and you have room for over two million. Each level you add doubles the reach, which is exactly what log₂n means read backwards.

Degenerate. Now insert values already in sorted order into a tree that always sends the larger value right. Every node gets only a right child, so the tree collapses into a single diagonal spine. Its height is n−1. Measured, 1,000 nodes give height 999. That is not a tree in any useful sense. It is the same linked list we built a few chapters back, wearing a tree costume, and every search is O(n).

Watch it happen with five values. Insert 1, then 2: two is larger, so it hangs to the right of one. Insert 3, and it slides past one and two to the right again; 4 and 5 do the same thing. What you are left with is the chain 1→2→3→4→5, a spine of height 4, which is exactly n−1 for five nodes. Now search for 5 and you must step through all four nodes stacked above it. Five sorted values already cost four hops, and a thousand would cost 999 — the linked list wearing its tree costume.

So the same n nodes can cost 9 hops or 999, and nothing about being “a tree” decides which. The order you insert in does. Feed a balanced shape and you get log-depth. Feed already-sorted data into a naive tree and you get the spine. This is the crack that the whole rest of the volume is built to seal. The self-balancing trees ahead notice when one side grows too tall and quietly rotate nodes to keep the height near log n. For now, just hold the tension: height is earned, not guaranteed.

BALANCED — 7 nodes, height 2 DEGENERATE — 7 nodes, height 6 4 2 6 1 3 5 7 reach any node in ≤ 2 hops · O(log n) 1 2 3 4 5 6 reach the last node = 6 hops · a linked list in disguise · O(n)
Fig — identical node count, opposite fates. Balance packs n nodes into ~log₂n levels; sorted insertion unspools them into a spine of height n−1. Height, not node count, is what your search pays.
InteractiveGrow the tree — watch balanced crawl while degenerate races
height = pointer hops to reach the deepest node (same n nodes in both) balanced 4 hops degenerate 15 hops
16
Balanced height is ⌊log₂ n⌋; degenerate height is n−1. The gap between the bars is the gap between O(log n) and O(n).
↺ The thing people get backwards
People hear “tree” and think “fast — O(log n), like binary search.” Backwards. O(log n) is a property of the tree's shape, not of its being a tree. A binary tree gives you the possibility of logarithmic height; it does not guarantee it. Feed one sorted data and it degenerates into an O(n) spine — the exact worst case it was supposed to save you from. The whole engineering problem of the next chapters (self-balancing BSTs, complete heaps) is a single sentence: keep the height near log n no matter what order the data arrives.
A tall tree doesn't just run slow — it can crash
Because the natural way to walk a tree is recursion, and recursion depth follows the tree's height, a degenerate spine can blow the call stack. On CPython 3.12.7 (default recursion limit ≈ 1000), a recursive traversal of a 5,000-deep spine dies with RecursionError: maximum recursion depth exceeded — quoted from a real run. On a balanced million-node tree that same walk recurses only ~19 deep. Height is a correctness concern, not only a speed one.

To reach a node you followed pointers, one per level. But each pointer lands at a random address in RAM — and that randomness carries a cost that Big-O never prints on the label. →

03The pointer tax, paid again at runtime

A Python list is contiguous: element i lives at base + i·8, so scanning it streams whole cache lines the CPU prefetched (Volume 3's memory hierarchy). A tree's nodes were allocated one at a time, whenever you inserted them, so they sit at unrelated addresses. Look back at the memory diagram's scattered 0x7f… values. Walking the tree means chasing a pointer to a random location every single step. The prefetcher cannot guess where you'll jump next, so each hop risks a cache miss and a stall while the machine waits on RAM. The node count is the same O(n) as a list scan, but the real speed is wildly different. The exponent ties, and the constant, set by the memory layout, decides.

a list: consecutive cells — the prefetcher streams them, every access a cache hit a[0] a[1] a[2] a[3] a[4] a[5] one smooth stream a tree: nodes scattered across RAM — every child pointer is a leap to somewhere unpredictable A B C D E each red leap can miss the cache and stall on RAM
Fig — same amount of data, two memory patterns. The array streams; the tree hops. The prefetcher rewards the first and stalls on the second.

Measured, and hedged as machine-dependent: summing a million values by walking a balanced tree in Python took about 312 ms on this machine. Summing the same million from a contiguous list in an equivalent Python loop took about 82 ms, roughly 3.8× faster. (The built-in sum(), running in C over that same packed array, did it in ~6.6 ms.) Part of the tree's penalty is the explicit stack and the attribute lookups. Part is the cache misses of pointer-chasing. Both are the price of scatter, and both are invisible in the O(n) that the two share.

InteractiveThe tax Big-O never prints — count the instructions the CPU forfeits waiting on RAM
Visit every node once — same n, same O(n). Array streams; tree pointer-chases. 343 M instructions the CPU could have retired — forfeited to waiting on RAM array streamed 1.0 ms tree scattered 105 ms The bar gap is a fixed ~100× — a constant factor, not an exponent. It never closes as n grows.
1.0 M
Order-of-magnitude, machine-dependent: a cache hit off the streamed array ≈ 1 ns/node; a cache miss chasing a scattered node ≈ 100 ns of DRAM latency. At ~3.3 GHz that 99 ns stall is ~330 instructions the core sits idle for — per node. Slide n up: the exponent stays O(n) for both, but the 100× constant — the thing Big-O hides — turns a 1-second array scan into a two-minute tree walk.

Myth

“Walking a tree and scanning an array are both O(n) — so they run at the same speed.”

Reality

O(n) counts operations; the machine bills by the cache line. The array streams through prefetched cache; the tree stalls on a fresh miss at every scattered node. Here that gap was ~3.8× in equivalent Python — and it only widens as the data outgrows the cache.
Wait —
if scattering the nodes is what costs us, why not just store the whole tree inside one contiguous array and get an array's cache behavior with a tree's shape?

You can — and that exact trick is the secret of the heap, a couple of chapters ahead. But first: how do you visit every node at all, when the shape exists only as pointers? The structure hands you the tool. →

04Recursion is a tree's native tongue

Volume 3 taught recursion: solve a big problem by solving smaller versions of the same problem, down to a base case that needs no thought. A binary tree is that shape frozen into data, a node whose two children are themselves smaller trees. So the walk writes itself. To do something to a tree, handle this node, then recurse on the left subtree, then recurse on the right subtree. The base case is the empty tree (None), which needs no work at all. There are no loops and no manual bookkeeping. Here, the data's self-similarity becomes the code's.

The smallest possible example is measuring the height itself:

recursion.pypython
def height(node):
    if node is None:          # BASE CASE: an empty tree is -1 edges tall
        return -1
    return 1 + max(height(node.left), height(node.right))   # 1 edge + the taller subtree

Line 2–3 is the floor: an empty tree contributes −1, so that a lone leaf (whose two children are empty) comes out to 0. Line 4 is the shrink: this node adds one edge on top of whichever subtree is taller, and each subtree is measured by the very same function on smaller data. It visits every node exactly once, so it is O(n). Ran on the six-node tree above, height returns 2, matching the diagram.

Walk it once on our tree to feel the shrink. Call height(A). It needs its taller subtree, so it calls height(B) and height(C). height(B) asks its children D and E, both leaves, and each returns 0, so B returns 1. height(C) has an empty left that returns −1 and a leaf F on the right that returns 0, so C also returns 1. Back at A, the taller of 1 and 1 is 1, plus its own edge, gives 2. The call stack was never deeper than the path A→B→D, three nodes. On a spine it would have been as deep as the tree is tall.

A B C D E the path A→B→D is 2 edges deep the call stack while visiting D (Vol 1): height(A) — waiting on left height(B) — waiting on left height(D) — deepest frame frames alive = 3 = height + 1 recursion depth tracks the tree's height — which is why a spine overflows it
Fig — the call stack is the tree's memory of “where am I?”. The deepest stack equals the height — nothing on a balanced tree, everything on a spine.

Notice the three moves inside every tree walk: touch this node, go left, go right. The only real decision is when you touch the node relative to the two descents — and that single choice spawns four different walks, each with a different job. →

05Four walks, four jobs

A traversal is a rule for visiting every node exactly once. Three of the four are depth-first. They plunge to the bottom before backing up, and they differ only by where the “touch the node” step sits among the two recursive descents. Slide that one line and you get three orders:

The fourth is breadth-first. Level-order abandons recursion for a queue (the structure from the queue chapter, and this is exactly BFS from Volume 3): visit the root, enqueue its children, pop the next node, enqueue its children, and repeat. You sweep the tree one level at a time, nearest first. It is the walk you want when “closest to the root” means “most important.”

Trace that queue on our tree and the order falls straight out. Start by visiting A and enqueueing its children, so the queue holds [B, C]. Pop B, visit it, and push D and E, which leaves [C, D, E]. Pop C, visit it, and push its lone child F, which leaves [D, E, F]. From here every pop is a leaf with nothing to add, so you drain D, E, and F in turn. The visit order is A B C D E F, top level first and each level left to right — precisely what a queue's first-in-first-out discipline buys you.

traversals.pypython
from collections import deque

def inorder(node, out):
    if node is None: return         # base case: empty subtree, nothing to do
    inorder(node.left, out)         # LEFT subtree first
    out.append(node.value)          # THEN this node
    inorder(node.right, out)        # THEN the RIGHT subtree
# pre-order: move the append ABOVE the two calls; post-order: move it BELOW both.

def levelorder(root):
    out, q = [], deque([root])      # a queue (Vol 1 deque) — this is BFS
    while q:
        node = q.popleft()          # take the shallowest waiting node
        out.append(node.value)
        if node.left:  q.append(node.left)    # its children go to the BACK
        if node.right: q.append(node.right)
    return out

The top function is the whole depth-first family. Those three lines, with append in the middle slot, are in-order. Lift it above both recursive calls and you have pre-order. Drop it below both and you have post-order. The bottom function swaps the call stack for an explicit queue, pulling from the front and pushing children to the back. That single change turns depth-first into level-by-level. Ran on the six-node tree A(B(D,E), C(·,F)), CPython 3.12.7 gave exactly: in-order D B E A C F, pre-order A B D E C F, post-order D E B F C A, level-order A B C D E F.

THE STDLIB TOOLBELT · the four walks, written as generatorsone line slides three ways — and yield from is what lets a walk compose
def pre(node): # visit BEFORE both descents if node is None: return yield node.value # <- slide THIS line to change the order yield from pre(node.left) yield from pre(node.right) # in-order : the yield sits BETWEEN the two descents # post-order: the yield sits BELOW both of them from collections import deque def level(root): # breadth-first: a queue, not the stack q = deque([root]) while q: n = q.popleft() # the shallowest waiting node yield n.value if n.left: q.append(n.left) # children go to the BACK if n.right: q.append(n.right)
yield fromHand the caller every value the inner generator produces, one at a time, and resume where you left off. It is what makes a recursive walk a single flat stream of values instead of a nest of generator objects. Drop the from and nothing raises — see the first tripwire.
yield, not out.append(…)An out list forces the whole walk before you see anything. A generator is lazy: next(walk) twice on the six-node tree returned D B and left C and F untouched. That is how you write “the first match in a huge tree” without walking it all.
one-shotA generator is an iterator, so it is consumed. Measured above: list(g) gave the six values, and list(g) again gave []. Need it twice, call the function twice.
deque(…) · popleft()Chapter 52's queue, doing the one job a list cannot: remove from the front in O(1). Swap the call stack for this queue and depth-first becomes level-order — same three lines of body, a different memory of “where was I?”
' '.join(pre(tree))join takes any iterable, so a traversal generator plugs straight into it — no intermediate list, no out parameter to thread through every recursive call.
INPUTfrom collections import deque

class Node:
    __slots__ = ('value', 'left', 'right')
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def pre(n):                       # touch the node BEFORE both descents
    if n is None: return
    yield n.value
    yield from pre(n.left)
    yield from pre(n.right)

def ino(n):                       # touch it BETWEEN the two descents
    if n is None: return
    yield from ino(n.left)
    yield n.value
    yield from ino(n.right)

def post(n):                      # touch it AFTER both descents
    if n is None: return
    yield from post(n.left)
    yield from post(n.right)
    yield n.value

def level(root):                  # breadth-first: a queue, not the call stack
    q = deque([root])
    while q:
        n = q.popleft()
        yield n.value
        if n.left:  q.append(n.left)
        if n.right: q.append(n.right)

tree = Node('A', Node('B', Node('D'), Node('E')), Node('C', None, Node('F')))

print("pre-order  :", ' '.join(pre(tree)))
print("in-order   :", ' '.join(ino(tree)))
print("post-order :", ' '.join(post(tree)))
print("level-order:", ' '.join(level(tree)))

walk = ino(tree)                  # a generator is LAZY: it walks only as far as asked
print("first two of in-order:", next(walk), next(walk))

g = pre(tree)                     # ...and one-shot: drained once, empty ever after
print("drain once :", list(g))
print("drain twice:", list(g))
OUTPUTpre-order  : A B D E C F
in-order   : D B E A C F
post-order : D E B F C A
level-order: A B C D E F
first two of in-order: D B
drain once : ['A', 'B', 'D', 'E', 'C', 'F']
drain twice: []
TRIPWIRES
  • Drop one from and the walk goes quiet, not wrong-loud. With yield from pre(node.left) the six-node tree yielded ['A', 'B', 'D', 'E', 'C', 'F']; with a bare pre(node.left) it yielded ['A'] — no error, no warning. The bare call built a generator (type(...).__name__ printed generator) and dropped it on the floor, unstarted. A generator you never iterate does nothing at all.
  • yield from still borrows the call stack, one frame per level. sys.getrecursionlimit() is 1000 here, and by bisection the deepest right-leaning spine this walk survived was 998 nodes; a 5,000-node spine raised RecursionError: maximum recursion depth exceeded. A balanced million-node tree recurses only ~19 deep, so the danger is never size — it is shape. The fix is the explicit-stack walk from the deeper cut: stack = [root], pop, yield, push both children. That version walked a 1,000,000-node spine and returned 0 first and 999999 last.
  • The recursive walks handle the empty tree for free; the queue version does not. list(ino(None)) returned [], but list(level(None)) raised AttributeError: 'NoneType' object has no attribute 'value' — because deque([None]) puts the emptiness inside the queue, where nothing checks it. Guard level-order at the door: if root is None: return.

Each order is the right tool for a different job, and that is why all three survive. Pre-order touches a node before its children, so it is how you copy a tree or write it to a file: you emit the parent, then everything beneath it. Post-order touches a node after both children, so it is how you free or delete a tree safely, because you never remove a parent while its children still hang off it. In-order touches the left subtree, then the node, then the right. On the ordered trees of the next chapter, that single rule hands you every value in sorted order, for free.

Feel why post-order is the safe order to delete in. On our tree it visits D E B F C A, so you free D and E first, and only then their parent B. Next you free F, then its parent C, and the root A comes last of all. At no moment do you free a node while a child still points into it — every parent outlives its children by exactly one step. Reverse the order and you would free A while B and C still dangle, leaking the whole tree hanging beneath it. Here the order is the correctness, not a detail you can shrug off.

one recursion body · slide the “visit node” line to one of three slots PRE-ORDER visit node go left go right A B D E C F serialize / copy IN-ORDER go left visit node go right D B E A C F sorted (on a BST) POST-ORDER go left go right visit node D E B F C A delete / evaluate
Fig — the three depth-first walks are one body with the visit step in three slots — before, between, or after the two descents. Level-order is the odd one out: swap the stack for a queue.
InteractivePick a walk — watch the order light up
A B C D E F D → B → E → A → C → F in-order (left · node · right) → a BST yields sorted order
Same tree, four rules. Only the placement of “touch the node” changes the order — except level-order, which trades the stack for a queue.
Where you meet this — including the page you're reading right now
The DOM is a tree: every web page is a hierarchy of elements, and your browser built this page by walking one. querySelector is a tree search; React and every virtual-DOM framework works by diffing two trees. Your computer's file system is a tree of folders — find descends it pre-order, du sums sizes post-order (a folder's size needs its children's first). Compilers parse source into an expression/syntax tree and evaluate it post-order (operands before the operator). JSON and XML are trees on the wire; org charts, family trees, and machine-learning decision trees are trees by name. You navigate a dozen trees before breakfast.
In-order only means “sorted” on a BST
The famous line “in-order traversal gives sorted output” is true only when the tree is ordered — a binary search tree (the next chapter), where left descendants are smaller and right descendants larger. On the general tree here, in-order is just left·node·right — D B E A C F — with no sorted meaning. The order in the output comes from the order in the tree, not from the traversal.
The deeper cut — walking without recursion

Recursion is the natural way, but it borrows the call stack, and on a deep or degenerate tree that stack overflows (the RecursionError above). Production tree code often carries its own explicit stack instead. Push the root, then loop: pop a node, do its work, push its children. That turns depth-first recursion into an ordinary while loop with a Python list as the stack. Same visit order, no recursion-limit wall. There's an even slyer method, Morris traversal, that temporarily rewires leaf pointers to thread the tree and walks it in-order using O(1) extra space, no stack at all. Both are the same lesson: the traversal order is a choice, and the machinery that remembers “where was I?” is a separate, swappable choice.

How deep is “too deep”? CPython ships with a recursion limit of 1,000 frames by default. A balanced tree would need more than 21000 nodes to reach that depth, more than there are atoms in the observable universe, so recursion is perfectly safe there. But a degenerate spine of just a couple of thousand sorted values will sail straight past it and raise RecursionError. That is the real reason production code keeps its own stack. Not speed, but survival on the exact input that already hurt you most.

Four walks and a two-pointer node: that's the entire general binary tree. Its power comes alive the moment you add one rule to the shape — and each rule you can add is a whole chapter. →

06When to reach for a tree

Strip everything away and a binary tree earns its keep in two situations. First, when your data is a hierarchy, a thing that contains things that contain things. Then the tree isn't a clever choice, it's the honest shape of the data: files, the DOM, an org chart. Second, when you need logarithmic reach, with insert, find, and delete all in O(log n). A tree delivers that, but only while its height stays near log n. That “only while” is the entire craft. A bare binary tree gives you the shape, but it does not defend the balance. The specialized trees ahead each add exactly one rule to earn the log back and never let it slip.

BINARY TREE a node · two child pointers BST — ch 9 + order: left < node < right → search & sorted walk HEAP — ch 10 + complete · array-backed → instant min / max TRIE — ch 11 + branch by symbol → prefix lookup
Fig — the binary tree is the substrate. Add order and it becomes a BST; add completeness and an array and it becomes a heap; branch by symbol and it becomes a trie. Each “+ one rule” is a chapter.

That is the 1% move, and it is most of what being “good at data structures” actually is: match the access pattern to the layout. See a hierarchy, and you reach for a tree. Need ordered data with fast search, and it's a BST. Need the smallest or largest item again and again, and it's a heap. Need to look things up by prefix, and it's a trie. All four are this chapter's node-with-two-pointers wearing one extra discipline. Choosing the right discipline on sight, before you write a single line, is the real skill. The code is the easy part that follows.

WATCH AN EXPERT THINKwhich node is the deepest one with both of these underneath it?
A hierarchy is in memory and two of its nodes are handed to you — the node objects themselves, not their values. One question has to be answered: which node is the deepest one that has both of them somewhere beneath it? That node is the lowest common ancestor, and the everyday spellings of the question are all around you: the nearest shared folder of two files, the closest manager two people both report to, the smallest element on a page that contains two clicked elements, the heading two paragraphs of a document both sit under. (The commit where two branches last agreed sounds like the same question and is not quite — a Git history is a DAG, not a tree, so a merge base needs the graph-aware version, for exactly the reason the misconception check gave.) The facts that ship with the problem are exactly this chapter's. A node is a value and two child pointers. There are no parent pointers — a node cannot look up, only down. And the values obey no order whatsoever: this is the plain binary tree of section 01, not the ordered one arriving next chapter, so there is nothing to compare and nothing to steer by. One label before we start, so nothing here reads as more than it is: the classic statement of this problem assumes both nodes really are in the tree, and we will come back and test that assumption at the end, because it turns out to be the whole distance between an answer that is right and an answer that is merely confident. Now rewind to before you read section 05 and watch somebody arrive, because the route matters more than the destination — the turn nearly everybody takes is a good one.
FIRST INSTINCT
The question says ancestor, so build ancestry. A node here cannot look up, having no parent field, so manufacture one: sweep the tree once and for every child you meet record parent[child] = node. Now it is the problem you would solve on paper with a finger. Climb from p to the root dropping every node you pass into a seen set, then climb from q and return the first node already in it. It is correct on the first attempt and it stays correct, which is exactly what will make its close cousin a good referee further down. Then you sharpen it, and the sharpening is real work rather than a straw man. The map does not depend on p or q at all — so build it once and keep it, and every later query is two short climbs, O(h). Measured below on a 32,767-node tree and hedged as machine-dependent, the cached version answered in about 14 us a query on this machine, against about 12,240 us for the solution this section is walking toward. On a hierarchy that never changes, asked millions of times, precomputation is the right call and the numbers say so out loud.
WHY IT STALLS
Look at what you had to build in order to answer a question about two nodes: a second copy of the whole tree. Every edge is already stored once, in a child pointer; the map stores every edge a second time, backwards. Rebuilt on each call that is 2,599,800 bytes of dictionary on a 32,767-node tree, measured — and the bill tracks the node count, not the height, which is the number the rest of this chapter has been about: 2,047 nodes cost 154,456 B, 8,191 cost 617,304 B, 32,767 cost 2,599,800 B. That is about 79 bytes a node, so a ten-million-node hierarchy would want roughly 790 MB for a single query — arithmetic from the measured per-node figure, not a measurement. Cache the map instead and the bytes stop repeating, but they never leave, and now you own a structure that is only as correct as it is fresh: every insert and every delete has to update the mirror, and a stale mirror hands back a confident wrong ancestor. One correctness question has quietly become two. The path-comparison variant drops the dictionary and keeps the idea — record both root-to-node paths, walk them together, take the last node they agree on — and it is small and honest enough to serve as the referee below. It simply never stops storing ancestry: two searches down the tree, two chains held in memory, and a comparison loop, all to answer a question that turns out not to need a chain at all.
THE TELL
Say what a lowest common ancestor is, in one sentence, without using the word ancestor: it is the one node where the two targets part company. Everywhere above it, both targets are on the same side. At it, one goes left and the other goes right — or it is one of them. That sentence never asks for a chain. It asks for a single bit about each subtree, does a target live down there, and a recursion can hand that bit back as its ordinary return value, with nothing stored anywhere at all. So let the return value carry it: a subtree containing neither target returns None; a subtree containing one returns that one; and the first node whose left and right both come back non-None is the answer, because that is the only place the two can be on opposite sides. The awkward case needs no clause of its own — a node that is a target returns itself before it descends, so a target that is the other's own ancestor falls out for free, and lca(5, 4) came back 5. Notice which walk this is. The answer at a node is computed only after both children have reported: post-order, for precisely the reason section 05 gave, and the sheet on the pub wall gave before it. The box where two teams meet is the first box both their runs reach, and it cannot be filled in until both sides have come back.
import random, time, tracemalloc
from itertools import combinations_with_replacement

class Node:
    __slots__ = ('v', 'left', 'right')
    def __init__(self, v, left=None, right=None):
        self.v, self.left, self.right = v, left, right

#            3                  the tree everybody draws for this question
#         /     \
#        5       1
#      /  \     /  \
#     6    2   0    8
#         / \
#        7   4
def build():
    return Node(3,
                Node(5, Node(6), Node(2, Node(7), Node(4))),
                Node(1, Node(0), Node(8)))

# ---- FIRST INSTINCT: manufacture the parent links, then climb --------------
def parent_map(root):
    parent, stack = {id(root): None}, [root]
    while stack:                               # a whole pass over the tree
        n = stack.pop()
        for c in (n.left, n.right):
            if c is not None:
                parent[id(c)] = n
                stack.append(c)
    return parent

def climb(parent, p, q):
    seen = set()
    while p is not None:                       # every ancestor of p, root-ward
        seen.add(id(p)); p = parent[id(p)]
    while q is not None:                       # the first one q shares with p
        if id(q) in seen: return q
        q = parent[id(q)]
    return None

def lca_parents(root, p, q):                   # the map, rebuilt on every call
    parent = parent_map(root)
    if id(p) not in parent or id(q) not in parent: return None
    return climb(parent, p, q)

# ---- THE REFEREE: record both root-to-node paths, then compare -------------
def path_to(root, target):
    if root is None: return None
    if root is target: return [root]
    for child in (root.left, root.right):
        sub = path_to(child, target)
        if sub is not None: return [root] + sub
    return None

def lca_paths(root, p, q):
    a, b = path_to(root, p), path_to(root, q)
    if a is None or b is None: return None     # a target that is not in the tree
    best = None
    for x, y in zip(a, b):
        if x is not y: break                   # this is where they part company
        best = x
    return best

# ---- THE TELL: one post-order pass, no ancestry stored anywhere ------------
def lca(node, p, q):
    if node is None or node is p or node is q:
        return node                            # empty, or one of the targets
    L = lca(node.left,  p, q)
    R = lca(node.right, p, q)
    if L is not None and R is not None:
        return node                            # they parted company HERE
    return L if L is not None else R           # both, or neither, lie one side

# ---- the same single pass, made honest about targets that are absent ------
def lca_checked(node, p, q):
    """(candidate, how many of p and q this subtree actually contained)"""
    if node is None: return (None, 0)
    hit = 1 if (node is p or node is q) else 0
    L, lf = lca_checked(node.left,  p, q)
    R, rf = lca_checked(node.right, p, q)
    found = hit + lf + rf
    if hit or (L is not None and R is not None): return (node, found)
    return ((L if L is not None else R), found)

def lca_safe(root, p, q):
    node, found = lca_checked(root, p, q)
    return node if found == 2 else None

# ==== 1. the referee agrees with both, on every pair ========================
def index(node, out=None):
    out = {} if out is None else out
    if node is not None:
        out[node.v] = node; index(node.left, out); index(node.right, out)
    return out

root = build(); V = index(root)
pairs = list(combinations_with_replacement(sorted(V), 2))
agree = all(lca(root, V[a], V[b]) is lca_paths(root, V[a], V[b])
                                 is lca_parents(root, V[a], V[b]) for a, b in pairs)
print("tree 3(5(6, 2(7, 4)), 1(0, 8))")
print(f"all {len(pairs)} pairs : one-pass == path-referee == parent-map -> {agree}")
for a, b in ((6, 4), (7, 8), (5, 4), (2, 2)):
    tag = "  <- a target that is the other's own ancestor" if a in (5, 2) else ""
    print(f"   lca({a}, {b}) = {lca(root, V[a], V[b]).v}   referee {lca_paths(root, V[a], V[b]).v}{tag}")

# ==== 2. what each one costs on a real tree =================================
def perfect(h, n=[0]):
    if h < 0: return None
    left = perfect(h - 1); n[0] += 1
    return Node(n[0], left, perfect(h - 1))

def leaves_of(node, out=None):
    out = [] if out is None else out
    if node is not None:
        if node.left is None and node.right is None: out.append(node)
        leaves_of(node.left, out); leaves_of(node.right, out)
    return out

H = 14
BIG = perfect(H)
random.seed(56)
leaves = leaves_of(BIG)
queries = [(random.choice(leaves), random.choice(leaves)) for _ in range(200)]

CACHE = parent_map(BIG)                        # the instinct at its strongest:
def lca_cached(root, p, q):                    # pay for the map ONCE, keep it
    return climb(CACHE, p, q)

def best_us(fn, rounds=3):
    runs = []
    for _ in range(rounds):
        t0 = time.perf_counter()
        for p, q in queries: fn(BIG, p, q)
        runs.append(time.perf_counter() - t0)
    return min(runs) * 1e6 / len(queries)

def peak_bytes(fn, tree=None, pair=None):
    tree = BIG if tree is None else tree
    p, q = queries[0] if pair is None else pair
    tracemalloc.start(); tracemalloc.reset_peak()
    fn(tree, p, q)
    _, peak = tracemalloc.get_traced_memory(); tracemalloc.stop()
    return peak

print(f"\nperfect tree, {2**(H+1)-1:,} nodes, height {H} -- 200 random leaf pairs, best of 3")
print(f"   {'approach':<24}{'us/query':>10}{'peak extra bytes':>19}   what it holds")
for name, fn, holds in (
        ("parent map, per query", lca_parents, "a dict of every node, rebuilt every call"),
        ("parent map, cached once", lca_cached, "nothing new -- but 2.6 MB standing, forever"),
        ("two paths + compare", lca_paths, "two root-to-node lists"),
        ("one post-order pass", lca, "nothing but its own frames")):
    print(f"   {name:<24}{best_us(fn):>9,.0f}{peak_bytes(fn):>19,}   {holds}")
print("   all 200 answers identical:",
      all(lca(BIG,p,q) is lca_paths(BIG,p,q) is lca_parents(BIG,p,q) is lca_cached(BIG,p,q)
          for p, q in queries))
sizes = []
for h in (10, 12, 14):
    t = perfect(h); lv = leaves_of(t)
    sizes.append(f"{2**(h+1)-1:,} nodes -> {peak_bytes(lca_parents, t, (lv[0], lv[-1])):,} B")
print("   the map tracks the NODE count, not the height:", ";  ".join(sizes))

# ==== 3. the near miss: a target that is not in the tree ====================
orphan = Node(99)
print("\nnear miss -- the lca of node 6 and a node that is NOT in this tree")
print(f"   one-pass -> {lca(root, V[6], orphan).v}     confident, and wrong")
print(f"   referee  -> {lca_paths(root, V[6], orphan)}")
print(f"   lca_safe -> {lca_safe(root, V[6], orphan)}    same one pass, it just counts what it met")
print(f"   lca_safe(6, 4) -> {lca_safe(root, V[6], V[4]).v}    still right when both really are there")
tree 3(5(6, 2(7, 4)), 1(0, 8))
all 45 pairs : one-pass == path-referee == parent-map -> True
   lca(6, 4) = 5   referee 5
   lca(7, 8) = 3   referee 3
   lca(5, 4) = 5   referee 5  <- a target that is the other's own ancestor
   lca(2, 2) = 2   referee 2  <- a target that is the other's own ancestor

perfect tree, 32,767 nodes, height 14 -- 200 random leaf pairs, best of 3
   approach                  us/query   peak extra bytes   what it holds
   parent map, per query       5,319          2,599,800   a dict of every node, rebuilt every call
   parent map, cached once        14              1,240   nothing new -- but 2.6 MB standing, forever
   two paths + compare        14,613                840   two root-to-node lists
   one post-order pass        12,240                  0   nothing but its own frames
   all 200 answers identical: True
   the map tracks the NODE count, not the height: 2,047 nodes -> 154,456 B;  8,191 nodes -> 617,304 B;  32,767 nodes -> 2,599,800 B

near miss -- the lca of node 6 and a node that is NOT in this tree
   one-pass -> 6     confident, and wrong
   referee  -> None
   lca_safe -> None    same one pass, it just counts what it met
   lca_safe(6, 4) -> 5    still right when both really are there
trigger: reach for a single post-order pass the moment a question about two nodes turns out to be a question about which subtree each of them is in — then the recursion returns a summary, and you never store a path. The family is large once you can see it, and every member is the same six lines with a different summary coming back up: is this subtree balanced (return its height, or a sentinel), how wide is the tree at its widest (return the depth on each side), what is the deepest leaf, does this subtree contain X. In every one of them the parent's answer is a small fact assembled from two smaller facts, which is the post-order shape and nothing more. Then learn the near miss, because a tell that fires on everything is not a tell. Ask the one-pass version for the ancestor of node 6 and a node that is not in the tree at all, and it returned 6 — instantly, with no error, and wrong. It is not broken; it answers the question it was actually written for, which is “where do these two part company, given that both are here”, and with one target missing that question has no answer to give. The referee said None, correctly, because a path it could not build is a target it never found. And the repair does not cost you the single pass: return a pair, the candidate and how many of the two targets that subtree actually met, then refuse to answer unless the count is 2. Measured, lca_safe returned None on the orphan and still 5 for lca(6, 4). Two more habits are worth as much as the tell itself. Keep the stupid version and let it referee: the path-comparison walk was written before anybody got clever, so it cannot share the clever mistake, and it confirmed the one-pass on all 45 pairs of the nine-node tree and all 200 queries on the big one. And quote the clock honestly. On the wall clock the one-pass was slower: about 12,240 us on this machine against the rebuilt map's 5,319 us — and both of those are machine-dependent in a way the byte columns beside them are not, since re-running the same script moves the clock figures by a percent or two while every allocation figure lands identically every time. It is slower because the map's sweep is an iterative loop while the recursion pays CPython for about 65,000 function calls — chapter 33's lesson that equal Big-O can run far apart, arriving one more time. Its win was never the clock. It allocated 0 bytes against 2,599,800, it holds nothing that can go stale, and it is six lines you can get right on a tree you are not allowed to copy. When the tree really is static and the query really is hot, cache the map, or reach past both of these for a proper precomputation scheme. The skill is knowing which of those three worlds you are standing in before you write a line.
NOW COUNT THE LEAVES, THEN TURN THE TREE INSIDE OUTtwo three-line recursions — and one of them you can check without eyes
The drill. Two functions on the chapter's Node (the one with __slots__ = ('value', 'left', 'right')). First count_leaves(node): how many nodes have no children at all. Then mirror(node): swap every left/right pair, top to bottom, so the tree comes out as its own reflection.

Write them the way the data is shaped. Both are three lines, and both are the same move you have already seen in height: name the base case (the empty tree), then combine the answers from two smaller trees. For count_leaves the trap is the base case — there are two of them, and getting “empty” and “leaf” confused makes a one-node tree answer 0. For mirror, write the swap as a single tuple assignment and ask yourself why node.left = mirror(node.right) on its own line would destroy the tree.

The check. Run count_leaves on the six-node tree (expect 3: D, E, F), on None (expect 0) and on a lone node (expect 1). Then prove mirror without drawing anything: an in-order walk of the mirrored tree must be the exact reverse of the in-order walk before it, and mirroring twice must give you back what you started with. Finish by building perfect trees of height 0…4 and checking that the leaf count is 2**h every time — that is the doubling from section 02, measured instead of believed.
show the solution
class Node:
    __slots__ = ('value', 'left', 'right')
    def __init__(self, value, left=None, right=None):
        self.value, self.left, self.right = value, left, right

def count_leaves(node):
    """A leaf is a node with no children at all."""
    if node is None:                            # empty tree -> no leaves
        return 0
    if node.left is None and node.right is None:
        return 1                                # a leaf counts itself and stops
    return count_leaves(node.left) + count_leaves(node.right)

def count_nodes(node):
    if node is None: return 0
    return 1 + count_nodes(node.left) + count_nodes(node.right)

def mirror(node):
    """Swap every left/right pair, top to bottom - in place."""
    if node is None:
        return None
    node.left, node.right = mirror(node.right), mirror(node.left)
    return node

def inorder(node, out):
    if node is None: return
    inorder(node.left, out); out.append(node.value); inorder(node.right, out)

def build():
    return Node('A', Node('B', Node('D'), Node('E')), Node('C', None, Node('F')))

def perfect(h, k=0):
    if h < 0: return None
    return Node(k, perfect(h - 1, 2*k + 1), perfect(h - 1, 2*k + 2))

t = build()
print("nodes      :", count_nodes(t))
print("leaves     :", count_leaves(t), "  (D, E, F)")
print("empty tree :", count_leaves(None))
print("lone node  :", count_leaves(Node('X')))

before = []; inorder(t, before)
mirror(t)
after = []; inorder(t, after)
print("\nin-order before mirror:", ' '.join(before))
print("in-order after  mirror:", ' '.join(after))
print("after == reversed(before)?", after == before[::-1])
print("leaves survive the mirror:", count_leaves(t))

mirror(t); again = []; inorder(t, again)          # a mirror is its own undo
print("mirror twice == original?", again == before)

print()
for h in range(5):
    p = perfect(h)
    print(f"perfect height {h}: nodes={count_nodes(p):3}  leaves={count_leaves(p):3}"
          f"  2**h={2**h:3}")

# ---------------- OUTPUT, verbatim (CPython 3.12.7) ----------------
# nodes      : 6
# leaves     : 3   (D, E, F)
# empty tree : 0
# lone node  : 1
#
# in-order before mirror: D B E A C F
# in-order after  mirror: F C A E B D
# after == reversed(before)? True
# leaves survive the mirror: 3
# mirror twice == original? True
#
# perfect height 0: nodes=  1  leaves=  1  2**h=  1
# perfect height 1: nodes=  3  leaves=  2  2**h=  2
# perfect height 2: nodes=  7  leaves=  4  2**h=  4
# perfect height 3: nodes= 15  leaves=  8  2**h=  8
# perfect height 4: nodes= 31  leaves= 16  2**h= 16

Next — the binary search tree: add a single rule to the shape you just learned — every value in the left subtree smaller than the node, every value on the right larger. Suddenly the in-order walk prints in sorted order for free, and finding a value becomes binary search running on a tree. That is the binary search tree — and the whole fight to keep it from degenerating into the spine you met here. →

SAY IT BACKthe chapter in five breaths
  1. A tree is scattered boxes joined by pointers — nowhere in RAM is there a thing shaped like a tree, only node objects at unrelated heap addresses, each holding one reference to a value and two more to its left and right children (56 bytes with __slots__, 344 without, measured), and the root is nothing but a name holding a single 8-byte address; the hierarchy you picture exists only as the arrows.
  2. Height, not node count, is what a lookup costs — there is no index to jump with, so reaching any node means following one child pointer per level, which is O(height): balanced, a million nodes put every element within 19 hops of the root, while the very same million arriving in sorted order unspool into a spine of height n-1, a linked list in a tree costume that searches in O(n) and is deep enough to blow the call stack.
  3. The definition contains itself — a binary tree is either empty or a node plus two smaller binary trees — so the code gets to be recursive because the data already is: name the base case (the empty tree, which needs no work), combine the answers from the two smaller trees, and height, mirror, counting the nodes, and summing the values are three lines each, every one of them O(n) because every node is visited exactly once.
  4. Sliding a single line changes the job: touch the node before, between, or after the two descents and you get pre-order A B D E C F (a parent before its subtree — copy and serialize), in-order D B E A C F (sorted only on an ordered tree, never on a general one), and post-order D E B F C A (a parent only after both children — delete and evaluate); swap the call stack for a queue and the same tree comes out level by level, A B C D E F.
  5. Nothing enforces treeness, so the shape is whatever your arrows say it is — one node hung in two places is a DAG your traversal counts twice without a word of complaint, and a child pointing at an ancestor is a cycle it never escapes — and the scatter carries a second bill Big-O never prints: summing a million values by hopping pointers took about 312 ms against about 82 ms for the same million in a contiguous list, identical O(n), opposite memory behaviour.
reach for this when… your data really is a hierarchy — a thing that contains things that contain things — and you get to an item by descending into it rather than by index or by name. All three clauses carry weight, and each one has a near neighbour that wants a different structure. If you reach items by a name, you never wanted two axes of descent at all — that is chapter 54's hash map. If the items form a line, chapter 49's array is lighter and streams through cache instead of stalling on it, and the tree's two extra pointers per element buy you nothing. If you need ordered search or a sorted walk, add one rule to what you just built and it becomes chapter 57's BST; if you need the smallest item again and again, add completeness and an array underneath and it becomes a heap; if you look things up by prefix, branch on the symbol and it becomes a trie. And if an item can have two parents, or a link can loop back on itself, you do not have a tree at all — you have chapter 44's graph, and every walk over it needs a seen set before it is safe to run.
You already owned the pieces: chapter 50 gave you the linked list — a value plus one pointer to the next box, scattered across the heap and reachable only by following the chain — and a binary tree is that identical box with a second pointer bolted on: one pointer makes a line, two make a hierarchy, and everything expensive about both (the pointer tax, the cache misses, the O(n) walk with no index to jump with) came with the first pointer, not the second. Chapter 41 gave you recursion and a base case, which is why every function in this chapter fitted in three lines; chapter 52 gave you the queue, and level-order is that queue doing its one job, holding the shallowest waiting node at the front. Chapter 33 explained why two O(n) walks can run four times apart, and Volume 1 supplied the ground under all of it: RAM as one line of numbered boxes, and a name as a single 8-byte reference — which is exactly what root is, and why losing it loses the whole tree. What chapter 56 added is small and load-bearing: a second child pointer, and the discipline that keeps the two of them a tree.
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 56, in working code

A tree is nothing but boxes scattered across the heap, each holding a value and two pointers — so let's build one from those boxes, measure why its height is the whole game, and walk it four different ways.

Boxes joined by pointers
A node is one value and two references; nowhere in RAM is there a thing shaped like a tree — the shape exists only in where those pointers point.
Height is the whole game
Every find, insert, and delete is a walk from the root down one pointer per level, so a tree's cost is exactly how tall it stands — log n when balanced, n when it collapses into a spine.
Recursion is its native tongue
A node whose two children are themselves smaller trees is a recursive definition frozen into data — so counting, summing, and walking all write themselves against the empty tree as the base case.
Four walks, four jobs
Slide the single 'touch the node' line among the two recursive descents for three depth-first orders; swap the call stack for a queue and the same tree comes out level by level.
end of chapter 56 · six sections
iolinked
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked