Binary Trees

Imagine organizing a company's org chart, a filesystem, or the moves in a chess game. Each of these is naturally a branching hierarchy, not a flat sequence. A linked list can only chain items in one dimension, front to back. The moment a problem has a natural notion of "children," a linear structure becomes awkward. This includes parent-child reporting lines, folders containing subfolders, and decisions branching into sub-decisions. A tree becomes the right shape of thought. A binary tree is the simplest branching structure. Each node has at most two children, conventionally called left and right. This one constraint — "at most two" — is enough to build an entire universe of efficient algorithms. Binary search trees turn ordered search into a logarithmic-time operation. Binary heaps turn "find the minimum" into a constant-time query with logarithmic updates. Expression trees encode arithmetic so that evaluation is a simple traversal. Huffman coding trees compress data. Every one of these rests on the same skeleton described here. This article isolates that skeleton: what a binary tree is, how its size relates to its height. It also covers the vocabulary — full, complete, perfect — used to describe its shape. These shape properties are not pedantic naming exercises. They are the reason a Binary Heap can guarantee O(log⁡n)O(\log n) insertion. They also explain why a poorly maintained Binary Search Tree can degrade to O(n)O(n) per operation. Understanding shape is understanding performance. By the end, you will be able to define a binary tree precisely using recursion. You will derive the relationship between height and node count in both directions. You will classify any binary tree's shape correctly. You will also recognize why an unbalanced tree is a performance trap waiting to spring. This groundwork sets up Tree Traversals. It directly prepares the ground for binary search trees and binary heaps.

Key takeaways
  • A binary tree is recursively defined: it is empty, or a root with a left binary tree and a right binary tree as children.
  • Height hh bounds node count: a binary tree of height hh has at most 2h+1−12^{h+1}-1 nodes and at least h+1h+1 nodes.
  • Equivalently, nn nodes force height at least ⌈log⁡2(n+1)⌉−1\lceil \log_2(n+1)\rceil - 1, which is the source of O(log⁡n)O(\log n) tree algorithms.
  • Full: every node has 0 or 2 children. Complete: all levels filled except possibly the last, filled left-to-right. Perfect: all internal nodes have 2 children and all leaves sit at the same depth.
  • Perfect trees are a special case of complete trees, which are a special case of full trees is not generally true — the three properties are independent-ish and worth distinguishing carefully.
  • Balance (keeping height close to log⁡n\log n) is the central engineering concern that later structures like AVL trees, red-black trees, and heaps are built to guarantee.
The core insight

A binary tree's height, not its node count, is what determines how fast operations on it run — so controlling shape is controlling performance.

The idea

A binary tree is defined recursively: it is either empty (denoted ∅\varnothing or `NIL`), or it consists of a root node holding a value, together with a left subtree and a right subtree, each of which is itself a binary tree. This recursive definition is deliberately unlike an ordinary graph-theoretic tree, because it treats "has no left child" and "has no right child" as structurally distinct — a node with only a right child is a different shape from a node with only a left child, even though both have exactly one child. That distinction, absent from general trees, is what makes binary trees so convenient to reason about with recursive algorithms. Standard vocabulary: the parent of a node is the node directly above it; a node with no parent is the root. A node with no children is a leaf (or external node); a node with at least one child is internal. The depth of a node is the number of edges from the root to that node (root has depth 0). The height of a node is the number of edges on the longest downward path from that node to a leaf; the height of the tree is the height of its root. A single-node tree has height 0; the empty tree is conventionally assigned height −1-1 so that the recursive formula h(T)=1+max⁡(h(Tleft),h(Tright))h(T) = 1 + \max(h(T_{\text{left}}), h(T_{\text{right}})) holds cleanly even when a subtree is empty. Three shape classifications matter throughout the rest of tree-based data structures:

  • A binary tree is full (or "proper") if every node has either 0 or 2 children — no node has exactly one child.
  • A binary tree is complete if every level is fully filled except possibly the last, and the last level's nodes are packed as far left as possible.
  • A binary tree is perfect if every internal node has exactly 2 children and every leaf is at the same depth.

A perfect tree is always complete and always full. A complete tree need not be full (a node in the last level can have only a left child, making that node degree-1). A full tree need not be complete (a lopsided tree can have every node with 0 or 2 children yet leave gaps in shape). These three notions are logically independent except for the strict implication chain perfect ⟹ complete and perfect ⟹ full; the reverse implications fail. Binary heaps insist on completeness because it lets the tree be stored implicitly in an array; balanced search trees do not insist on completeness, only on height being O(log⁡n)O(\log n).

How it works

There is no single "algorithm" that is a binary tree — it is a data structure, not a procedure — but there is a standard mechanism for representing one and a standard family of recursive procedures that operate on it. The representation uses a node record with three fields: a value, and pointers/references to a left child and a right child, either of which may be null. This is a direct generalization of the singly linked list's one-pointer node to a two-pointer node, and it should feel familiar coming from Linked Lists and Pointers and references. Because the structure is defined recursively, essentially every operation on it is naturally written recursively: compute a size, compute a height, check a shape property, search for a value, or rebuild the tree from a serialization. The pattern is always the same — handle the empty tree as the base case, then combine the results from the left and right recursive calls. ``` struct Node: value left # pointer to Node, or NIL right # pointer to Node, or NIL

# ---- Basic recursive queries ---- function SIZE(node): if node == NIL: return 0 return 1 + SIZE(node.left) + SIZE(node.right) function HEIGHT(node): if node == NIL: return -1 # empty tree has height -1 by convention return 1 + max(HEIGHT(node.left), HEIGHT(node.right))

# ---- Shape classification ---- function IS_FULL(node): if node == NIL: return true if node.left == NIL and node.right == NIL: return true if node.left != NIL and node.right != NIL: return IS_FULL(node.left) and IS_FULL(node.right) return false # exactly one child present: not full function IS_PERFECT(node): return IS_PERFECT_HELPER(node, HEIGHT(node), 0) function IS_PERFECT_HELPER(node, h, depth): if node == NIL: return true if node.left == NIL and node.right == NIL: return depth == h # every leaf must sit at depth h if node.left == NIL or node.right == NIL: return false # internal node must have BOTH children return IS_PERFECT_HELPER(node.left, h, depth + 1) and IS_PERFECT_HELPER(node.right, h, depth + 1)

# ---- Completeness check (level-order / BFS based) ---- function IS_COMPLETE(root): if root == NIL: return true queue = new Queue() queue.push(root) seenNilAfterNode = false # once we see a "gap", no real node may follow while queue is not empty: node = queue.pop_front() if node == NIL: seenNilAfterNode = true else: if seenNilAfterNode: return false # a real node appeared after a gap: not complete queue.push(node.left) # push even if NIL — we need to detect gaps queue.push(node.right) return true ``` `SIZE` and `HEIGHT` walk every node once, combining left and right results — the canonical divide-and-conquer pattern for trees. `IS_FULL` and `IS_PERFECT` similarly recurse, with `IS_PERFECT` additionally threading the tree's height down so every leaf's depth can be checked against it. `IS_COMPLETE` is different in character: completeness is fundamentally a level-by-level ("breadth-first") property, so it is checked using a queue-based traversal that walks the tree level by level, pushing `NIL` placeholders for missing children — the moment a real node shows up after a gap has been recorded, completeness is violated. This level-order idea foreshadows Tree Traversals, where breadth-first and depth-first orders are treated as a topic in their own right.

Figure
A binary tree of height 3 with node and level labels
depth 0 depth 1 depth 2 depth 3 A B C D E F G height = 3, nodes = 7 full: yes (every node 0 or 2 children)

Complexity & correctness

Space. A binary tree with nn nodes uses O(n)O(n) space for the nodes themselves, plus O(n)O(n) pointers (two per node, some null). No hidden overhead scales worse than linear — this is a direct consequence of the recursive definition, where each node is counted exactly once across the whole structure.

Time for the recursive queries. `SIZE`, `HEIGHT`, and `IS_FULL` each visit every node exactly once and perform O(1)O(1) work per node (besides the recursive calls), so each runs in O(n)O(n) time and uses O(h)O(h) auxiliary stack space, where hh is the tree's height — this is the depth of the recursion. In the worst case (a completely lopsided tree, effectively a linked list) h=n−1h = n-1, so the stack usage is O(n)O(n); in the best case (a perfect or complete tree) h=O(log⁡n)h = O(\log n).

`IS_COMPLETE` complexity. The queue-based check visits each real node once and enqueues at most two `NIL` placeholders per node, giving O(n)O(n) time and O(n)O(n) worst-case queue space (a perfect tree's last level alone can hold roughly n/2n/2 nodes simultaneously in the queue).

The height–node-count relationship — the central fact of this article. Claim (upper bound). A binary tree of height hh has at most 2h+1−12^{h+1}-1 nodes. Proof sketch by induction on hh. Base case h=−1h = -1 (empty tree): 20−1=02^{0}-1 = 0 nodes, correct. Base case h=0h=0 (single node): 21−1=12^1 - 1 = 1, correct. Inductive step: a tree of height hh has a root plus a left subtree of height at most h−1h-1 and a right subtree of height at most h−1h-1. By the inductive hypothesis each subtree has at most 2h−12^{h}-1 nodes, so the whole tree has at most

1+(2h−1)+(2h−1)=2h+1−11 + (2^{h}-1) + (2^{h}-1) = 2^{h+1}-1

nodes. Equality holds exactly when the tree is perfect, since that is precisely the condition that both subtrees are themselves full-sized trees of height h−1h-1. ■\blacksquare Claim (lower bound). A binary tree of height hh has at least h+1h+1 nodes. Proof sketch. The longest root-to-leaf path realizing the height has h+1h+1 nodes on it (depths 0,1,…,h0,1,\dots,h), and every node on this path is a distinct node of the tree, so the tree contains at least h+1h+1 nodes. Equality holds exactly for a degenerate tree — one where every node has at most one child, i.e., the tree is effectively a linked list bent into tree form. ■\blacksquare

Why this matters for algorithm design. Rearranging the upper bound n≤2h+1−1n \le 2^{h+1}-1 gives

h≥⌈log⁡2(n+1)⌉−1,h \ge \lceil \log_2(n+1) \rceil - 1,

meaning a tree cannot be shorter than roughly log⁡2n\log_2 n — but it is entirely free to be as tall as n−1n-1. This asymmetry is the whole reason balanced tree structures exist: nothing in the bare definition of "binary tree" prevents height from degenerating to n−1n-1, which is exactly what happens if you insert already-sorted keys one at a time into a naive Binary Search Tree. A search, insertion, or deletion that walks root-to-leaf costs O(h)O(h), so an unbalanced tree gives O(n)O(n) operations — no better than a linked list — while a tree kept close to perfect gives O(log⁡n)O(\log n) operations. Binary heaps sidestep the whole issue by enforcing completeness on every insertion and deletion, which guarantees h=⌊log⁡2n⌋h = \lfloor \log_2 n \rfloor at all times; self-balancing search trees (AVL, red-black) enforce a weaker but sufficient invariant that still guarantees h=O(log⁡n)h = O(\log n).

Tradeoff to be honest about. Guaranteeing balance is not free: it requires extra bookkeeping (heights or colors stored per node) and rebalancing work (rotations) on every insertion and deletion. A plain unbalanced binary tree is simpler to implement and faster in the average case on random data — but it offers no worst-case guarantee, which is precisely why production systems favor the balanced variants for adversarial or unpredictable workloads.

Worked example

Consider building a binary tree by inserting the keys `50, 30, 70, 20, 40, 60, 80` in this order, using the ordinary binary-search-tree rule (smaller keys go left, larger keys go right) purely as a way to generate a concrete shape — full BST search semantics are covered in Binary Search Trees.

StepInsertTree after insertion (parent → child)
150root = 50
23050 → left 30
37050 → left 30, right 70
42030 → left 20
54030 → right 40
66070 → left 60
78070 → right 80

The resulting tree: ``` 50 / \ 30 70 / \ / \ 20 40 60 80 ``` Trace `HEIGHT(50)`:

  • `HEIGHT(50) = 1 + max(HEIGHT(30), HEIGHT(70))`
  • `HEIGHT(30) = 1 + max(HEIGHT(20), HEIGHT(40)) = 1 + max(0, 0) = 1`
  • `HEIGHT(70) = 1 + max(HEIGHT(60), HEIGHT(80)) = 1 + max(0, 0) = 1`
  • `HEIGHT(50) = 1 + max(1, 1) = 2`

Trace `SIZE(50) = 1 + SIZE(30) + SIZE(70) = 1 + 3 + 3 = 7`, matching n=7n = 7. Check against the bound: height h=2h = 2, so the maximum possible node count is 2h+1−1=23−1=72^{h+1}-1 = 2^3-1 = 7. The tree hits the bound exactly — confirming, via the proof above, that this tree must be perfect. Indeed every internal node (50, 30, 70) has exactly two children, and every leaf (20, 40, 60, 80) sits at depth 2. Running `IS_PERFECT` confirms this: at depth 2 every branch bottoms out at a leaf, and `HEIGHT(50) = 2` matches every leaf's depth exactly. Now contrast with inserting the same seven keys already sorted: `20, 30, 40, 50, 60, 70, 80`. Each new key is larger than everything before it, so every node gets only a right child: ``` 20 \ 30 \ 40 \ 50 \ 60 \ 70 \ 80 ``` Here `HEIGHT(20) = 6` and `SIZE(20) = 7` — the same node count as before, but height has jumped from 2 to 6, hitting the lower bound h=n−1=6h = n - 1 = 6 exactly. This tree is degenerate: `IS_FULL` returns false at the very first node (20 has one child), and a search for `80` now costs 7 comparisons instead of 3. Same data, same number of nodes, wildly different performance — precisely because shape, not size, governs cost.

Common pitfalls & traps

PitfallWhy it happensFix
Off-by-one in height, treating empty tree as height 0Forgetting the −1-1 convention for the empty tree breaks the recursive formula h=1+max⁡(… )h = 1+\max(\dots)Fix the base case explicitly: `HEIGHT(NIL) = -1`, single node = 0
Confusing "complete" with "full"Both sound like "no gaps," but complete is about level-filling order, full is about children-count parityTest each with a small counterexample: a root with only a left child is full-violating but could still be part of a complete tree only if it's the very last node
Assuming perfect ⟹ height is log⁡2n\log_2 n exactlyTrue only when n=2h+1−1n = 2^{h+1}-1; for arbitrary nn the tree cannot even be perfectUse h≥⌈log⁡2(n+1)⌉−1h \ge \lceil \log_2(n+1)\rceil - 1 as the general lower bound, not equality
Checking completeness with plain recursion (no queue)Completeness is a breadth-first, level-by-level property; naive depth-first recursion can't see "gaps across siblings" without extra bookkeepingUse the level-order/queue technique, marking the first `NIL` encountered and rejecting any real node found afterward
Building a search tree from sorted input and expecting O(log⁡n)O(\log n)Naive BST insertion of monotonic data produces a degenerate, linked-list-shaped treeUse a self-balancing structure, or insert in randomized/shuffled order, or bulk-load into a perfect shape upfront
Stack overflow on deep recursive traversalRecursive `HEIGHT`/`SIZE` use O(h)O(h) stack frames; a degenerate tree has h=n−1h = n-1For very large or untrusted trees, use an explicit stack or iterative level-order approach

Pioneers & history

The binary tree as an abstract data type crystallized alongside the broader development of list processing and symbolic computation in the late 1950s, most directly through work on the LISP programming language, whose core data structure — the cons cell, a pair of pointers — is exactly a binary tree node. The formal analysis of tree height, balance, and search cost was developed through the 1960s and 1970s as computer scientists built the first self-balancing search trees and heap-based priority queues, turning "keep the tree short" from an informal goal into a provable invariant.

Origins · 1 minJohn McCarthy and how this was discovered
JM
John McCarthy
1927–2011 · Stanford AI Lab, inventor of LISP and the cons-cell binary tree

McCarthy designed LISP in 1958 with the `cons` pair — two pointers, `car` and `cdr` — as its sole compound data primitive. Chaining `cons` cells through their second pointer builds linked lists; using both pointers to point to further pairs builds binary trees directly. This made the binary tree, rather than the array, the default hierarchical structure of an entire generation of symbolic and AI programming.

History
1958
— John McCarthy introduces LISP; the cons cell makes binary-tree-like pairs a primitive data type.
1962
— J. Robson and others formalize height-balance conditions that later shape AVL analysis.
1962
— Georgy Adelson-Velsky and Evgenii Landis publish the AVL tree, the first self-balancing binary search tree, guaranteeing O(log⁡n)O(\log n) height.
1964
— J. W. J. Williams introduces the binary heap as an implicit, array-based complete binary tree for efficient priority queues (heapsort).
1972
— Rudolf Bayer introduces symmetric binary B-trees, an ancestor of the red-black tree, further formalizing balanced binary search structures.

Where this matters

Binary trees underlie an enormous share of practical computing infrastructure precisely because of the height-versus-size relationship proven above. Database indexes and filesystem metadata frequently use B-trees and their binary relatives to guarantee logarithmic-time lookups even as data grows into the billions of records. Priority queues used in operating system schedulers, Dijkstra's shortest-path algorithm, and event-driven simulations rely on binary heaps — complete binary trees stored implicitly in arrays, exploiting exactly the shape guarantee discussed here. Compilers represent arithmetic and boolean expressions as expression trees, where a binary tree's recursive structure mirrors the recursive structure of the grammar being parsed. Compression schemes such as Huffman coding build an explicit binary tree whose leaf depths directly encode codeword lengths, again leaning on the height-count relationship to bound compression efficiency. Self-balancing binary search trees (red-black trees, AVL trees) back the standard ordered-map and ordered-set containers found in nearly every major programming language's standard library, from C++'s `std::map` to Java's `TreeMap`. Every one of these systems is, underneath, a careful engineering response to the single fact proved in this article: an unconstrained binary tree can be as tall as n−1n-1, and something has to actively fight that possibility to keep operations fast.

Ask the tutor: How exactly do AVL rotations restore the height-balance invariant after an insertion, and why is one or two rotations always enough?

Is every binary tree also a general (graph-theoretic) tree?

Yes, structurally — a binary tree with nn nodes has n−1n-1 edges, is connected, and acyclic, satisfying the definition of a free tree once you forget the left/right labeling. But binary trees carry extra information a general tree does not: each child is explicitly tagged left or right, and a node with a single child is different depending on which side that child is on. This distinction matters enormously for algorithms — a Binary Search Tree relies on "left means smaller, right means larger," which requires exactly this left/right identity that plain unordered trees don't have.

Can a binary tree have a node with two children where one subtree is empty and the other isn't?

Yes — this is completely standard and is, in fact, the single most common way trees fail to be "full." A node can have only a left child, only a right child, or both, or neither; nothing in the basic definition forbids any of these four combinations. The "full" classification is precisely the restriction that rules out the two single-child cases, requiring every node to have 0 or 2 children. Ordinary, unconstrained binary trees (as used, say, in a freshly built BST) very frequently have single-child nodes and are not full.

Why does the empty tree get height -1 instead of height 0 or being left undefined?

The choice is purely a convention chosen to make formulas compose cleanly. If a single node has height 0, and height should equal 1+max⁡1 + \max(children's heights), then a leaf (whose children are both empty trees) needs 1+max⁡(h∅,h∅)=01 + \max(h_\varnothing, h_\varnothing) = 0, which forces h∅=−1h_\varnothing = -1. Some textbooks instead define the empty tree's height as undefined and special-case leaves directly; both approaches are logically equivalent, but the −1-1 convention lets a single recursive formula handle every case uniformly, which is why it's preferred in most treatments including this one.

Do complete binary trees have to be full?

No, and this is a common source of confusion. A complete binary tree only requires that all levels are filled except possibly the last, with the last level's nodes packed left. It is entirely possible for the very last node inserted at the last level to have only a left child, or only a right child if it's not the last one there — such a node has one child, violating fullness, while the tree as a whole is still complete. This is precisely the shape maintained by a Binary Heap during ordinary insertions and deletions: complete, but frequently not full.

How do binary search trees keep height close to log n in practice?

A plain binary search tree offers no such guarantee on its own — insertion order can degrade it to a linked list, as shown in the worked example. Self-balancing variants like AVL trees and red-black trees add extra rules enforced after every insertion or deletion: AVL trees require the heights of sibling subtrees to differ by at most 1, and restore this via rotations whenever it's violated; red-black trees use a weaker coloring invariant that still guarantees height is at most roughly 2log⁡2(n+1)2\log_2(n+1). Both approaches trade a small constant amount of extra bookkeeping per update for a hard worst-case O(log⁡n)O(\log n) guarantee on every subsequent search.

What's the practical difference between a binary tree and a binary heap?

A binary tree is the general shape; a binary heap is a specific, constrained use of that shape for priority-queue purposes. A binary heap is always a complete binary tree (guaranteeing height ⌊log⁡2n⌋\lfloor \log_2 n \rfloor), and additionally satisfies a heap-order invariant — every parent is smaller (min-heap) or larger (max-heap) than its children. Because it's complete, a heap can be stored implicitly in a plain array with no pointers at all, using arithmetic (`2i+1`, `2i+2`, `(i-1)/2`) to find children and parents — an efficiency trick unavailable to general binary trees, which typically need explicit left/right pointers.

Why do textbooks bother distinguishing full, complete, and perfect if they seem similar?

Because each property is used to guarantee a different algorithmic fact, and conflating them leads to incorrect proofs. "Perfect" is used to derive the exact node-count formula 2h+1−12^{h+1}-1, useful for capacity planning. "Complete" is the load-bearing property behind array-based heap storage and its O(log⁡n)O(\log n) height guarantee even while the heap is only partially filled. "Full" shows up in counting arguments — for instance, in a full binary tree, the number of leaves is always exactly one more than the number of internal nodes, a fact used in Huffman coding and expression-tree analysis. Using the wrong one in a proof silently invalidates the argument.

Before this

Where this leads