Lesson 12 / 26
Binary Trees
The at-most-two-children constraint, the definitions of full and complete trees, the height–node count relationship, and array representation.
Contents
In a general tree, how many children a node will have is unpredictable; this complicates both representation and cost analysis. A binary tree limits branching to at most two: every node can have a left child and a right child, or neither.
The constraint is not merely a simplification. Two children are the natural counterpart of binary decisions in the form of “greater or smaller,” “left or right,” “yes or no”; this is why search trees, decision trees, and expression trees are always binary.
Left and Right Are Separate Information
In a general tree, the order of children is usually meaningless. In a binary tree, though, left and right are separate positions: whether a node’s single child is on the left or the right is part of the structure.
This distinction gains meaning in later lessons: in a search tree, the left carries smaller values and the right carries larger ones; in an expression tree representing subtraction, swapping left and right changes the result.
Shape Classes
Three definitions are commonly used and commonly confused.
Full binary tree: Every node has either zero or two children; no node has a single child.
Complete binary tree: Every level except possibly the last is entirely full, and the nodes on the last level are packed from left to right. A gap can occur only at the right end.
Perfect binary tree: Every level is entirely full; leaves are all at the same depth.
full but not complete complete but not perfect perfect
(a) (a) (a)
/ \ / \ / \
(b) (c) (b) (c) (b) (c)
/ \ / \ / / \ / \
(d) (e) (d) (e)(f) (d) (e)(f) (g)
A perfect tree is both full and complete. The concept of a complete tree will be the foundation of the heap structure in later lessons: gaps occurring only at the right end is what makes it possible to fit the tree into an array.
Height and Node Count
In a binary tree, level holds at most nodes ( being the depth). Two bounds follow from this.
The maximum number of nodes in a binary tree of height :
Read in reverse, the minimum height of a tree with nodes:
In the worst case, every node has a single child and the height is .
These two bounds quantify the previous lesson’s closing question. A binary tree with one thousand nodes has a height around nine if balanced, and nine hundred ninety-nine if degenerate. Since search cost is proportional to height, the difference between the two is more than a hundredfold.
Linked Representation
The general representation is every node holding two links:
class BinaryNode: def __init__(self, value: int) -> None: self.value = value self.left: "BinaryNode | None" = None self.right: "BinaryNode | None" = None def node_count(node: BinaryNode | None) -> int: if node is None: return 0 return 1 + node_count(node.left) + node_count(node.right) def height(node: BinaryNode | None) -> int: if node is None: return -1 # an empty tree's height is treated as -1 return 1 + max(height(node.left), height(node.right)) def is_full(node: BinaryNode | None) -> bool: if node is None: return True if (node.left is None) != (node.right is None): return False # a single-child node was found return is_full(node.left) and is_full(node.right) root = BinaryNode(12) root.left = BinaryNode(18) root.right = BinaryNode(7) root.left.left = BinaryNode(25) root.left.right = BinaryNode(14) print(node_count(root), height(root)) # 5 2 print(is_full(root)) # True root.right.left = BinaryNode(30) # a single child added to node 7 print(is_full(root)) # False
Treating an empty tree’s height as is a convention; a single-node tree’s height then comes out to , staying consistent with the definition from the previous lesson.
Array Representation
If the tree is complete, links are not needed at all. Nodes are placed into an array level by level, from left to right; the relationship is computed with index arithmetic:
tree = [12, 18, 7, 25, 14, 30] # complete binary tree, in level order def left(i: int) -> int: return 2 * i + 1 def right(i: int) -> int: return 2 * i + 2 def parent(i: int) -> int: return (i - 1) // 2 print(tree[0], tree[left(0)], tree[right(0)]) # 12 18 7 print(tree[left(1)], tree[right(1)]) # 25 14 print(tree[parent(5)], tree[parent(4)]) # 7 18
The advantages of this representation are the same as the array discussion in the first topic: there is no pointer overhead, and because nodes sit contiguously, cache behavior is good.
Its constraint is that the tree must be complete. If the tree is sparse — a degenerate chain, for example — the array representation requires slots and most of it stays empty. For this reason the array representation is used in structures whose shape is guaranteed; the heap is the main example of this.
What “Balanced” Means
The word “balanced” does not have a single definition. Three criteria are common: the difference between subtree heights at every node being bounded, the ratio of subtree sizes at every node being bounded, and leaves being at the same depth.
All three serve the same purpose — keeping height logarithmic — but different structures choose different criteria. All three will appear in later lessons: height difference in AVL trees, black node count in red–black trees, and equal depth in 2-3 trees.
The Number of Binary Trees
A side note shows how varied tree shapes can be: the number of distinct binary tree shapes with nodes grows quickly — there are five distinct shapes with three nodes and fourteen with four. These numbers are known as Catalan numbers and are the solution to a counting problem: once a root is chosen for nodes, every way of dividing the remaining nodes between the left and right subtrees produces a distinct shape, so the count grows by roughly a factor of four per node.
The practical consequence is this: the same data set can be stored in very different shapes depending on insertion order, and shape determines cost. Only a small fraction of these shapes are balanced; relying on a random shape means staying exposed to the worst case. This is the rationale behind the concept of balancing.
Summary
- In a binary tree every node has at most two children, and left and right are separate positions.
- A full tree has no single-child node; a complete tree has a gap only at the right end of the last level; a perfect tree has every level full.
- A tree of height holds at most nodes; the minimum height of an -node tree is logarithmic, and linear in the worst case.
- In the linked representation every node holds two links; an empty tree’s height is counted as .
- Complete trees can be represented with an array: left , right , parent .
- The same data set can be stored in many different tree shapes; shape determines cost.
Next Step
The tree has been built, but it has not yet been traversed. There is more than one way to visit all the nodes of a tree, and which way is chosen determines the ordering obtained. The next lesson takes up the four basic traversal orders and which problem each is used for.
To keep your progress and take notes, Log in
My notes
Log in to take notes.