Skip to main content

Introduction to Binary Trees

Editorial

Twenty questions

Think of a game of twenty questions. The first question is "Is it alive?" A yes leads to one next question and a no to another, and each of those splits again, until a question has no follow-up and you make a guess. Drawn out, the questions form a binary tree: every question is a node, and each node has at most two children, a left one and a right one.

The words come from that picture, drawn upside down. The first question is the root. A node's children hang below it, joined to it by edges, and it's their parent. A node with no children is a leaf: here, a guess. Take any node with everything below it and you have a subtree, a smaller binary tree of its own.

That last point is the one this whole section rests on. A binary tree is either empty, or a root with a left tree and a right tree, and each of those is a binary tree too. So most code that works on a tree is a step at the root and two calls, one for each smaller tree.

Twenty questions also shows why trees matter. If every question really splits the possibilities in two, twenty levels of questions end in 2²⁰ = 1,048,576 leaves: twenty questions tell a million things apart. Asking "is it this one?" down a list would take up to a million.

A binary tree of seven nodes. The root at the top is ringed on its own; its left subtree, three nodes, sits in one shaded region and its right subtree, three nodes, in another. A focal line reads: a root, and two smaller trees.

Depth, height, and an off-by-one

Two measurements come up constantly, and they run in opposite directions. The depth of a node counts the edges from the root down to it: the root is at depth 0, its children at depth 1. The height of a node counts the edges on the longest path from it down to a leaf: every leaf has height 0, and the tree's height is its root's. A level is all the nodes at one depth.

Here's the trap. LeetCode's Maximum Depth of Binary Tree counts nodes, not edges: the number of nodes on the longest path from the root down to a leaf. A single node has maximum depth 1, while its height is 0. Same tree, two numbers one apart:

A binary tree of six nodes in four levels, 1 at the top, then 2 and 3, then 4 and 5, then 6. Depths 0 to 3 run down the left edge beside the levels. On the right, two brackets: the height, 3 edges along the longest path 1 to 2 to 4 to 6, and LeetCode's maximum depth, 4 nodes along the same path.
MeasureIt countsA single nodeThe tree above
Depth of a nodeedges from the root down to it00 to 3
Height of the treeedges on the longest path down03
LeetCode's maximum depthnodes on the longest path down14

Books give the empty tree a height of −1, so that a leaf's height, 1 + the larger of its two children's, comes out 0. LeetCode's maximum depth gives the empty tree 0, and a leaf 1. When a problem says depth or height, check its examples for which one it counts. In this section, maximum depth always means LeetCode's: levels, counted in nodes.

Shapes with names

Some shapes have names, because problems and other structures depend on them. Books don't all use the words the same way (one classic textbook says "complete" for a tree with every level full); these are the meanings interviews and LeetCode use:

ShapeThe ruleWhere you meet it
Fullevery node has 0 or 2 childrenexpression trees, decision trees
Completeevery level is full except perhaps the last, which fills from the leftheaps, stored in an array
Perfectevery level is full2ʰ⁺¹ − 1 nodes at height h
Height-balancedat every node, the two subtrees' heights differ by at most 1Balanced Binary Tree; AVL trees
Chainevery node has at most one childvalues inserted in sorted order into a plain BST

Balanced is a rule about every node, not only the root: a root whose two subtrees have equal heights can still hide a long chain inside one of them.

Two counts hold for every binary tree, and they're handy for checking your code. n nodes have n − 1 edges, one going up from every node except the root. They have 2n child links in all, n − 1 of them used by those edges, so n + 1 links are empty. And the height of n nodes sits between ⌊log₂ n⌋, when every level is full, and n − 1, when the tree is a chain. For a million nodes that's anywhere from 19 to 999,999, and that difference decides whether recursion is safe, as you'll see further down.

Two ways to store a tree

The usual way is linked nodes: each node holds its value and a link to each child, and the tree is a link to its root. That's what LeetCode gives you, a TreeNode with val, left and right, and a missing child is null (None in Python, nullptr in C++). It costs one node per value, whatever the shape.

A complete tree can live in a plain array instead, level by level from index 0, with no links at all: the children of index i sit at 2i + 1 and 2i + 2, and its parent at (i − 1) / 2, rounded down. Heaps are stored exactly this way (Heaps & Priority Queues):

tree = [1, 2, 3, 4, 5, 6, 7]       # a complete tree, level by level
i = 1                               # the node holding 2
left, right, parent = 2 * i + 1, 2 * i + 2, (i - 1) // 2
print(tree[left], tree[right], tree[parent])

It printed 4 5 1: index 1 holds 2, its children 4 and 5 sit at indexes 3 and 4, and its parent 1 at index 0. The catch is a tree that isn't complete. Every missing node still owns its slot, and the slots double with each level. A chain of 20 nodes, each the right child of the one before, puts its last node far out:

i = 0                               # the root
for _ in range(19):                 # 19 steps down, always to the right child
    i = 2 * i + 2
print(i + 1)                        # slots the array needs to hold the last node

That's 1,048,575 slots for 20 values. Linked nodes take any shape; the array is for complete trees.

How a problem hands you a tree

On LeetCode, and on our judge, a tree arrives as a list in level order: the root, then the next level from left to right, and so on down, with null wherever a node has no child on that side. [3, 9, 20, null, null, 15, 7] is a root 3 with children 9 and 20; 9 has no children, and 20 has 15 and 7.

Two rules keep the list short, and they're what trips people up. Only the children of real nodes are written, because a missing node has no children to list. And nulls at the very end are left off. To read it, keep a queue of the nodes waiting for their children. The one at the front takes the next entry as its left child and the one after as its right, and every real child joins the back of the queue:

Reading the list with a queue of parents — Amber is the parent taking this entry, violet the child just placed; a crossed circle is a null.

struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};

// A tree from LeetCode's level-order list; nullopt is a missing child.
TreeNode* build(const vector<optional<int>>& values) {
    if (values.empty() || !values[0]) return nullptr;
    TreeNode* root = new TreeNode(*values[0]);
    queue<TreeNode*> parents;                  // nodes still waiting for their children
    parents.push(root);
    size_t i = 1;
    while (!parents.empty() && i < values.size()) {
        TreeNode* parent = parents.front();
        parents.pop();
        if (values[i]) {                       // the next entry is the left child
            parent->left = new TreeNode(*values[i]);
            parents.push(parent->left);
        }
        i++;
        if (i < values.size() && values[i]) {  // the one after it, the right child
            parent->right = new TreeNode(*values[i]);
            parents.push(parent->right);
        }
        i++;
    }
    return root;
}

// The list back: level by level, null for each missing child, trailing nulls dropped.
string serialize(TreeNode* root) {
    vector<string> out;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front();
        q.pop();
        if (!node) {
            out.push_back("null");
            continue;
        }
        out.push_back(to_string(node->val));
        q.push(node->left);
        q.push(node->right);
    }
    while (!out.empty() && out.back() == "null") out.pop_back();
    string s = "[";
    for (size_t k = 0; k < out.size(); k++) s += (k ? ", " : "") + out[k];
    return s + "]";
}
from collections import deque


class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


def build(values):
    """A tree from LeetCode's level-order list; None is a missing child."""
    if not values or values[0] is None:
        return None
    root = TreeNode(values[0])
    parents = deque([root])                    # nodes still waiting for their children
    i = 1
    while parents and i < len(values):
        parent = parents.popleft()
        if values[i] is not None:              # the next entry is the left child
            parent.left = TreeNode(values[i])
            parents.append(parent.left)
        i += 1
        if i < len(values) and values[i] is not None:   # the one after it, the right child
            parent.right = TreeNode(values[i])
            parents.append(parent.right)
        i += 1
    return root


def serialize(root):
    """The list back: level by level, None for each missing child, trailing Nones dropped."""
    out, queue = [], deque([root])
    while queue:
        node = queue.popleft()
        if node is None:
            out.append(None)
            continue
        out.append(node.val)
        queue.append(node.left)
        queue.append(node.right)
    while out and out[-1] is None:
        out.pop()
    return out
static class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int val) { this.val = val; }
}

// A tree from LeetCode's level-order list; null is a missing child.
static TreeNode build(Integer[] values) {
    if (values.length == 0 || values[0] == null) return null;
    TreeNode root = new TreeNode(values[0]);
    Deque<TreeNode> parents = new ArrayDeque<>();   // nodes still waiting for their children
    parents.add(root);
    int i = 1;
    while (!parents.isEmpty() && i < values.length) {
        TreeNode parent = parents.poll();
        if (values[i] != null) {                      // the next entry is the left child
            parent.left = new TreeNode(values[i]);
            parents.add(parent.left);
        }
        i++;
        if (i < values.length && values[i] != null) { // the one after it, the right child
            parent.right = new TreeNode(values[i]);
            parents.add(parent.right);
        }
        i++;
    }
    return root;
}

// The list back: level by level, null for each missing child, trailing nulls dropped.
static String serialize(TreeNode root) {
    List<String> out = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();       // it holds nulls, which an ArrayDeque refuses
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        if (node == null) {
            out.add("null");
            continue;
        }
        out.add(String.valueOf(node.val));
        queue.add(node.left);
        queue.add(node.right);
    }
    while (!out.isEmpty() && out.get(out.size() - 1).equals("null")) out.remove(out.size() - 1);
    return "[" + String.join(", ", out) + "]";
}
class TreeNode {
    constructor(val, left = null, right = null) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

// A tree from LeetCode's level-order list; null is a missing child.
function build(values) {
    if (values.length === 0 || values[0] === null) return null;
    const root = new TreeNode(values[0]);
    const parents = [root];                // nodes still waiting for their children
    let head = 0;                          // the front of that queue, with no shift()
    let i = 1;
    while (head < parents.length && i < values.length) {
        const parent = parents[head++];
        if (values[i] !== null) {                       // the next entry is the left child
            parent.left = new TreeNode(values[i]);
            parents.push(parent.left);
        }
        i++;
        if (i < values.length && values[i] !== null) {  // the one after it, the right child
            parent.right = new TreeNode(values[i]);
            parents.push(parent.right);
        }
        i++;
    }
    return root;
}

// The list back: level by level, null for each missing child, trailing nulls dropped.
function serialize(root) {
    const out = [];
    const queue = [root];
    for (let head = 0; head < queue.length; head++) {
        const node = queue[head];
        if (node === null) {
            out.push(null);
            continue;
        }
        out.push(node.val);
        queue.push(node.left, node.right);
    }
    while (out.length && out[out.length - 1] === null) out.pop();
    return "[" + out.map((v) => (v === null ? "null" : v)).join(", ") + "]";
}

Each one built three trees and printed them back as lists: [1, 2, 3, null, 4, 5, null, 6], [1, null, 2, 3], [1, 2, null, 3]. The last input was [1, 2, null, 3, null, null, null]: its trailing nulls are dropped on the way back, which is how LeetCode writes it. Building takes one step per entry, O(n), and the queue holds at most one level's worth of nodes.

Reading the list as the array from the last section, with the children of index i at 2i + 1 and 2i + 2, gets [3, 9, 20, null, null, 15, 7] right by luck: 9's missing children are written out, so every node lands where the array would put it. It breaks as soon as a missing node would have had children after it. In [1, 2, 3, null, 4, 5, null, 6], the 6 is at index 7, and 2 × 3 + 1 = 7 makes it a child of index 3, which is a null. Its real parent is 4, the next node waiting in the queue:

The list 1, 2, 3, null, 4, 5, null, 6 drawn twice. Read with a queue, it builds 1 with children 2 and 3, 4 under 2 on the right, 5 under 3 on the left, and 6 under 4. Read as an array with children at 2i + 1 and 2i + 2, the 6 at index 7 hangs under index 3, a null, and is lost, crossed out in red.

Now you build the tree

Below, you are the queue. The next entry of the list waits at the pointer; click the place it goes under its parent. The front parent takes the left place first, then the right, and a null leaves its place empty. Put one in the wrong place and you're told which parent is first in line, and why. Try the examples, then type a list of your own.

Build the Tree: place each entry of a level-order list under the right parent

Recursion follows the shape

Since every node is the root of a smaller tree, most tree code has one shape: an answer for the empty tree, and for a node, an answer made from its two subtrees' answers. The number of nodes is 1 + the left subtree's count + the right's, with 0 for an empty tree. The sum of the values is the node's value plus the two sums. And LeetCode's maximum depth is 1 + the larger of the two depths, with 0 for an empty tree:

// LeetCode's maximum depth: the nodes on the longest path from the root down.
int maxDepth(TreeNode* node) {
    if (node == nullptr) return 0;           // an empty tree has no levels
    return 1 + max(maxDepth(node->left), maxDepth(node->right));
}
def max_depth(node):
    """LeetCode's maximum depth: the nodes on the longest path from the root down."""
    if node is None:                         # an empty tree has no levels
        return 0
    return 1 + max(max_depth(node.left), max_depth(node.right))
// LeetCode's maximum depth: the nodes on the longest path from the root down.
static int maxDepth(TreeNode node) {
    if (node == null) return 0;              // an empty tree has no levels
    return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));
}
// LeetCode's maximum depth: the nodes on the longest path from the root down.
function maxDepth(node) {
    if (node === null) return 0;             // an empty tree has no levels
    return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));
}

Each printed 3 2 0 on the problem's examples, [3, 9, 20, null, null, 15, 7], [1, null, 2] and the empty tree. Every node is visited once, so it's O(n) time. The calls open at any moment are the path from the root to the node being worked on, so the extra space is O(h) for a tree of height h. Watch the answers come back up:

Maximum depth, answered from the leaves up — Indigo is the call being worked on, amber the calls waiting on it, violet the nodes that have answered, with the answer on their badge.

A node knows its answer only after both its children have answered, so the answers come back from the leaves up: 9, 15 and 7 first, then 20, then the root. That order, children before their parent, is called postorder, and Tree Traversals: Depth-First and Breadth-First is about it and the orders beside it.

Invert: three famous lines

In June 2015, Max Howell, who wrote the package manager Homebrew, posted that Google had turned him down after he couldn't invert a binary tree on a whiteboard, though most of its engineers used his software. Invert Binary Tree asks for the mirror image: every node's left and right children swap places. It's the same recursion: invert both subtrees, then swap them.

// The mirror image: every node's two children swap places.
TreeNode* invert(TreeNode* node) {
    if (node == nullptr) return nullptr;
    TreeNode* left = invert(node->left);     // both answers first,
    TreeNode* right = invert(node->right);
    node->left = right;                       // then swap them in
    node->right = left;
    return node;
}
def invert(node):
    """The mirror image: every node's two children swap places."""
    if node is None:
        return None
    left = invert(node.left)                 # both answers first,
    right = invert(node.right)
    node.left = right                        # then swap them in
    node.right = left
    return node
// The mirror image: every node's two children swap places.
static TreeNode invert(TreeNode node) {
    if (node == null) return null;
    TreeNode left = invert(node.left);       // both answers first,
    TreeNode right = invert(node.right);
    node.left = right;                        // then swap them in
    node.right = left;
    return node;
}
// The mirror image: every node's two children swap places.
function invert(node) {
    if (node === null) return null;
    const left = invert(node.left);          // both answers first,
    const right = invert(node.right);
    node.left = right;                        // then swap them in
    node.right = left;
    return node;
}

Each printed [4, 7, 2, 9, 6, 3, 1], [2, 3, 1] and [] for the problem's examples. It visits each node once, O(n), with O(h) calls open. Holding both answers before assigning either is the part that matters, as a bug below shows:

Invert, from the bottom up — Indigo is the node swapping its subtrees; orange marks the two children that just traded places, each bringing its subtree along.

When the tree is a long chain

The calls open at once are as many as the tree has levels, and a chain has as many levels as it has nodes. Every language allows only so many nested calls. On our judge, here's the recursive maximum depth above on a chain of nodes that each have only a right child:

Language1,000 nodes100,000 nodesGives out at
C++1000100000never here: 1,000,000 passed
PythonRecursionErrorRecursionError998 nodes (997 passed)
Java1000StackOverflowError10,500 to 13,600 nodes, by run
JavaScript1000RangeErrorabout 8,400 nodes

Python counts calls against a limit of 1,000. sys.setrecursionlimit(10**6) lifts it, and 100,000 nodes then passed, but at 300,000 the open calls ran the program out of memory. Java and JavaScript stop when the thread's stack is full, and Java's point moves from run to run: the JVM compiles a busy method to machine code part-way through, and compiled calls take less stack. LeetCode's tree problems go up to 10⁴ or 10⁵ nodes, and a test made of one long chain is a legal input.

The fix that works in every language is to keep the waiting work yourself, on a stack of (node, depth) pairs:

// The same maximum depth, with the waiting work on a stack of (node, depth) pairs.
int maxDepth(TreeNode* root) {
    int best = 0;
    vector<pair<TreeNode*, int>> stack;
    if (root) stack.push_back({root, 1});
    while (!stack.empty()) {
        auto [node, depth] = stack.back();
        stack.pop_back();
        best = max(best, depth);
        if (node->left) stack.push_back({node->left, depth + 1});
        if (node->right) stack.push_back({node->right, depth + 1});
    }
    return best;
}
def max_depth(root):
    """The same maximum depth, with the waiting work on a stack of (node, depth) pairs."""
    best = 0
    stack = [(root, 1)] if root else []
    while stack:
        node, depth = stack.pop()
        best = max(best, depth)
        if node.left:
            stack.append((node.left, depth + 1))
        if node.right:
            stack.append((node.right, depth + 1))
    return best
// The same maximum depth, with the waiting work on a stack of (node, depth) pairs.
static int maxDepth(TreeNode root) {
    int best = 0;
    Deque<TreeNode> nodes = new ArrayDeque<>();
    Deque<Integer> depths = new ArrayDeque<>();
    if (root != null) {
        nodes.push(root);
        depths.push(1);
    }
    while (!nodes.isEmpty()) {
        TreeNode node = nodes.pop();
        int depth = depths.pop();
        best = Math.max(best, depth);
        if (node.left != null) {
            nodes.push(node.left);
            depths.push(depth + 1);
        }
        if (node.right != null) {
            nodes.push(node.right);
            depths.push(depth + 1);
        }
    }
    return best;
}
// The same maximum depth, with the waiting work on a stack of (node, depth) pairs.
function maxDepth(root) {
    let best = 0;
    const stack = root ? [[root, 1]] : [];
    while (stack.length) {
        const [node, depth] = stack.pop();
        best = Math.max(best, depth);
        if (node.left) stack.push([node.left, depth + 1]);
        if (node.right) stack.push([node.right, depth + 1]);
    }
    return best;
}

Each printed 3 2 0 on the examples and then 1000000 for a chain of a million nodes. It's the same O(n) time and O(h) memory, but the memory is ordinary heap memory, where there's room.

Spotting it, and when it breaks

The clues. "The root of a binary tree", a TreeNode with left and right, an input written as a level-order list with nulls, "each node has at most two children". Anything that splits in two at every step: yes-or-no decisions, a knockout tournament, an expression whose operators each take two operands.

The near-misses. A node with any number of children is an N-ary tree: the same recursion, with a loop over the children. A binary tree with smaller values on the left and bigger on the right is a binary search tree, the next section, and that order is a promise you can use. Links that can loop back, or give a node two parents, make a graph (Graphs). A complete tree kept in an array with a rule between parent and child is a heap (Heaps & Priority Queues).

When it breaks. Recursion goes as deep as the tree is tall, and a chain is as tall as it is long (the table above). The array layout wastes slots that double per level on a sparse tree. And code that changes the tree while it recurses has to read what it needs before it writes.

Bugs that cost a submission

Reading the list with 2i + 1.

def build_by_index(values, i=0):
    """Wrong: reads LeetCode's list as if the children of i were at 2i + 1 and 2i + 2."""
    if i >= len(values) or values[i] is None:
        return None
    node = TreeNode(values[i])
    node.left = build_by_index(values, 2 * i + 1)
    node.right = build_by_index(values, 2 * i + 2)
    return node

On [1, 2, 3, null, 4, 5, null, 6] it built [1, 2, 3, null, 4, 5] and gave a maximum depth of 3 instead of 4: the 6 landed under a null and vanished. Read the list with a queue of parents.

Counting edges where the problem counts nodes.

def max_depth(node):
    if node is None:
        return -1                    # wrong: counts edges, the problem counts nodes
    return 1 + max(max_depth(node.left), max_depth(node.right))

On [3, 9, 20, null, null, 15, 7] it returned 2; the answer is 3. When the problem counts nodes, the empty tree is 0.

Swapping one side at a time.

def invert(node):
    if node is None:
        return None
    node.left = invert(node.right)   # wrong: overwrites node.left ...
    node.right = invert(node.left)   # ... then inverts what it just wrote
    return node

On [4, 2, 7, 1, 3, 6, 9] it returned [4, 7, 7, 9, 9, 9, 9]. The second line inverted the subtree the first line had just put on the left, so every node ended with its old right subtree on both sides, and 2, 1, 3 and 6 were lost. Compute both answers, then assign.

No case for the empty tree.

def max_depth(node):                 # wrong: no case for the empty tree
    return 1 + max(max_depth(node.left), max_depth(node.right))

On [3, 9, 20, null, null, 15, 7] it crashed with AttributeError: 'NoneType' object has no attribute 'left', as soon as a leaf asked its missing children. The empty tree is a real input, and it's where the recursion stops.

Recursion on a chain. Fine on a balanced tree, and an error from about a thousand levels down in Python, or several thousand in JavaScript and Java (the table above). Use a stack of pairs, or, in Python, raise the limit.

Check yourself

Quiz

1 / 6

Multiple choice

What is LeetCode's maximum depth of [3, 9, 20, null, null, 15, 7]?

Practice problems

In this order:

  1. Tree Data Structure Basics: the root, parents, children and leaves of a tree given as edges.
  2. Binary Tree Representation: the same binary tree as an array and as linked nodes.
  3. Binary Tree Representation (Java): a complete tree of seven built from its level order.
  4. Maximum Depth of Binary Tree: the recursion on the shape, counting nodes.
  5. Invert Binary Tree: swap the children, everywhere, both answers first.

Next, Tree Traversals: Depth-First and Breadth-First: the orders a tree can be walked in, and what each one is for.