~/graphs/trees
Binary trees and traversals
Visit every node of a binary tree in preorder, inorder, postorder or level order, with recursion, a stack or a queue.
Four fixed orders for visiting a binary tree. Pre, in and post differ only in when a node is output relative to its subtrees; level order goes row by row.
Any problem handed a tree: sorted output from a BST, copying or serializing a tree, answers built up from the children, anything per level.
O(n)
O(h) for the stack, O(w) for the queue
You’ll recognise it when
- The input is a
TreeNodewithleftandright, or a list like[5, 3, 8, 1, 4, null, 9]that describes one. - You need the values of a binary search tree in sorted order, or the k-th smallest: that’s inorder.
- The answer for a node depends on answers from its children: height, diameter, “is it balanced”, subtree sums. That’s postorder, usually written as recursion.
- The question says “per level”: the average of each row, the rightmost node you’d see, zigzag order. That’s level order with a queue.
- You need to copy or save a tree and rebuild it later: preorder with markers for missing children.
Level order is BFS and the other three are DFS; a tree is just a graph with no cycles, so you never need a visited set.
The idea
A binary tree is a set of nodes where each node has at most two children, a left and a right. The top node is the root, nodes with no children are leaves, and a node plus everything below it is a subtree. A node’s depth is how many edges it sits below the root; the tree’s height is the largest depth.
Every traversal visits every node once. What differs is when you output a node compared with its two subtrees. Picture walking around the outside of the tree, starting at the root and keeping the tree on your left hand. You pass each node three times: on the way down, underneath it (between its left and right subtrees), and on the way back up.
- Preorder outputs a node the first time you pass it: node, left, right.
- Inorder outputs it the second time: left, node, right.
- Postorder outputs it the last time: left, right, node.
- Level order ignores the walk and reads the tree row by row, top to bottom.
In a binary search tree (BST), everything in a node’s left subtree is smaller and everything in its right subtree is bigger. Inorder visits the left side, then the node, then the right side, so on a BST it reads the values in sorted order.
How it works
Recursively, inorder is three lines: traverse the left subtree, output the node, traverse the right subtree. The iterative version does the same thing with its own stack instead of the call stack, which matters on deep trees. The graphic runs it on a BST of 9 nodes, laid out so each node has its own column; scroll through the steps and it follows along.
- Start with
nodeat the root, 6. Thestackholds nodes whose left side isn’t finished yet; it starts empty. - Go left as far as you can. Push each node and step to its left child: 6, 3, 1. When 1 has no left child,
nodebecomes None. nodeis None, so nothing is left to the left. Pop the top of thestack: 1.- Output it. 1 is the smallest value, and it comes first. The number next to each node is its place in
out. - Then turn to its right subtree:
node= 2. The loop starts over from there: push 2, go left (nothing), pop it, output it. - 2 has no right child, so
nodeis None and the next pop goes back up to 3. Everything left of 3 is already inout. nodemoves to 3’s right child, 5. Before 5 comes out, the loop walks left again to 4. Notice the output row: every value drops straight down under its own node.- The
stackis empty andnodeis None: done.out= [1, 2, 3, 4, 5, 6, 8, 9, 11], sorted.
Why it’s right: a node is popped only after the inner loop has pushed and finished everything to its left, and the stack always holds the path of ancestors still waiting for their left side to finish, deepest on top. So each node comes out after its left subtree and before its right one, which is exactly inorder.
Now open edit and try order = pre, post and level. Same tree, different rule for when a node leaves:
- pre: pop a node and output it at once, then push its right child and then its left, so the left one is on top and goes first. Output: 6 3 1 2 5 4 9 8 11.
- post: like inorder, but a node stays on the
stackuntil its right subtree is also done. Output: 2 1 4 5 3 8 11 9 6, root last. - level: a queue instead of a stack. Taking exactly
len(queue)nodes per round gives one level per round: [6], [3, 9], [1, 5, 8, 11], [2, 4].
from collections import deque
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
def inorder(root):
"""Values in left, node, right order, with an explicit stack."""
out, stack = [], []
node = root
while node or stack:
while node: # go as far left as possible
stack.append(node)
node = node.left
node = stack.pop()
out.append(node.val)
node = node.right
return out
def level_order(root):
"""Values level by level, each level left to right."""
levels = []
queue = deque([root] if root else [])
while queue:
level = []
for _ in range(len(queue)): # exactly the nodes of this level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
levels.append(level)
return levels#include <deque>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode* left = nullptr;
TreeNode* right = nullptr;
explicit TreeNode(int v) : val(v) {}
};
// Values in left, node, right order, with an explicit stack.
vector<int> inorder(TreeNode* root) {
vector<int> out;
vector<TreeNode*> stack;
TreeNode* node = root;
while (node || !stack.empty()) {
while (node) { // go as far left as possible
stack.push_back(node);
node = node->left;
}
node = stack.back();
stack.pop_back();
out.push_back(node->val);
node = node->right;
}
return out;
}
// Values level by level, each level left to right.
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> levels;
deque<TreeNode*> queue;
if (root) queue.push_back(root);
while (!queue.empty()) {
vector<int> level;
size_t size = queue.size(); // exactly the nodes of this level
for (size_t i = 0; i < size; i++) {
TreeNode* node = queue.front();
queue.pop_front();
level.push_back(node->val);
if (node->left) queue.push_back(node->left);
if (node->right) queue.push_back(node->right);
}
levels.push_back(level);
}
return levels;
}import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}
class Traversals {
// Values in left, node, right order, with an explicit stack.
static List<Integer> inorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode node = root;
while (node != null || !stack.isEmpty()) {
while (node != null) { // go as far left as possible
stack.push(node);
node = node.left;
}
node = stack.pop();
out.add(node.val);
node = node.right;
}
return out;
}
// Values level by level, each level left to right.
static List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> levels = new ArrayList<>();
Deque<TreeNode> queue = new ArrayDeque<>();
if (root != null) queue.add(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<>();
int size = queue.size(); // exactly the nodes of this level
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
levels.add(level);
}
return levels;
}
}Most tree problems are solved recursively. Ask: what do I need from each child? Write a function that takes a node, gets an answer from its left child and from its right child, and combines them. An empty child returns a base value. For the height (number of nodes on the longest path down):
def height(node):
if node is None:
return 0
return 1 + max(height(node.left), height(node.right))
Sometimes the answer isn’t what the parent needs. The diameter is the longest path between any two nodes, in edges. The longest path through a node goes down both sides, so it’s left_height + right_height, but the parent can only extend one side, so the function returns the height and keeps the best path in a separate variable:
def diameter(root):
best = 0
def height(node):
nonlocal best
if node is None:
return 0
l, r = height(node.left), height(node.right)
best = max(best, l + r) # the longest path bending at this node
return 1 + max(l, r) # what the parent needs
height(root)
return best
Both are postorder: a node’s work happens after both children have reported back.
Why it’s O(n)
Each node is pushed once and popped once (or enqueued and dequeued once), with O(1) work each time, so every traversal is O(n) for n nodes.
The extra space is the frontier. The stack holds one path from the root, so it’s O(h) where h is the height: about log₂ n for a balanced tree, but n for a tree that’s one long chain. The queue holds at most one full level plus part of the next, O(w) where w is the widest level, up to about n/2 for a complete tree.
| Traversal | Time | Extra space |
|---|---|---|
| Pre / in / post, stack or recursion | O(n) | O(h): O(log n) balanced, O(n) a chain |
| Level order, queue | O(n) | O(w): up to about n/2 |
Common mistakes
Recursion on a deep tree
Inserting sorted keys into a plain BST builds a chain, and a recursive traversal then goes one call deeper per node. Python stops at about 1,000 calls. Use the explicit stack version, or level order.
def walk(node): ...walk(node.left)... # ✗ RecursionError on a 10⁵-node chain
stack.append(node); node = node.left # ✓ the depth lives in a list
Checking a BST against the children only
A BST needs every value in the left subtree to be smaller, not just the left child. In [5, 1, 6, null, null, 3, 7], each parent-child pair looks fine, but 3 sits right of 5. Pass bounds down, or check that inorder is strictly increasing.
node.left.val < node.val < node.right.val # ✗ misses deeper values
lo < node.val < hi # ✓ bounds from all ancestors
Returning the answer instead of what the parent needs
In diameter, the parent needs a height, but the answer is a path length. Returning l + r gives the parent a meaningless number and the result is wrong.
return l + r # ✗ a path, not a height
return 1 + max(l, r) # ✓ height up; best = max(best, l + r) on the side
Pushing children in the wrong order for preorder
A stack hands back the last thing pushed. Push the left child last so it’s explored first.
stack += [node.left, node.right] # ✗ right subtree comes out first
stack += [node.right, node.left] # ✓ left on top
Variations
- Maximum depth.
1 + max(depth(left), depth(right)), with 0 for an empty tree. Or count the rounds of a level-order traversal. - Diameter and friends. Return one thing to the parent (a height, a best single-branch sum) and keep the overall best in a separate variable. The same shape solves “maximum path sum” and “longest path with equal values”.
- Validate a BST. Pass
(lo, hi)bounds down: going left setshito the node’s value, going right setslo. Or run inorder and check each value is bigger than the one before. - Lowest common ancestor. Recurse: return the node if it is one of the two targets, otherwise ask both children. If both sides return something, this node is the answer; else pass up whichever side found one. In a BST, just walk down from the root toward both values until they split.
- Serialize and deserialize. Write preorder with a marker for every missing child (
1 2 # # 3 # #). The markers pin down the shape, so reading the list back in the same order, one token at a time, rebuilds the exact tree in O(n).
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
You’re given the root of a binary search tree and asked for its 3rd smallest value. Which approach fits best?
Inorder visits a BST’s values in sorted order, so the k-th node it outputs is the k-th smallest, and the iterative version can stop early after O(h + k) work. Level order and preorder follow the tree’s shape, not the values: the 3rd node in those orders can be anywhere in the sorted list. Walking left finds the minimum, but three steps left may run off the tree or skip values in right subtrees.
-
2
What does this print?
class N:def __init__(self, val, left=None, right=None):self.val, self.left, self.right = val, left, rightdef walk(n, out):if n is None: returnwalk(n.left, out)walk(n.right, out)out.append(n.val)root = N(1, N(2, N(4), N(5)), N(3, None, N(6)))out = []walk(root, out)print(out)walkrecurses left, then right, and appends last: postorder. Node 2’s subtree gives 4, 5, 2; node 3’s gives 6, 3; the root comes last.[1, 2, 4, 5, 3, 6]is preorder and[4, 2, 5, 1, 3, 6]is inorder.[4, 5, 6, 2, 3, 1]lists the leaves first, which is bottom-up by level, not postorder. -
3
This diameter function has a bug. The true diameter of the tree below is 3 edges (4 to 2 to 1 to 3). What does it print?
class N:def __init__(self, val, left=None, right=None):self.val, self.left, self.right = val, left, rightdef diameter(root):best = 0def go(n):nonlocal bestif not n: return 0l, r = go(n.left), go(n.right)best = max(best, l + r)return l + r # buggo(root)return bestroot = N(1, N(2, N(4)), N(3))print(diameter(root))Each call hands its parent
l + rinstead of a height. A leaf returns 0 + 0 = 0, so every node sees heights of 0 from its children andbestnever gets above 0. The parent needs1 + max(l, r);l + ris only for updatingbest.. -
4
This BST check only compares each node with its own children. What does it print?
class N:def __init__(self, val, left=None, right=None):self.val, self.left, self.right = val, left, rightdef valid(n):if not n: return Trueif n.left and n.left.val >= n.val: return Falseif n.right and n.right.val <= n.val: return Falsereturn valid(n.left) and valid(n.right)root = N(5, N(1), N(6, N(3), N(7)))print(valid(root))Every parent-child pair passes: 1 < 5, 6 > 5, 3 < 6, 7 > 6. So it prints
True, but the tree is not a BST: 3 is in 5’s right subtree and smaller than 5. Pass(lo, hi)bounds down from all ancestors, or check that inorder is strictly increasing. -
5
The iterative inorder traversal with an explicit stack runs on a tree of n nodes. How big can the stack get?
The stack holds nodes on one path from the root that still wait for their left side, so its size is bounded by the height. A binary tree is only O(log n) tall when it’s balanced; inserting sorted keys into a plain BST gives a chain of height n. The widest level bounds the queue in level order, not the stack.
Practice problems
- easy Maximum Depth of Binary Tree leetcode.com
- easy Diameter of Binary Tree leetcode.com
- medium Binary Tree Level Order Traversal leetcode.com
- medium Validate Binary Search Tree leetcode.com
- medium Kth Smallest Element in a BST leetcode.com
- medium Lowest Common Ancestor of a Binary Tree leetcode.com
- hard Serialize and Deserialize Binary Tree leetcode.com