Part 11 · Lesson 41 · read
Binary Trees and Traversals
Root, leaves and height. Preorder, inorder and postorder, drawn and coded.
On this page
Why trees?
So far every structure was a straight line: an array, a string, a linked list, a stack. A line is the wrong shape for things that branch. Think of a folder with sub-folders, a family tree, the nested tags of a web page, or the choices in a game. A tree is a group of nodes (boxes that hold a value). Each node can lead to several other nodes below it, and there are no loops. In this lesson we meet the most common kind, the binary tree. In a binary tree every node has at most two children, called the left child and the right child.
The big new idea: a tree is recursive. Recursive means made of smaller copies of itself, like a set of nesting dolls. In code, recursion is a function that calls itself on a smaller part of the problem. Look at any node and everything below it, and you see a smaller tree. So you can answer a question about a whole tree by answering the same question for the left part and the right part, then joining the two answers. Almost every tree problem in this part of the series uses this one trick.
Tree vocabulary
We use this example tree for the whole lesson. Keep the picture in mind while you read the table.
1
/ \
2 3
/ \
4 5| Word | In this tree | Meaning |
|---|---|---|
| root | 1 | the one node at the top; it is the only node with no parent |
| parent / child | 2 is the parent of 4 and 5 | a parent is the node right above its children; each node has at most one parent |
| leaf | 4, 5 and 3 | a node with no children |
| edge | 1-2, 1-3, 2-4, 2-5 | a link between a parent and a child |
| subtree | 2, 4, 5 | a node together with everything below it; the subtree of 2 is a small tree on its own |
| depth of a node | depth(4) = 2 | how many edges (links) you cross going from the root down to that node (the root has depth 0) |
| height of a tree | 2 | the number of edges on the longest path from the root down to a leaf |
Careful: some books count height and depth in nodes, not edges. LeetCode's 'maximum depth' counts nodes, so this tree has maximum depth 3. Always check which one the problem means.
A node in JavaScript
A node is a small object with a value and two links. A link is either another node or null, which means "nothing here". A whole tree is just its root node. From the root you can reach every other node by following links, just like a linked list is just its head.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; // the value stored in this node this.left = left; // another TreeNode, or null if there is no left child this.right = right; // another TreeNode, or null if there is no right child }}// 1// / \// 2 3// / \// 4 5const root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3));console.log(root.left.right.val); // 5console.log(root.right.left); // null (3 is a leaf, it has no children)null. Almost every recursive tree function starts with if (node === null) return .... That line is the base case. The base case is the smallest problem, the one you can answer right away without calling the function again.Building a tree from an array
Linking nodes by hand takes a long time. LeetCode shows trees as arrays like [3, 9, 20, null, null, 15, 7]. This is the level-order form. You read the tree row by row, from the top, left to right. You write null where a child is missing. Here 3 is the root and its children are 9 and 20. Node 9 has no children (two nulls). Node 20 has children 15 and 7.
To build the tree, keep a queue of nodes that still need children. A queue is a first-in, first-out line, like people waiting at a shop (see lesson 39). Take the next waiting node. Give it the next two array values as its left and right child. We use a head number instead of shift(), because removing from the front of a JavaScript array is slow. We will reuse this helper in most of the samples to come.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}// Level-order array -> tree. null marks a missing child (the format LeetCode uses).function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; // nodes that still need their children attached let head = 0; // read position in the queue (no slow shift) let i = 1; // next unread value in arr while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}const root = buildTree([3, 9, 20, null, null, 15, 7]);console.log(root.val, root.left.val, root.right.val); // 3 9 20console.log(root.right.left.val, root.right.right.val); // 15 7console.log(root.left.left, root.left.right); // null nullThree ways to walk a tree
To traverse a tree means to visit every node exactly once. A line has one natural order. A tree gives you a choice. The three classic depth-first orders differ in only one thing: when you write down the current node, compared with its two sides:
- Preorder: node, then left subtree, then right subtree. ("pre" = before the children.)
- Inorder: left subtree, then node, then right subtree. ("in" = between the children.)
- Postorder: left subtree, then right subtree, then node. ("post" = after the children.)
Depth-first means we go all the way down one branch before we come back to try another branch. Each order uses the same three lines, just in a different order:
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function preorder(node, out = []) { if (node === null) return out; out.push(node.val); // visit first ... preorder(node.left, out); preorder(node.right, out); return out;}function inorder(node, out = []) { if (node === null) return out; inorder(node.left, out); out.push(node.val); // ... visit in the middle ... inorder(node.right, out); return out;}function postorder(node, out = []) { if (node === null) return out; postorder(node.left, out); postorder(node.right, out); out.push(node.val); // ... or visit last return out;}const root = buildTree([1, 2, 3, 4, 5]);console.log(preorder(root)); // [1, 2, 4, 5, 3]console.log(inorder(root)); // [4, 2, 5, 1, 3]console.log(postorder(root)); // [4, 5, 2, 3, 1]Recursion keeps track of where to go back to without extra work from you. The language keeps a call stack, a pile of open function calls that remembers where to return. Time is O(n), because each node is visited once. Extra space is O(h) for the call stack, where h is the height of the tree. For a well-balanced tree (both sides about equal) h is about log n. For a tree that is one long chain, h can be as big as n.
Traced: one walk, three orders
One walk can build all three orders at once. We pass every node three times: on the way down, after coming back from the left side, and after coming back from the right side. Watch the three result lists grow at different times.
We call walk on the root, node 1. Each call handles one node, then makes two more calls: one for the left side and one for the right side. The path shows the calls that are still open right now.
1function walk(node) {2 if (node === null) return;3 pre.push(node.val);4 walk(node.left);5 ino.push(node.val);6 walk(node.right);7 post.push(node.val);8}▶walk(root);| pre | [] |
| ino | [] |
| post | [] |
Walking with an explicit stack
Recursion can fail on a very deep tree. A chain of 100,000 nodes overflows the call stack (the call stack runs out of room). Interviewers also like to ask "now do it without recursion". The answer is to do the job the language did for you. Keep your own stack, which is a pile where you add and remove only at the top.
Preorder is the easiest. Pop a node, visit it, then push its children. A stack gives back the last item first. So push the right child before the left child, and the left child comes out first.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function preorderIterative(root) { if (root === null) return []; const out = []; const stack = [root]; while (stack.length > 0) { const node = stack.pop(); out.push(node.val); if (node.right) stack.push(node.right); // pushed first, so it comes out second if (node.left) stack.push(node.left); // pushed last, so it comes out next } return out;}const root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3));console.log(preorderIterative(root)); // [1, 2, 4, 5, 3]| Step | Stack (top on the right) | Output so far |
|---|---|---|
| start | [1] | [] |
| pop 1, visit, push 3 then 2 | [3, 2] | [1] |
| pop 2, visit, push 5 then 4 | [3, 5, 4] | [1, 2] |
| pop 4, visit (leaf) | [3, 5] | [1, 2, 4] |
| pop 5, visit (leaf) | [3] | [1, 2, 4, 5] |
| pop 3, visit (leaf) | [] | [1, 2, 4, 5, 3] |
Inorder needs one more idea. Before you visit a node, you must finish its whole left side. So go left as far as you can and push each node you pass. The stack remembers the way back. Then pop the leftmost node and visit it. After that, do the same for its right side.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function inorderIterative(root) { const out = []; const stack = []; let cur = root; while (cur !== null || stack.length > 0) { while (cur !== null) { // dive as far left as possible, remembering the way back stack.push(cur); cur = cur.left; } cur = stack.pop(); // the leftmost unvisited node out.push(cur.val); cur = cur.right; // then do the same for its right subtree } return out;}const root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3));console.log(inorderIterative(root)); // [4, 2, 5, 1, 3]Postorder is the hard one, because you visit a node only after both of its children. Here is a trick. Postorder (left, right, node) is exactly the reverse of (node, right, left). And (node, right, left) is just preorder with the children pushed in the opposite order. So run that, then reverse the result.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}// Postorder (left, right, node) is the reverse of "node, right, left".function postorderIterative(root) { if (root === null) return []; const out = []; const stack = [root]; while (stack.length > 0) { const node = stack.pop(); out.push(node.val); // node, then right, then left ... if (node.left) stack.push(node.left); if (node.right) stack.push(node.right); } return out.reverse(); // ... reversed is left, right, node}const root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3));console.log(postorderIterative(root)); // [4, 5, 2, 3, 1]All three take O(n) time and O(h) extra space for the stack, the same as the recursive versions. The difference is the limit on depth. Now it is your computer's memory, not the small call stack.
Depth, same tree, invert
Here is the recursive way of thinking on three classic questions. Each one follows the same recipe. First handle null. Then ask the question of the left and right sides. Then combine the two answers.
- Maximum depth: an empty tree has depth 0. Otherwise the depth is 1 (for this node) plus the larger depth of the two sides.
- Same tree: two empty trees match. One empty tree and one non-empty tree do not match. Otherwise the two values must match, and both pairs of sides must match too.
- Invert: swap the left and right child of every node. Invert the children first, then swap them.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}// Number of nodes on the longest path from the root down to a leaf.function maxDepth(node) { if (node === null) return 0; return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));}// Two trees are the same if their shapes match and every pair of values matches.function isSameTree(a, b) { if (a === null && b === null) return true; if (a === null || b === null) return false; return a.val === b.val && isSameTree(a.left, b.left) && isSameTree(a.right, b.right);}// Swap the two children of every node.function invertTree(node) { if (node === null) return null; const left = invertTree(node.left); const right = invertTree(node.right); node.left = right; node.right = left; return node;}function preorder(node, out = []) { if (node === null) return out; out.push(node.val); preorder(node.left, out); preorder(node.right, out); return out;}console.log(maxDepth(buildTree([3, 9, 20, null, null, 15, 7]))); // 3console.log(isSameTree(buildTree([1, 2, 3]), buildTree([1, 2, 3]))); // trueconsole.log(isSameTree(buildTree([1, 2]), buildTree([1, null, 2]))); // falseconsole.log(preorder(invertTree(buildTree([4, 2, 7, 1, 3, 6, 9])))); // [4, 7, 9, 6, 2, 3, 1]node.left.val when node.left is null. That gives the error "Cannot read properties of null". Put the null check at the top of the function. Then the rest of the function can be sure it has a real node.Practice questions
For each question, decide two things first. Which moment do I need: before, between or after the children? And what does the base case return?
Return the preorder traversal (the list of node values in preorder) of a binary tree.
Hint 1
Hint 2
Show all 2 solutions with explanations
If the node is null, stop. Otherwise add its value to the answer, then walk the left side, then the right side.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function preorderTraversal(root) { const out = []; function walk(node) { if (node === null) return; out.push(node.val); walk(node.left); walk(node.right); } walk(root); return out;}console.log(preorderTraversal(buildTree([1, null, 2, 3]))); // [1, 2, 3]console.log(preorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [1, 2, 4, 5, 3]console.log(preorderTraversal(buildTree([]))); // []O(n) time. O(h) space for the call stack (the pile of open calls), where h is the height of the tree. h can be as big as n for a tree that is one long chain.
- Start with the root on a stack.
- Pop a node, record it, then push its right child and then its left child.
- The left child is on top, so it is handled next. The whole left side is finished before we touch the right side.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function preorderTraversal(root) { if (root === null) return []; const out = []; const stack = [root]; while (stack.length > 0) { const node = stack.pop(); out.push(node.val); if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } return out;}console.log(preorderTraversal(buildTree([1, null, 2, 3]))); // [1, 2, 3]console.log(preorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [1, 2, 4, 5, 3]O(n) time. Each level of the tree leaves at most one waiting sibling (the other child of the same parent) on the stack. So the stack holds at most about h + 1 nodes, which is O(h) space. There is no limit on recursion depth to worry about.
Write the recursive one first. It is only three lines. If the interviewer says "without recursion", switch to the stack. (LeetCode 144.)
Return the inorder traversal (the list of node values in inorder) of a binary tree.
Hint 1
Hint 2
Hint 3
Show all 3 solutions with explanations
Walk left, record the node, walk right.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function inorderTraversal(root) { const out = []; function walk(node) { if (node === null) return; walk(node.left); out.push(node.val); walk(node.right); } walk(root); return out;}console.log(inorderTraversal(buildTree([1, null, 2, 3]))); // [1, 3, 2]console.log(inorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 2, 5, 1, 3]O(n) time, O(h) space.
- Keep a pointer
curand a stack. - While
curexists, push it and move to its left child. - When it is null, pop the top of the stack. That is the next node in order. Record it and set
curto its right child. - Stop when both
curis null and the stack is empty.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function inorderTraversal(root) { const out = []; const stack = []; let cur = root; while (cur !== null || stack.length > 0) { while (cur !== null) { stack.push(cur); cur = cur.left; } cur = stack.pop(); out.push(cur.val); cur = cur.right; } return out;}console.log(inorderTraversal(buildTree([1, null, 2, 3]))); // [1, 3, 2]console.log(inorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 2, 5, 1, 3]O(n) time, O(h) space. Every node is pushed once and popped once.
- If the current node has no left child, record it and go right.
- Otherwise find its predecessor. This is the node that comes just before it in inorder: the rightmost node of its left side.
- If the predecessor's right link is empty, point it back at the current node (a temporary link, like a thread) and go left.
- If it already points at the current node, we have just finished the left side. Remove the thread, record the current node, and go right.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function inorderTraversal(root) { const out = []; let cur = root; while (cur !== null) { if (cur.left === null) { out.push(cur.val); cur = cur.right; } else { let pred = cur.left; while (pred.right !== null && pred.right !== cur) pred = pred.right; if (pred.right === null) { pred.right = cur; // temporary link: a way back once the left side is done cur = cur.left; } else { pred.right = null; // second visit: remove the temporary link out.push(cur.val); cur = cur.right; } } } return out;}console.log(inorderTraversal(buildTree([1, null, 2, 3]))); // [1, 3, 2]console.log(inorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 2, 5, 1, 3]Still O(n) time (each edge, or link, is walked only a small fixed number of times). But it uses only O(1) extra space, and the tree is back to normal at the end. While it runs, it changes the tree for a short time. So it is not safe if other code reads the tree at the same moment. Know that it exists. Interviews usually expect the stack version.
Start with recursion. Use the stack version if recursion is not allowed. Mention Morris if they ask for O(1) space. (LeetCode 94.)
Return the postorder traversal (the list of node values in postorder) of a binary tree.
Hint 1
Hint 2
Show all 3 solutions with explanations
Walk left, walk right, then record the node.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function postorderTraversal(root) { const out = []; function walk(node) { if (node === null) return; walk(node.left); walk(node.right); out.push(node.val); } walk(root); return out;}console.log(postorderTraversal(buildTree([1, null, 2, 3]))); // [3, 2, 1]console.log(postorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 5, 2, 3, 1]O(n) time, O(h) space.
Run a stack-based preorder that goes node, then right, then left (push left first). Reverse the output to get left, right, node.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function postorderTraversal(root) { if (root === null) return []; const out = []; const stack = [root]; while (stack.length > 0) { const node = stack.pop(); out.push(node.val); if (node.left) stack.push(node.left); if (node.right) stack.push(node.right); } return out.reverse();}console.log(postorderTraversal(buildTree([1, null, 2, 3]))); // [3, 2, 1]console.log(postorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 5, 2, 3, 1]O(n) time and space. It is simple and hard to get wrong. It records the nodes in the wrong order and fixes the order at the end.
- Go left and push each node, like the stack version of inorder.
- Look at the top node without removing it. If it has a right child we have not handled yet, move to that child.
- Otherwise both sides are done. Record the node, pop it, and remember it in
last.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function postorderTraversal(root) { const out = []; const stack = []; let cur = root; let last = null; // the node we most recently finished while (cur !== null || stack.length > 0) { while (cur !== null) { stack.push(cur); cur = cur.left; } const top = stack[stack.length - 1]; if (top.right !== null && top.right !== last) { cur = top.right; // the right side is not done yet } else { out.push(top.val); last = stack.pop(); } } return out;}console.log(postorderTraversal(buildTree([1, null, 2, 3]))); // [3, 2, 1]console.log(postorderTraversal(buildTree([1, 2, 3, 4, 5]))); // [4, 5, 2, 3, 1]O(n) time, O(h) space, and the output is built in the right order. The last variable tells us that we are coming back from the right child, not arriving for the first time.
Use recursion by default. For the stack version, the reverse-preorder trick is the easiest to remember when you are nervous. (LeetCode 145.)
Return the maximum depth of a binary tree: the number of nodes along the longest path from the root down to a leaf.
Hint 1
Hint 2
Show all 2 solutions with explanations
The depth of null is 0. The depth of a node is 1 plus the larger depth of its two children.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function maxDepth(root) { if (root === null) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));}console.log(maxDepth(buildTree([3, 9, 20, null, null, 15, 7]))); // 3console.log(maxDepth(buildTree([1, null, 2]))); // 2console.log(maxDepth(buildTree([]))); // 0O(n) time, O(h) space. Each node asks its children for their answers and then adds itself. This is postorder thinking: children first, then the node.
Push [node, depth] pairs. Each time you pop a node, update the best depth seen so far. Then push its children with depth + 1.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function maxDepth(root) { if (root === null) return 0; let best = 0; const stack = [[root, 1]]; while (stack.length > 0) { 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;}console.log(maxDepth(buildTree([3, 9, 20, null, null, 15, 7]))); // 3console.log(maxDepth(buildTree([1, null, 2]))); // 2console.log(maxDepth(buildTree([]))); // 0O(n) time. Here the depth travels down with each stack entry, instead of coming back up from the recursion. It also works for very deep trees.
Use the two-line recursion. (LeetCode 104.) Lesson 42 adds a third way: count the levels with a queue.
Invert a binary tree (swap the left and right child of every node) and return its root.
Hint 1
Hint 2
Show all 2 solutions with explanations
- If the node is null, return null.
- Invert the left side and the right side.
- Put the inverted right side on the left, and the inverted left side on the right.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}// Tree -> level-order array with null gaps (trailing nulls trimmed), for printing.function toList(root) { const out = [], queue = [root]; let head = 0; while (head < queue.length) { const node = queue[head++]; if (node === null) { out.push(null); continue; } out.push(node.val); queue.push(node.left, node.right); } while (out.length > 0 && out[out.length - 1] === null) out.pop(); return out;}function invertTree(root) { if (root === null) return null; const left = invertTree(root.left); const right = invertTree(root.right); root.left = right; root.right = left; return root;}console.log(toList(invertTree(buildTree([4, 2, 7, 1, 3, 6, 9])))); // [4, 7, 2, 9, 6, 3, 1]console.log(toList(invertTree(buildTree([2, 1, 3])))); // [2, 3, 1]console.log(toList(invertTree(buildTree([])))); // []O(n) time, O(h) space. It changes the tree itself (no copy) and returns the same root.
Pop a node, swap its children, then push the children that exist. The order does not matter, because every node only needs its own swap.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}// Tree -> level-order array with null gaps (trailing nulls trimmed), for printing.function toList(root) { const out = [], queue = [root]; let head = 0; while (head < queue.length) { const node = queue[head++]; if (node === null) { out.push(null); continue; } out.push(node.val); queue.push(node.left, node.right); } while (out.length > 0 && out[out.length - 1] === null) out.pop(); return out;}function invertTree(root) { if (root === null) return null; const stack = [root]; while (stack.length > 0) { const node = stack.pop(); const left = node.left; node.left = node.right; node.right = left; if (node.left) stack.push(node.left); if (node.right) stack.push(node.right); } return root;}console.log(toList(invertTree(buildTree([4, 2, 7, 1, 3, 6, 9])))); // [4, 7, 2, 9, 6, 3, 1]console.log(toList(invertTree(buildTree([2, 1, 3])))); // [2, 3, 1]O(n) time, O(h) space. Any walking order works, including the queue-based one from the next lesson.
Use recursion. The code reads just like the definition. (LeetCode 226.)
You get the roots of two binary trees p and q. Return whether they are the same: same shape and same values.
Hint 1
Hint 2
Show all 3 solutions with explanations
Turn each tree into a string of text in preorder. Write a marker for every missing child, so the shape is kept too. Equal strings mean equal trees.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function serialize(node) { if (node === null) return "#"; return node.val + "," + serialize(node.left) + "," + serialize(node.right);}function isSameTree(p, q) { return serialize(p) === serialize(q);}console.log(isSameTree(buildTree([1, 2, 3]), buildTree([1, 2, 3]))); // trueconsole.log(isSameTree(buildTree([1, 2]), buildTree([1, null, 2]))); // falseconsole.log(isSameTree(buildTree([1, 2, 1]), buildTree([1, 1, 2]))); // falseO(n) time and extra space for the strings. Without the # markers, [1,2] and [1,null,2] would look the same, so the markers are a must. The answer is correct but wasteful. It builds both strings even if the roots already differ.
- Both are null: they are the same.
- Only one is null: they are different.
- Otherwise the two values must be equal, and both pairs of children must be the same.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function isSameTree(p, q) { if (p === null && q === null) return true; if (p === null || q === null) return false; return p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right);}console.log(isSameTree(buildTree([1, 2, 3]), buildTree([1, 2, 3]))); // trueconsole.log(isSameTree(buildTree([1, 2]), buildTree([1, null, 2]))); // falseconsole.log(isSameTree(buildTree([1, 2, 1]), buildTree([1, 1, 2]))); // falseO(n) time, O(h) space. It stops at the first difference, because && skips the rest as soon as one part is false.
Push the pair of roots. Pop a pair and run the same three checks. Then push the left pair and the right pair.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function isSameTree(p, q) { const stack = [[p, q]]; while (stack.length > 0) { const [a, b] = stack.pop(); if (a === null && b === null) continue; if (a === null || b === null || a.val !== b.val) return false; stack.push([a.left, b.left], [a.right, b.right]); } return true;}console.log(isSameTree(buildTree([1, 2, 3]), buildTree([1, 2, 3]))); // trueconsole.log(isSameTree(buildTree([1, 2]), buildTree([1, null, 2]))); // falseconsole.log(isSameTree(buildTree([1, 2, 1]), buildTree([1, 1, 2]))); // falseThe same logic with your own stack: O(n) time, O(h) space.
Use the recursion. It is the clearest. (LeetCode 100.) The same idea of walking two nodes together comes back in lesson 43 for symmetric trees and subtrees.
The leaf value sequence of a tree is the list of its leaf values, read from left to right. Two trees are leaf-similar if their lists are equal. Return whether root1 and root2 are leaf-similar.
Hint 1
Hint 2
Show all 2 solutions with explanations
Walk each tree and add the value of every leaf to an array. Then compare the two arrays item by item.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function leaves(node, out = []) { if (node === null) return out; if (node.left === null && node.right === null) out.push(node.val); leaves(node.left, out); leaves(node.right, out); return out;}function leafSimilar(root1, root2) { const a = leaves(root1), b = leaves(root2); return a.length === b.length && a.every((v, i) => v === b[i]);}console.log(leafSimilar( buildTree([3, 5, 1, 6, 2, 9, 8, null, null, 7, 4]), buildTree([3, 5, 1, 6, 7, 4, 2, null, null, null, null, null, null, 9, 8]),)); // trueconsole.log(leafSimilar(buildTree([1, 2, 3]), buildTree([1, 3, 2]))); // falseO(n + m) time and space for the two lists.
Give each tree its own stack and a helper that returns the next leaf, using the stack-based preorder from this lesson. Compare the leaves as they come. Stop at the first mismatch.
class TreeNode { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; }}function buildTree(arr) { if (arr.length === 0 || arr[0] === null) return null; const root = new TreeNode(arr[0]); const queue = [root]; let head = 0, i = 1; while (head < queue.length && i < arr.length) { const node = queue[head++]; if (i < arr.length && arr[i] !== null) { node.left = new TreeNode(arr[i]); queue.push(node.left); } i++; if (i < arr.length && arr[i] !== null) { node.right = new TreeNode(arr[i]); queue.push(node.right); } i++; } return root;}function nextLeaf(stack) { while (stack.length > 0) { const node = stack.pop(); if (node.left === null && node.right === null) return node.val; if (node.right) stack.push(node.right); if (node.left) stack.push(node.left); } return undefined; // no more leaves}function leafSimilar(root1, root2) { const s1 = root1 ? [root1] : []; const s2 = root2 ? [root2] : []; while (true) { const a = nextLeaf(s1), b = nextLeaf(s2); if (a !== b) return false; if (a === undefined) return true; // both ran out together }}console.log(leafSimilar( buildTree([3, 5, 1, 6, 2, 9, 8, null, null, 7, 4]), buildTree([3, 5, 1, 6, 7, 4, 2, null, null, null, null, null, null, 9, 8]),)); // trueconsole.log(leafSimilar(buildTree([1, 2, 3]), buildTree([1, 3, 2]))); // falseO(n + m) time in the worst case, but it can stop early. The extra space is only the two stacks (O(h)), not two full lists.
Collecting the lists is simpler and fine for interviews. Mention the one-leaf-at-a-time version as a way to make it faster. (LeetCode 872.)
Make it stick
- Define root, leaf, subtree, depth and height, and say what a binary tree is.
- Say where the line that records the node sits in preorder, inorder and postorder.
- Explain why the extra space of a recursive walk is O(h), and when h is as big as n.
- Write iterative preorder and inorder with a stack, without looking.
- Explain why the recursive solution always starts with the null check.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Depth-first goes deep before it goes wide. Lesson 42 does the opposite. Level order, also called breadth-first search, visits the tree row by row with a queue. It helps with right side views, zigzag orders and the shortest path to a leaf.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series