8.5 Tree Height
Tree height measures how far a tree extends downward from a node.
8.5 Tree Height
Tree height measures how far a tree extends downward from a node. It is one of the first values you learn to compute on a tree, but it also appears inside more advanced algorithms. Balanced-tree checks, diameter computation, expression-tree evaluation cost, recursion-depth analysis, and many tree dynamic programming problems all depend on height.
The main idea is recursive: the height of a node depends on the maximum height among its children.
Problem
You need to compute the height of a tree, or the height of every subtree.
Solution
For a rooted tree represented as child lists, compute height with a postorder DFS.
package main
import "fmt"
func height(tree [][]int, node int) int {
best := 0
for _, child := range tree[node] {
childHeight := height(tree, child)
if childHeight > best {
best = childHeight
}
}
return best + 1
}
func main() {
tree := [][]int{
{1, 2},
{3, 4},
{},
{},
{},
}
fmt.Println(height(tree, 0))
}
The output is:
3
The root has height 3 when height is counted in nodes.
Some algorithms count height in edges instead. Under that convention, a leaf has height 0, and an empty child contributes -1.
func heightEdges(tree [][]int, node int) int {
best := -1
for _, child := range tree[node] {
childHeight := heightEdges(tree, child)
if childHeight > best {
best = childHeight
}
}
return best + 1
}
For the same tree, this returns:
2
Choose one convention and keep it consistent.
Discussion
There are two common definitions of height:
| Convention | Leaf Height | Root Height in Sample |
|---|---|---|
| Count nodes | 1 | 3 |
| Count edges | 0 | 2 |
Both are valid. The node-counting version is often convenient for recursive formulas because every non-empty subtree has height at least 1. The edge-counting version is common in graph theory because distance usually counts edges.
The recurrence is the same in both cases:
height(node) = 1 + max(height(child))
The only difference is the base value used for a node with no children.
For node-counting height, the maximum over an empty child list is treated as 0.
For edge-counting height, the maximum over an empty child list is treated as -1.
Computing All Heights
If you need the height of every subtree, store the result during DFS.
func allHeights(tree [][]int, root int) []int {
heights := make([]int, len(tree))
var dfs func(int)
dfs = func(node int) {
best := 0
for _, child := range tree[node] {
dfs(child)
if heights[child] > best {
best = heights[child]
}
}
heights[node] = best + 1
}
dfs(root)
return heights
}
For this tree:
0
├── 1
│ ├── 3
│ └── 4
└── 2
the heights are:
node 0: 3
node 1: 2
node 2: 1
node 3: 1
node 4: 1
The algorithm computes child heights before parent heights, which makes it a postorder traversal.
Undirected Trees
When the input is an undirected tree, impose a root during traversal and pass the parent.
func heightUndirected(adj [][]int, node, parent int) int {
best := 0
for _, next := range adj[node] {
if next == parent {
continue
}
childHeight := heightUndirected(adj, next, node)
if childHeight > best {
best = childHeight
}
}
return best + 1
}
The parent guard prevents the recursion from walking back along the edge it just used.
This version computes height relative to the chosen root. If you choose a different root, subtree relationships change, and the height value may change as well.
Height of a Binary Tree
For pointer-based binary trees, the same recurrence becomes simpler because every node has at most two children.
type Node struct {
Value int
Left *Node
Right *Node
}
func heightBinary(root *Node) int {
if root == nil {
return 0
}
leftHeight := heightBinary(root.Left)
rightHeight := heightBinary(root.Right)
if leftHeight > rightHeight {
return leftHeight + 1
}
return rightHeight + 1
}
Here an empty tree has height 0, and a leaf has height 1.
This convention works well for many binary-tree algorithms.
Iterative Height Computation
Recursive height computation is compact, but an iterative version is useful when the tree may be very deep.
For a rooted tree, use an explicit stack to produce a postorder sequence.
func heightIterative(tree [][]int, root int) int {
order := []int{}
stack := []int{root}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
order = append(order, node)
for _, child := range tree[node] {
stack = append(stack, child)
}
}
heights := make([]int, len(tree))
for i := len(order) - 1; i >= 0; i-- {
node := order[i]
best := 0
for _, child := range tree[node] {
if heights[child] > best {
best = heights[child]
}
}
heights[node] = best + 1
}
return heights[root]
}
The first pass records a traversal order. Reversing that order ensures children are processed before parents.
This method avoids recursive call-stack growth.
Using Height to Check Balance
A binary tree is height-balanced if the left and right subtrees of every node differ in height by at most one.
A direct implementation computes height repeatedly and may become quadratic.
func isBalancedSlow(root *Node) bool {
if root == nil {
return true
}
leftHeight := heightBinary(root.Left)
rightHeight := heightBinary(root.Right)
if abs(leftHeight-rightHeight) > 1 {
return false
}
return isBalancedSlow(root.Left) && isBalancedSlow(root.Right)
}
The problem is that heightBinary is called again for the same subtrees many times.
A better implementation combines height computation and balance checking in one traversal.
func balancedHeight(root *Node) (int, bool) {
if root == nil {
return 0, true
}
leftHeight, leftOK := balancedHeight(root.Left)
if !leftOK {
return 0, false
}
rightHeight, rightOK := balancedHeight(root.Right)
if !rightOK {
return 0, false
}
if abs(leftHeight-rightHeight) > 1 {
return 0, false
}
if leftHeight > rightHeight {
return leftHeight + 1, true
}
return rightHeight + 1, true
}
func isBalanced(root *Node) bool {
_, ok := balancedHeight(root)
return ok
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
This version visits every node once.
Complexity Analysis
For a tree with n nodes:
| Algorithm | Time | Extra Space |
|---|---|---|
| Recursive height | O(n) | O(h) |
| Store all heights | O(n) | O(n) |
| Iterative height | O(n) | O(n) |
| Slow balance check | O(n²) worst case | O(h) |
| One-pass balance check | O(n) | O(h) |
Here h is the height of the tree.
For a balanced tree, h = O(log n). For a degenerate tree, h = O(n).
Common Mistakes
The most common mistake is mixing height conventions. If one function treats a leaf as height 1 and another treats it as height 0, later formulas will be off by one.
Another mistake is computing height repeatedly inside a larger recursive algorithm. When a result is needed more than once, return it alongside the other information or store it in an array.
A third mistake is assuming height is a property of an unrooted tree without specifying the root. In an undirected tree, height depends on which node you choose as the root.
Takeaway
Tree height is a postorder computation. Each node waits for its children, takes the maximum child height, and adds one. The algorithm is simple, but the convention matters: decide whether height counts nodes or edges, then use that convention everywhere. When height is part of a larger algorithm, compute it in the same traversal instead of recomputing it from scratch.