Chapter 10
18 min read
Section 58 of 261

Tree Terminology and Properties

Tree Fundamentals

Why Trees? The Shape of Hierarchy

Up to this chapter we have worked with structures that are essentially linear: arrays, linked lists, stacks, queues. Each element has at most one predecessor and one successor. Linearity is powerful, but the world is not flat. The folders on your computer, the replies under a forum post, the pages reachable from a homepage, the executives reporting to a CEO, the syntactic structure of a sentence, the state of a chess game several moves ahead — none of these are lines. They branch.

A tree is the data structure that captures branching hierarchy. One designated element — the root — sits at the top, every other element has exactly one immediate ancestor, and there are no cycles. That single restriction (one parent per element, no cycles) is what makes trees enormously useful: it lets us traverse, search, summarize, and modify hierarchies in time proportional to O(logn)O(\log n) when the tree is balanced, and never worse than O(n)O(n) in the worst case.

Intuition. If a linked list is a chain, a tree is a family tree. Every person has exactly one biological mother in the diagram, but a mother can have many children. Pick any person; trace upward and you eventually reach the original ancestor. There are no loops — you cannot be your own great-grandparent.

Before any formal definition, here are the everyday hierarchies that trees model in production software. Notice that the "branching factor" differs wildly — sometimes 2, sometimes thousands — and the depth differs too:

Real-world hierarchyRootInternal nodesLeaves
Unix file system/DirectoriesRegular files
DOM of a web page<html>Elements (<div>, <ul>, ...)Text nodes
JSON documentTop-level objectNested objects/arraysStrings, numbers, true/false/null
Decision tree (ML)Root questionInternal questionsClass label or value
Game tree (chess, Go)Current positionFuture positionsTerminal positions
Compiler ASTProgramExpressions, statementsIdentifiers, literals
Org chartCEODirectors, managersIndividual contributors
Phylogenetic treeLast common ancestorSpeciation eventsLiving species

Understanding tree terminology is therefore not just bookkeeping. It is the vocabulary you will use every time you ship a feature that touches a file system, a UI tree, a configuration document, a routing table, a version-control history, a database index, or a machine-learning model of class hierarchies. The rest of this chapter, and most of the next five chapters, will be unintelligible without it.


A Formal Definition

A tree is a finite set TT of nodes such that:

  1. Either T=T = \emptyset (the tree is empty),
  2. Or there is a designated node rTr \in T, called the root, and the remaining nodes T{r}T \setminus \{r\} are partitioned into k0k \geq 0 disjoint subsets T1,T2,,TkT_1, T_2, \dots, T_k, each of which is itself a tree, called a subtree of rr.

The recursive flavor is the point. The definition says a tree is either empty, or a root attached to a list of trees. That single sentence is what makes nearly every tree algorithm in this book a few lines of recursion: a function that handles the empty case and the "root plus subtrees" case is, by induction, a function that handles every tree.

An equivalent graph-theoretic definition is sometimes more useful: a tree is an undirected graph that is connected and acyclic. In a connected graph there is a path between every pair of vertices; in an acyclic graph there is no closed loop. A tree of nn nodes therefore has exactly n1n - 1 edges — one fewer than the number of nodes — because every node except the root contributes exactly one edge to its parent.

Rooted vs. free trees

The recursive definition gives you a rooted tree: there is a chosen top. The graph-theoretic definition gives you a free tree: nodes are equal citizens. Choosing a root in a free tree turns it into a rooted tree, and that is almost always what we mean in computer science. From here on, "tree" means "rooted tree" unless we say otherwise.

Core Terminology, Visualized

The visualizer below shows a single tree of n=11n = 11 nodes. Click any node to see all of the relationships defined in this section colored simultaneously. Each definition will refer back to the same picture, so you can read with one hand on the slider.

Click any node to see its parent, children, siblings, ancestors and descendants.
Selected node
G
is an internal node
Parent & ancestors (amber)
parent = C
ancestors = [C, A]
Children & descendants (green)
children = [K]
descendants = [K]
Siblings (purple)
siblings = []
depth = 2, height = 1, degree = 1
level 0 · depth 0level 1 · depth 1level 2 · depth 2level 3 · depth 3ABCDEFGHIJK
selectedancestors / path to rootdescendants / subtreesiblingstree height = 3, total nodes = 11, total edges = 10

With that picture in mind, here is the terminology, in the order you will use it most often:

  • Root. The unique node with no parent. In our example the root is AA. A non-empty tree has exactly one root.
  • Parent and child. If uu is immediately above vv in the tree, then uu is the parent of vv, and vv is a child of uu. Every non-root node has exactly one parent. BB is a parent of EE and FF.
  • Sibling. Two nodes are siblings if they share the same parent. EE and FF are siblings; so are H,I,JH, I, J.
  • Leaf (or external node). A node with no children. In our example the leaves are E,F,K,H,I,JE, F, K, H, I, J. Leaves carry the "data" in many trees: file contents in a file system, text nodes in the DOM, predicted classes in a decision tree.
  • Internal node. Any non-leaf node. In our example A,B,C,D,GA, B, C, D, G are internal. Internal nodes carry structure (the directories, the elements, the questions); leaves carry content.
  • Edge. The connection between a parent and one of its children. A tree with nn nodes has exactly n1n - 1 edges — we will prove this in a moment.
  • Path. A sequence of nodes v0,v1,,vkv_0, v_1, \dots, v_k in which each consecutive pair is connected by an edge. In a tree there is exactly one path between any two nodes — another consequence of being connected and acyclic.
  • Ancestor and descendant. If the unique path from vv upward to the root passes through uu, then uu is an ancestor of vv, and vv is a descendant of uu. Every node is its own ancestor and its own descendant by convention (these are the "improper" cases). Click KK in the visualizer: its ancestors are G,C,AG, C, A.
  • Subtree. The subtree rooted at vv is vv together with all of its descendants. The subtree at DD consists of {D,H,I,J}\{D, H, I, J\}. Subtrees are the recursive handle on a tree: every algorithm of the form "do something to a tree" is, almost always, "do something to the root, then recurse on each subtree."

Improper vs. proper

Some textbooks call the strict cases "proper ancestor", "proper descendant", "proper subtree", and reserve "ancestor" for the non-strict version that includes the node itself. We will write "strict" or "non-strict" when the distinction matters.

Depth, Height, Level, Degree

Five numbers are attached to every tree and every node. They look similar at first glance, but they measure different things, and mixing them up is the most common bug in undergraduate tree code.

QuantityDefined forDefinitionExample in our tree
depth(v)a node vNumber of edges on the path from the root to v.depth(K) = 3
height(v)a node vNumber of edges on the longest downward path from v to a leaf.height(C) = 2, height(leaf) = 0
height(T)the whole treeEqual to height(root). Equivalently: max depth over all nodes.height(T) = 3
level(v)a node vEqual to depth(v) + 1 in the 1-indexed convention, or just depth(v) in the 0-indexed convention.Root is at level 1 (or 0)
degree(v)a node vNumber of children of v.degree(D) = 3, degree(leaf) = 0
degree(T)the whole treeMaximum degree across all nodes.degree(T) = 3 in our example

Three short, important identities follow directly from the table:

  1. Root has depth 0. The path from the root to itself has zero edges. So depth(root)=0\text{depth}(\text{root}) = 0.
  2. Leaf has height 0. The longest downward path from a leaf is the empty path, which has zero edges. So height(leaf)=0\text{height}(\text{leaf}) = 0.
  3. Tree height is bounded by node count. height(T)n1\text{height}(T) \leq n - 1, with equality when the tree is a single chain (a "stick").

Off-by-one trap

Some textbooks define height in terms of nodes on the longest path rather than edges, which makes the height of a single-node tree 11 instead of 00. We use the edge-count convention throughout the book, because it makes the recurrences for balanced trees in Chapters 11 – 13 clean (an empty tree has height 1-1, a single-node tree has height 00, two levels of nodes has height 11, and so on). Whatever convention you pick, be ruthlessly consistent inside one project.

Mathematical Properties of Trees

Three properties are worth proving once, in full, because every later chapter quietly assumes them. They are simple, but they are the reason balanced search trees, heaps, segment trees, and tries can promise O(logn)O(\log n) operations.

1. A tree on n nodes has exactly n − 1 edges.

Every node except the root contributes exactly one edge to its parent. The root contributes none. So the total number of edges equals the number of non-root nodes, which is n1n - 1. As a corollary, if you ever count nn edges in something you are calling a tree, you have either miscounted or it is not a tree (it has a cycle, or a node with two parents).

2. The unique-path property.

Between any two nodes u,vu, v there is exactly one path. If there were two distinct paths, joining them would form a cycle, contradicting acyclicity; if there were none, the tree would not be connected. This is what allows ancestor/descendant relationships to be well-defined: there is no ambiguity about "the" path from a node to the root.

3. Height is between log₂ n and n − 1.

For a binary tree (every internal node has at most 2 children), the number of nodes at depth dd is at most 2d2^d. Summing over depths 0,1,,h0, 1, \dots, h gives at most 2h+112^{h+1} - 1 nodes total, so n2h+11n \leq 2^{h+1} - 1, which rearranges to hlog2(n+1)1h \geq \lceil \log_2 (n + 1) \rceil - 1. On the other extreme, a tree shaped like a chain has h=n1h = n - 1. So:

log2(n+1)1    h    n1\lceil \log_2(n+1) \rceil - 1 \;\leq\; h \;\leq\; n - 1

The whole story of balanced binary trees (AVL, red-black, B-trees) is the story of forcing the height to stay near the lower bound. We will explore that in Chapter 13. For now, slide nn below to feel the spread:

Edges in any tree
n − 1 = 14
Min height (balanced binary tree)
⌊log₂ n⌋ = 3
Max height (stick / linked-list-shaped)
n − 1 = 14

Notice how at n=64n = 64 the balanced height is 66 while the worst-case stick height is 6363. That ten-times-shorter path is the difference between an instant search and a sluggish one, and it is why the rest of this book cares so much about keeping trees balanced.


Kinds of Trees You Will Meet

The same word "tree" describes many shapes with very different properties. Knowing the names lets you communicate precisely with other engineers and pick the right algorithm.

NameDefining propertyWhere you see it
General treeEach node may have any non-negative number of children.File systems, DOM, org charts.
k-ary treeEach node has at most k children. (k = 2 is the most common.)Quad-trees (k=4), oct-trees (k=8) in 3D graphics.
Binary treeEach node has at most 2 children, conventionally labelled left and right.BSTs, heaps, expression trees, Huffman trees.
Full binary treeEvery internal node has exactly 2 children.Huffman coding, decision tournaments.
Complete binary treeAll levels filled except possibly the last, which is filled left-to-right.Binary heaps stored as arrays (Chapter 14).
Perfect binary treeAll internal nodes have 2 children AND all leaves are at the same depth.Idealized analyses; n = 2^(h+1) − 1.
Balanced treeHeight is O(log n). Specific definitions vary (AVL, red-black, weight-balanced).Maps and sets in std::map, java.util.TreeMap, etc.
Binary search tree (BST)In-order traversal yields a sorted sequence; left < parent < right.Indexes, ordered associative containers.
HeapParent dominates children under a fixed order (min-heap or max-heap).Priority queues, Dijkstra's algorithm.
TrieEdges labelled with alphabet symbols; paths spell out keys.Autocomplete, IP routing tables.
B-treeEach node holds many keys and many children; designed for disk pages.Database indexes, file system directory entries.

We will spend chapters on each of these. For this section the only thing to internalize is that they are all trees — the terminology in this section applies to every one of them — but each one piles extra invariants on top to make a particular operation cheap.


A Tree Node in C

Now let us encode a tree in actual code. We start with C because in C the memory layout of a tree is visible to the eye: a node is a struct with payload data and pointers to other structs. The pointer is the edge.

For a general tree (any number of children), the cleanest layout is the first-child / next-sibling representation: each node stores one pointer to its leftmost child and one pointer to the next sibling on its right. This handles arbitrary fan-out in two pointers per node, no dynamic arrays required.

A general-tree node in C: first-child / next-sibling
📄tree.c
1Standard headers

stdio.h for printf/fprintf, stdlib.h for malloc/free/exit, string.h pulled in for completeness so the same file compiles when later sections add string-keyed labels.

7The struct definition is recursive

TreeNode contains pointers to TreeNode. The struct is forward-referenced inside its own definition by writing 'struct TreeNode *child' before the typedef name is bound. This is the C idiom for building any pointer-based recursive structure.

EXAMPLE
Other recursive types follow the same shape: a linked list (one next pointer), a doubly-linked list (next + prev), or a binary tree (left + right).
8label: the payload

A single char keeps the printed example small. Real code stores anything here: a string, a struct, an offset into a payload arena, etc. The tree structure itself does not care what the payload is; that is the value of separating structure from data.

9child: pointer to the leftmost child

If the node is a leaf, child is NULL. Otherwise it points to one specific child — the leftmost — and the rest of the children are reachable by following sibling pointers from there. This is the classic 'multi-way tree on two pointers' trick.

EXAMPLE
Tree fragment: B has children E and F. Then B->child = E, E->sibling = F, F->sibling = NULL.
10sibling: pointer to the next sibling

Each node lives in a singly-linked list of its parent's children. The list is terminated by NULL. From any node v, walking v->sibling, (v->sibling)->sibling, ... visits all of v's right-siblings in order.

14Constructor pattern in C

C has no constructors, so the convention is a 'new_X' factory function: malloc, initialize fields, return the pointer. Always pair it with a 'free_X' that returns the memory to the heap when the structure is no longer needed.

EXAMPLE
Owners must remember to call free_tree() exactly once on every node they allocate, or you leak memory. Valgrind / AddressSanitizer will catch the mistake in CI.
15malloc + sizeof(TreeNode)

Allocate exactly enough bytes for one TreeNode on the heap. The cast to (TreeNode *) is stylistic in C; it is required in C++. malloc returns NULL when the OS refuses the allocation (almost never on a desktop, but real on embedded targets).

16Defensive failure handling

Always check malloc's return. The branch happens once per allocation and costs almost nothing; the alternative is dereferencing NULL elsewhere and crashing far away from the actual cause.

21Initialize child and sibling to NULL

A freshly created node has no children and no right-siblings yet. Setting both pointers to NULL makes the recursive walks (free_tree, traversals later in this chapter) terminate naturally without a separate 'is leaf?' flag.

27add_child: append in O(k)

We walk the existing sibling chain to its tail and append. This is fine for the typical case where children are added in document order and node degree is small. If degree gets large (thousands of children), a 'last_child' cache or an array-of-children representation is faster.

EXAMPLE
Adding [B, C, D] to A: first call sets A->child = B; second walks B and sets B->sibling = C; third walks B->C and sets C->sibling = D.
28Empty child list special case

When the parent has no children yet, parent->child IS the slot to fill. We don't need to walk anywhere.

31Walk to the tail

s starts at the parent's first child and advances along the sibling chain until s->sibling is NULL — that is, until s is the rightmost current child. Then we attach the new child there.

EXAMPLE
If parent already has [E, F] and we add G: s = E -> F (since F->sibling is NULL we stop) -> F->sibling = G.
39free_tree: recursive cleanup

We free the subtree rooted at the leftmost child first, then the chain of siblings, then the node itself. Doing the recursion BEFORE the free is essential: once we free(n) we may not dereference n->child or n->sibling.

EXAMPLE
On A: free_tree(B-subtree) -> free_tree(C-subtree) -> free_tree(D-subtree) -> free(A).
40Base case: NULL

Trying to free a NULL pointer is allowed in standard C (free(NULL) is a no-op), but reading n->child off a NULL n is undefined behavior. So we explicitly bail at NULL before any dereference.

41Free child subtree first

free_tree(n->child) recursively destroys all descendants of n. Because of the first-child / next-sibling layout, this single call also handles all of n's children's siblings — they are reachable via child->sibling.

42Free the right-sibling chain too

When free_tree is called on a 'subtree root' it should NOT also walk its siblings; but when free_tree is called from itself recursively on n->child, that child is the head of a sibling list that all need freeing. The same function handles both responsibilities cleanly.

43Finally, free the node itself

After the descendants and the right-siblings are gone, the node has no live references through this structure. We hand its memory back to the allocator.

49Build A and its three children

We allocate four nodes and wire B, C, D in as A's children using add_child. Each add_child call costs O(degree(A)-so-far), so the total cost of building A's child list is O(1 + 2 + 3) = O(6). Negligible here, but worth knowing for larger trees.

EXAMPLE
After these calls: A->child = B, B->sibling = C, C->sibling = D, D->sibling = NULL.
56Inline construction

We skip the named local for E and F because we never refer to them after construction. The pattern add_child(parent, new_node('X')) is idiomatic and reads top-to-bottom like the picture.

60Keep G named — we need it later

G has its own child K, so we need a handle to it. Naming G makes the next add_child(G, new_node('K')) line legible.

67free_tree(A) tears the whole structure down

A single call cleans up all 11 nodes. That is the elegance of the recursive structure: the recursion in free_tree walks every descendant exactly once and frees each in O(1).

57 lines without explanation
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5/* A node in a general tree. The "first-child / next-sibling" layout
6   represents arbitrary fan-out using only two pointers per node. */
7typedef struct TreeNode {
8    char label;                  /* node payload (a single char for the demo) */
9    struct TreeNode *child;      /* leftmost child, or NULL if leaf */
10    struct TreeNode *sibling;    /* next sibling to the right, or NULL */
11} TreeNode;
12
13/* Allocate a new node with the given label and no children. */
14TreeNode *new_node(char label) {
15    TreeNode *n = (TreeNode *) malloc(sizeof(TreeNode));
16    if (n == NULL) {
17        fprintf(stderr, "out of memory\n");
18        exit(1);
19    }
20    n->label   = label;
21    n->child   = NULL;
22    n->sibling = NULL;
23    return n;
24}
25
26/* Append a child to the end of parent's child list.
27   Cost: O(k) where k is the current number of children of parent. */
28void add_child(TreeNode *parent, TreeNode *child) {
29    if (parent->child == NULL) {
30        parent->child = child;
31    } else {
32        TreeNode *s = parent->child;
33        while (s->sibling != NULL) {
34            s = s->sibling;
35        }
36        s->sibling = child;
37    }
38}
39
40/* Recursively free the entire subtree rooted at n. Post-order:
41   children first, then the node itself. */
42void free_tree(TreeNode *n) {
43    if (n == NULL) return;
44    free_tree(n->child);
45    free_tree(n->sibling);
46    free(n);
47}
48
49int main(void) {
50    /* Build the tree from the visualizer:
51              A
52            / | \
53           B  C  D
54          /|  |  /|\
55         E F  G H I J
56              |
57              K
58    */
59    TreeNode *A = new_node('A');
60    TreeNode *B = new_node('B');
61    TreeNode *C = new_node('C');
62    TreeNode *D = new_node('D');
63    add_child(A, B); add_child(A, C); add_child(A, D);
64
65    add_child(B, new_node('E'));
66    add_child(B, new_node('F'));
67
68    TreeNode *G = new_node('G');
69    add_child(C, G);
70    add_child(G, new_node('K'));
71
72    add_child(D, new_node('H'));
73    add_child(D, new_node('I'));
74    add_child(D, new_node('J'));
75
76    free_tree(A);
77    return 0;
78}

Why first-child / next-sibling?

It is the cheapest representation that supports arbitrary fan-out in C without dynamic arrays. For binary trees specifically, we usually prefer a different layout — an explicit left and right pointer per node — because the structure of binary trees benefits from naming the two children. We will use that layout starting in Section 10.2.

The Same Tree in Python

Python hides pointers behind the garbage collector and lets us write the same structure as a small class. The semantics are identical (a node holds references to its children) but the lifetime is managed for us, and the algorithms are easier to read because the syntax is closer to the math.

A general tree node in pure Python
🐍tree.py
1Postponed annotation evaluation

from __future__ import annotations makes type hints like List['TreeNode'] (a self-referential forward reference) work without quoting tricks at every site. Standard practice on any module that defines a recursive class.

2dataclass + field for safe mutable defaults

@dataclass auto-generates __init__, __repr__, __eq__. field(default_factory=list) is the correct way to give each instance its own empty list — using `children: list = []` would make ALL instances share one list, a classic Python footgun.

EXAMPLE
Without default_factory: TreeNode('A').children is TreeNode('B').children would be True. With default_factory it is False.
7TreeNode class definition

We model a node with three fields: a label (the payload), a list of children, and an optional pointer back to the parent. The list-of-children layout is more Pythonic than first-child / next-sibling — it pays a few bytes per node in exchange for clearer code and O(1) append.

8Docstring on the class

A one-line docstring reachable via help(TreeNode). For a public data-structure type, the docstring is what shows up in IDE hover tooltips, so make it precise.

9label

Type-annotated as str for the demo. In a production tree this would be parameterized via Generic[T], or just left as Any. The point is that the *structure* of TreeNode is independent of the payload type.

10children: List[TreeNode]

Each node owns a Python list of its child nodes, in left-to-right order. Appending to this list is amortized O(1). Looking up the i-th child is O(1). Removing an arbitrary child is O(k) — fine for small fan-out.

EXAMPLE
After add_child calls in build_demo_tree, A.children == [B, C, D] in that order. A.children[0] is B.
11parent: Optional[TreeNode]

An upward link. Optional means the type is TreeNode or None. The root is the only node with parent == None, which is exactly what is_root checks. Storing parent costs one reference per node but makes upward walks (ancestors, depth) O(depth) instead of O(n).

13add_child: the only setter

Funneling all child additions through one method keeps the parent and children fields synchronised — the parent list and the child's parent attribute are updated together. Returning the child enables fluent chains like G = C.add_child(TreeNode('G')).

16Set parent BEFORE appending

Order matters when you write generic tree code. If a debugger or a logging callback is triggered on append, having parent already set means the callback sees a fully consistent node.

EXAMPLE
In a multithreaded context (rare for raw Python) the order also avoids a brief window where the child is in its parent's list but does not yet know its own parent.
19is_leaf: a node with no children

Returns True if children is empty. Often you write `if not node.children` directly, but a named predicate documents intent and is easy to grep for.

EXAMPLE
Leaves of build_demo_tree(): E, F, K, H, I, J — exactly the nodes for which is_leaf() is True.
22is_root: a node with no parent

Returns True only for the topmost node. In build_demo_tree, only A.is_root() is True; every other node has its parent attribute set by add_child.

26build_demo_tree mirrors the C example

Building the same 11-node tree as the C version. Comparing the two side-by-side is the point of this section: the algorithm is the same, the underlying machine model is the same, only the syntax differs.

28B = A.add_child(...) — fluent build

Capturing the return value lets us keep building without re-walking from the root. A.add_child(TreeNode('B')) creates B, attaches it under A, and hands B back, all in one line.

36G is held onto for K

G is the parent of K, so we need a reference to G when we call G.add_child(TreeNode('K')). Without the assignment we would have to find G again via root.children[1].children[0], which is brittle.

45The if __name__ == '__main__' guard

Lets this file be both an importable module and a runnable script. When someone imports tree, build_demo_tree exists but the print statements do not run. When someone runs `python tree.py`, the prints execute.

47Inspecting structure via attributes

root.children[0].children[0].label walks A -> B -> E and reads its label. This kind of expression is exactly what gets written first when you start exploring a tree in a REPL or a debugger; recognizing the pattern speeds up debugging tree code in any language.

EXAMPLE
Output:\n  root label: A\n  root degree: 3\n  first leaf under B: E
32 lines without explanation
1from __future__ import annotations
2from dataclasses import dataclass, field
3from typing import List, Optional
4
5
6@dataclass
7class TreeNode:
8    """A node in a general (multi-way) tree."""
9    label: str
10    children: List["TreeNode"] = field(default_factory=list)
11    parent: Optional["TreeNode"] = None  # filled in by add_child
12
13    def add_child(self, child: "TreeNode") -> "TreeNode":
14        """Attach child as the rightmost child of self. Returns child."""
15        child.parent = self
16        self.children.append(child)
17        return child
18
19    def is_leaf(self) -> bool:
20        return len(self.children) == 0
21
22    def is_root(self) -> bool:
23        return self.parent is None
24
25
26def build_demo_tree() -> TreeNode:
27    A = TreeNode("A")
28    B = A.add_child(TreeNode("B"))
29    C = A.add_child(TreeNode("C"))
30    D = A.add_child(TreeNode("D"))
31
32    B.add_child(TreeNode("E"))
33    B.add_child(TreeNode("F"))
34
35    G = C.add_child(TreeNode("G"))
36    G.add_child(TreeNode("K"))
37
38    D.add_child(TreeNode("H"))
39    D.add_child(TreeNode("I"))
40    D.add_child(TreeNode("J"))
41    return A
42
43
44if __name__ == "__main__":
45    root = build_demo_tree()
46    print("root label:", root.label)
47    print("root degree:", len(root.children))
48    print("first leaf under B:", root.children[0].children[0].label)

The two implementations are the same algorithm

Both encodings store eleven nodes and ten edges, both expose parent/children relationships, and both can be traversed in linear time. The Python version trades a little memory (a Python list per node, plus a parent pointer) for cleaner code; the C version trades a little code complexity (manual linked list of siblings, manual free) for tight, predictable memory use. Section 10.2 will revisit these trade-offs more carefully under the heading tree representations.

Computing Height and Depth from a Node

Two of the numbers we defined — depth and height — are computed by tiny recursive walks. They are the "hello world" of tree algorithms, and you should be able to write them without thinking.

Depth and height as recursive walks
🐍tree_props.py
1Reuse the demo tree

Importing TreeNode and build_demo_tree from the previous file keeps each example focused. In a real codebase tree.py would live in a tree/ package and these helpers would live in tree/properties.py.

4depth: walk upward from node to root

Depth counts EDGES, so we start at 0, then walk one parent at a time, counting each step. When we reach the root (parent is None) we stop. Time O(depth(node)), space O(1).

EXAMPLE
On node K in the demo tree:\n  start: cur = G, d = 1\n  step:  cur = C, d = 2\n  step:  cur = A, d = 3\n  step:  cur = None — stop\n  return 3
6Iterative, not recursive

We write depth as a loop because the parent chain is just a linked list — no branching. The iterative form avoids a recursion depth equal to the tree's depth, which matters for trees deeper than Python's default recursion limit (~1000).

14height: a recurrence on subtrees

Height of a node = 1 + max height of its children, with the leaf base case = 0. The function calls itself once on each child, visiting every descendant exactly once — total cost O(size of subtree).

EXAMPLE
On node C in the demo tree:\n  height(K) = 0 (leaf)\n  height(G) = 1 + max(0) = 1\n  height(C) = 1 + max(height(G)) = 1 + 1 = 2
15Base case: a leaf has height 0

A node with no children has no downward edges, so the longest downward path has length 0. Without this base case the recursion would never bottom out.

171 + max(height(c) for c in children)

max() over a generator expression evaluates height on every child and picks the largest. The +1 accounts for the edge from this node down to whichever child sits on the longest path.

EXAMPLE
If the children's heights are [0, 1, 0], max is 1 and we return 2. The tallest descendant lives via the middle child.
26Walk to K explicitly

We thread root.children[1].children[0].children[0] manually to grab K. In production you would expose a helper like find_by_label, but for a demonstration the explicit indexing makes the path obvious.

28Expected output

depth(K) = 3 because the path A -> C -> G -> K has 3 edges. height(root) = 3 because the root's tallest subtree is the one containing K. height(C) = 2 because the longest path under C is C -> G -> K, two edges.

EXAMPLE
Output:\n  depth(K) = 3\n  height(root) = 3\n  height(C) = 2
22 lines without explanation
1from tree import TreeNode, build_demo_tree
2
3
4def depth(node: TreeNode) -> int:
5    """Edges from the root to this node."""
6    d = 0
7    cur = node.parent
8    while cur is not None:
9        d += 1
10        cur = cur.parent
11    return d
12
13
14def height(node: TreeNode) -> int:
15    """Edges on the longest downward path from node to a leaf."""
16    if not node.children:
17        return 0
18    return 1 + max(height(c) for c in node.children)
19
20
21def height_of_tree(root: TreeNode) -> int:
22    return height(root)
23
24
25if __name__ == "__main__":
26    root = build_demo_tree()
27    K = root.children[1].children[0].children[0]   # A -> C -> G -> K
28    print("depth(K) =", depth(K))                  # 3
29    print("height(root) =", height_of_tree(root))  # 3
30    print("height(C) =", height(root.children[1])) # 2
Pattern. Almost every tree algorithm in this book follows the shape of height: a base case for the empty or leaf subtree, and a recursive case that combines the answers from each child. Internalize this pattern now and the rest of the chapter will feel like variations on a theme.

Edge Cases: Empty Trees, Single Nodes, Forests

Three boundary cases come up constantly in interviews and in real code, and getting them right is what separates a robust tree library from a leaky one.

  • The empty tree. The recursive definition allows T=T = \emptyset. In code this means the root pointer can be NULL (in C) or None (in Python). Every recursive algorithm must handle this case explicitly — usually with if node is None: return ... on the first line. The height of the empty tree is conventionally 1-1, so that height(leaf)=1+height()=0\text{height}(\text{leaf}) = 1 + \text{height}(\emptyset) = 0 comes out right.
  • The single-node tree. One node, zero edges. It is simultaneously the root, a leaf, the only ancestor of itself, and the only descendant of itself. depth = 0, height = 0, degree = 0. Test every traversal you write against this case before trusting it.
  • The forest. A collection of disjoint trees. Forests arise when you split a tree into parts (e.g. remove the root) or when an algorithm naturally produces several disconnected hierarchies (e.g. Kruskal's minimum-spanning-tree algorithm maintains a forest of growing components, Chapter 21). A forest can be turned into a single tree by adding a fake super-root with the forest's trees as its children — a useful trick when an algorithm requires a single root.

Off-by-one on the empty tree

The most common bug in beginner tree code is computing the height of an empty tree as 0 instead of −1. Symptom: every leaf reports height 1, every parent reports height 2, etc., and a balanced-tree rotation in Chapter 13 fails its own invariant by 1.

Where Trees Live in the Real World

It is worth pausing at the end of a foundational section to look outward. The terminology you just learned is the same terminology used by the engineers who built the systems you use every day.

SystemTree it usesWhat the structure buys
Linux ext4 / NTFS / APFSDirectory tree + B-tree indexesO(log n) file lookup; cheap rename = pointer swap.
GitCommit DAG + per-commit Merkle tree of blobsContent-addressed storage; tamper-evident history.
Browsers (DOM)Hierarchical element treeCSS selectors, event bubbling, layout all walk the tree.
React / Vue / AngularVirtual-DOM tree + fiber treeDiffing two trees produces minimal real-DOM updates.
PostgreSQL / MySQL / SQLiteB-tree (or B+-tree) indexesO(log n) range queries on disk.
MongoDB / ElasticsearchB-tree / LSM-tree indexesSame idea, tuned for write-heavy workloads.
IP routing tablesTrie (radix tree) on IP prefixesLongest-prefix match in time linear in prefix length.
Compilers (gcc, clang, rustc)AST + dominator tree + IR call treeOptimization passes are tree rewrites.
Databases of facts (Wikidata)Class hierarchy / taxonomy treeInheritance of properties between concepts.
Game AI (chess engines)Game tree (minimax + alpha-beta)Pruning relies on the tree's structure to skip subtrees.
Decision-tree models (XGBoost)Decision treeEach leaf is a prediction; depth controls capacity.
PhylogeneticsSpecies tree, gene treeCommon-ancestor queries answer evolution questions.

Read this table back to yourself with the words you learned in this section. The Git commit DAG is "almost a tree" with merge commits introducing extra parents; B-tree indexes are balanced trees with very high degree (typically hundreds), giving an extremely small height; the DOM is a general tree in which leaves are usually text nodes; an AST is a tree whose internal-node degree depends on the syntactic category. The same vocabulary, applied everywhere.


Quick Check

Quick Check

In the visualizer's tree, what is height(C)?

Quick Check

Which statement is FALSE for every tree?

Quick Check

You delete the root of a non-empty tree. What is left?


What Comes Next

You now have the vocabulary (root, parent, child, sibling, leaf, ancestor, descendant, subtree), the numbers (depth, height, level, degree), the mathematical properties (n1n - 1 edges, unique path, height bounds), the C and Python encodings, and the recursive pattern that every tree algorithm follows. Section 10.2 will widen the lens and survey the four standard representations of trees in memory — first-child / next-sibling, array-of-children, parent-pointer-only, and array-indexed (the heap-style flat encoding) — with a clear rubric for which to use when.

Sections 10.3 and 10.4 will then introduce the four classical traversals (preorder, inorder, postorder, level-order), each of which is a one-page recursive program built on the same node structures we defined here. By the end of the chapter you will have written, by hand, every operation you need to implement a basic file viewer, a simple expression evaluator, or a generic tree visitor.

Loading comments...