Views, Ancestors and Distances
Looking at a tree from outside
Every link in a binary tree points down, from a parent to a child. That's all the recursions so far needed. Some problems look at the tree from the side, or need to move up as well as down, and each of those needs one more thing per node:
| Problems | Each node needs | Then it's |
|---|---|---|
| Top View, Bottom View, Vertical Order Traversal | a column, and its level | a level-order walk, or a sort |
| Boundary of Binary Tree | which edge of the tree it's on | three walks |
| Lowest Common Ancestor of a Binary Tree | what its call found below it | a return value |
| All Nodes Distance K, Time to Burn Entire Tree | a link to its parent | a graph, and a breadth-first search |
A column is the simplest: the root is in column 0, a left child is one column left of its parent, a right child one column right. Stand above the tree and you see the highest node in each column; stand below it and you see the lowest:

A column for every node
Vertical Order Traversal of a Binary Tree lists the columns left to right, each from the top down. Two nodes can share a column and a row, like 5 and 6 under 1 in a full tree of seven, and then the problem orders them by value. So give every node its (column, row, value) and sort:
// Every node's (column, row, value), sorted: columns left to right, then rows top down, then values.
void collect(TreeNode* node, int col, int row, vector<tuple<int, int, int>>& cells) {
if (node == nullptr) return;
cells.push_back({col, row, node->val});
collect(node->left, col - 1, row + 1, cells); // a left child is one column left
collect(node->right, col + 1, row + 1, cells); // a right child one column right
}
vector<vector<int>> verticalTraversal(TreeNode* root) {
vector<tuple<int, int, int>> cells;
collect(root, 0, 0, cells);
sort(cells.begin(), cells.end());
vector<vector<int>> columns;
int last = INT_MIN;
for (auto [col, row, val] : cells) {
if (col != last) {
columns.push_back({});
last = col;
}
columns.back().push_back(val);
}
return columns;
}
def vertical_traversal(root):
"""Every node's (column, row, value), sorted: columns left to right, then rows top down, then values."""
cells = []
def collect(node, col, row):
if node is None:
return
cells.append((col, row, node.val))
collect(node.left, col - 1, row + 1) # a left child is one column left
collect(node.right, col + 1, row + 1) # a right child one column right
collect(root, 0, 0)
cells.sort()
columns = []
for col, row, val in cells:
if not columns or col != last:
columns.append([])
last = col
columns[-1].append(val)
return columns
// Every node's (column, row, value), sorted: columns left to right, then rows top down, then values.
static void collect(TreeNode node, int col, int row, List<int[]> cells) {
if (node == null) return;
cells.add(new int[]{col, row, node.val});
collect(node.left, col - 1, row + 1, cells); // a left child is one column left
collect(node.right, col + 1, row + 1, cells); // a right child one column right
}
static List<List<Integer>> verticalTraversal(TreeNode root) {
List<int[]> cells = new ArrayList<>();
collect(root, 0, 0, cells);
cells.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] != b[1] ? a[1] - b[1] : a[2] - b[2]);
List<List<Integer>> columns = new ArrayList<>();
int last = Integer.MIN_VALUE;
for (int[] c : cells) {
if (columns.isEmpty() || c[0] != last) {
columns.add(new ArrayList<>());
last = c[0];
}
columns.get(columns.size() - 1).add(c[2]);
}
return columns;
}
// Every node's [column, row, value], sorted: columns left to right, then rows top down, then values.
function verticalTraversal(root) {
const cells = [];
function collect(node, col, row) {
if (node === null) return;
cells.push([col, row, node.val]);
collect(node.left, col - 1, row + 1); // a left child is one column left
collect(node.right, col + 1, row + 1); // a right child one column right
}
collect(root, 0, 0);
cells.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
const columns = [];
let last = null;
for (const [col, , val] of cells) {
if (col !== last) {
columns.push([]);
last = col;
}
columns[columns.length - 1].push(val);
}
return columns;
}
Each printed [[9], [3, 15], [20], [7]], [[4], [2], [1, 5, 6], [3], [7]], [[4], [2], [1, 5, 6], [3], [7]] for the problem's three examples. In the last, 6 is visited before 5, and the sort still puts 5 first. Collecting is O(n) and the sort O(n log n).
The views don't need a sort. Walk the tree in level order, keeping each node's column: the first node to reach a column is the highest one, the top view, and the last is the lowest, the bottom view:
from collections import deque
def views(root):
"""The top and bottom views: the first and the last node in each column, in level order."""
top, bottom, queue = {}, {}, deque([(root, 0)])
while queue:
node, col = queue.popleft()
top.setdefault(col, node.val) # the first node to reach this column, highest up
bottom[col] = node.val # the latest one, lowest down
if node.left:
queue.append((node.left, col - 1))
if node.right:
queue.append((node.right, col + 1))
columns = sorted(top)
return [top[c] for c in columns], [bottom[c] for c in columns]
For [1, 2, 3], [10, 20, 30, 40, 60, 90, 100], [20, 8, 22, 5, 3, null, 25, null, null, 10, 14] it printed the top view and then the bottom view: [2, 1, 3], [2, 1, 3]; [40, 20, 10, 30, 100], [40, 20, 90, 30, 100]; [5, 8, 20, 22, 25], [5, 10, 3, 14, 25]. Level order matters: a depth-first walk reaches a deep left node before a shallow right one, and the first visit in a column isn't always the highest.
Boundary of Binary Tree walks the outline in three parts: the left edge down from the root's left child, every leaf left to right, then the right edge back up. Each edge stops before its leaf, so no node is listed twice:
def boundary(root):
"""The root, the left edge down, the leaves left to right, the right edge back up; no node twice."""
def is_leaf(node):
return node.left is None and node.right is None
out = [root.val]
if is_leaf(root):
return out
node = root.left # the left edge, stopping before its leaf
while node and not is_leaf(node):
out.append(node.val)
node = node.left if node.left else node.right
def leaves(node):
if node is None:
return
if is_leaf(node):
out.append(node.val)
leaves(node.left)
leaves(node.right)
leaves(root)
right, node = [], root.right # the right edge, stopping before its leaf, then reversed
while node and not is_leaf(node):
right.append(node.val)
node = node.right if node.right else node.left
return out + right[::-1]
It returned [1, 3, 4, 2] and [1, 2, 4, 7, 8, 9, 10, 6, 3] for the problem's two examples.
Where two nodes meet
Lowest Common Ancestor of a Binary Tree asks for the deepest node that has both p and q below it, counting a node as below itself. Let every call report what it found below it: p, q, or nothing. A call on p or q reports itself without looking further down. A call that hears a find from both sides is where the two paths meet; a call that hears from one side passes that find up:
// The deepest node with p and q both below it (a node counts as below itself).
TreeNode* lowestCommonAncestor(TreeNode* node, TreeNode* p, TreeNode* q) {
if (node == nullptr || node == p || node == q) return node; // nothing here, or one of the two
TreeNode* left = lowestCommonAncestor(node->left, p, q);
TreeNode* right = lowestCommonAncestor(node->right, p, q);
if (left && right) return node; // one on each side: this is where they meet
return left ? left : right; // pass up whatever was found
}
def lowest_common_ancestor(node, p, q):
"""The deepest node with p and q both below it (a node counts as below itself)."""
if node is None or node is p or node is q:
return node # nothing here, or one of the two
left = lowest_common_ancestor(node.left, p, q)
right = lowest_common_ancestor(node.right, p, q)
if left and right: # one on each side: this is where they meet
return node
return left or right # pass up whatever was found
// The deepest node with p and q both below it (a node counts as below itself).
static TreeNode lowestCommonAncestor(TreeNode node, TreeNode p, TreeNode q) {
if (node == null || node == p || node == q) return node; // nothing here, or one of the two
TreeNode left = lowestCommonAncestor(node.left, p, q);
TreeNode right = lowestCommonAncestor(node.right, p, q);
if (left != null && right != null) return node; // one on each side: this is where they meet
return left != null ? left : right; // pass up whatever was found
}
// The deepest node with p and q both below it (a node counts as below itself).
function lowestCommonAncestor(node, p, q) {
if (node === null || node === p || node === q) return node; // nothing here, or one of the two
const left = lowestCommonAncestor(node.left, p, q);
const right = lowestCommonAncestor(node.right, p, q);
if (left && right) return node; // one on each side: this is where they meet
return left || right; // pass up whatever was found
}
Each printed 3 5 1: on the problem's tree, 5 and 1 meet at the root 3; 5 and 4 meet at 5, since 4 is below 5 and 5 counts as below itself; and in [1, 2] the answer is 1. One visit per node at most, O(n). Watch the finds come up:
The lowest common ancestor of 6 and 4 — Amber marks the two nodes to find, indigo the call working, violet the calls finished, with what each handed up on its badge.

This needs both nodes to be in the tree: if q were missing, the call on p would still hand up p, and p would come out as the answer. In a binary search tree there's a faster way, by comparing values on the way down, in the next section.
Moving up: a link to the parent
All Nodes Distance K in Binary Tree wants every node exactly k edges from a target, and the path to one of them may go up through the target's parent first. A tree can't go up. So record every node's parent in one walk: now each node has up to three neighbours, its two children and its parent, and the tree is a graph. From the target, a breadth-first search spreads out one ring per step, with a set of nodes already reached so that it never walks back the way it came:
// Every node exactly k edges from target, moving up through parents as well as down.
vector<int> distanceK(TreeNode* root, TreeNode* target, int k) {
unordered_map<TreeNode*, TreeNode*> parent{{root, nullptr}};
vector<TreeNode*> stack{root};
while (!stack.empty()) { // one walk to record every node's parent
TreeNode* node = stack.back();
stack.pop_back();
for (TreeNode* child : {node->left, node->right})
if (child) {
parent[child] = node;
stack.push_back(child);
}
}
unordered_set<TreeNode*> seen{target};
vector<TreeNode*> ring{target};
for (int step = 0; step < k; step++) { // k rings out from the target
vector<TreeNode*> next;
for (TreeNode* node : ring)
for (TreeNode* near : {node->left, node->right, parent[node]})
if (near && seen.insert(near).second) next.push_back(near);
ring = next;
}
vector<int> out;
for (TreeNode* node : ring) out.push_back(node->val);
return out;
}
def distance_k(root, target, k):
"""Every node exactly k edges from target, moving up through parents as well as down."""
parent, stack = {root: None}, [root]
while stack: # one walk to record every node's parent
node = stack.pop()
for child in (node.left, node.right):
if child:
parent[child] = node
stack.append(child)
seen, ring = {target}, [target]
for _ in range(k): # k rings out from the target
nxt = []
for node in ring:
for near in (node.left, node.right, parent[node]):
if near and near not in seen:
seen.add(near)
nxt.append(near)
ring = nxt
return [node.val for node in ring]
// Every node exactly k edges from target, moving up through parents as well as down.
static List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
Map<TreeNode, TreeNode> parent = new HashMap<>();
Deque<TreeNode> stack = new ArrayDeque<>();
parent.put(root, null);
stack.push(root);
while (!stack.isEmpty()) { // one walk to record every node's parent
TreeNode node = stack.pop();
for (TreeNode child : new TreeNode[]{node.left, node.right})
if (child != null) {
parent.put(child, node);
stack.push(child);
}
}
Set<TreeNode> seen = new HashSet<>(List.of(target));
List<TreeNode> ring = new ArrayList<>(List.of(target));
for (int step = 0; step < k; step++) { // k rings out from the target
List<TreeNode> next = new ArrayList<>();
for (TreeNode node : ring)
for (TreeNode near : new TreeNode[]{node.left, node.right, parent.get(node)})
if (near != null && seen.add(near)) next.add(near);
ring = next;
}
List<Integer> out = new ArrayList<>();
for (TreeNode node : ring) out.add(node.val);
return out;
}
// Every node exactly k edges from target, moving up through parents as well as down.
function distanceK(root, target, k) {
const parent = new Map([[root, null]]);
const stack = [root];
while (stack.length) { // one walk to record every node's parent
const node = stack.pop();
for (const child of [node.left, node.right])
if (child) {
parent.set(child, node);
stack.push(child);
}
}
const seen = new Set([target]);
let ring = [target];
for (let step = 0; step < k; step++) { // k rings out from the target
const next = [];
for (const node of ring)
for (const near of [node.left, node.right, parent.get(node)])
if (near && !seen.has(near)) {
seen.add(near);
next.push(near);
}
ring = next;
}
return ring.map((node) => node.val);
}
Each printed [1, 4, 7], [1, 5], []: 2 from 5 on the problem's tree, 1 from the root, and nothing 3 away in a one-node tree (sorted, since any order is accepted). The walk and the search each touch every node once, O(n). Step through the rings from 5:
Rings out from 5 — Indigo is the target; green the ring just reached; violet the rings before it. The badge is each node's distance from 5.

Without parent links, you can still find every node's distance from the target, from the two paths down from the root, but each path costs a search. In Python on random trees, k = 3:
| Nodes | Distances from root paths | Parent links and rings |
|---|---|---|
| 500 | 58.0 ms | 0.20 ms |
| 1,000 | 228.3 ms | 0.43 ms |
| 2,000 | 857.4 ms | 0.74 ms |
Both found the same nodes every time. Time to Burn Entire Tree from a Node is the same search run to the end: a fire lit at one node spreads one edge a minute in every direction, and the answer is the number of the last ring:
def time_to_burn(root, start):
"""Minutes for a fire lit at `start` to reach every node, one edge a minute in every direction."""
parent, stack, origin = {root: None}, [root], None
while stack:
node = stack.pop()
if node.val == start:
origin = node
for child in (node.left, node.right):
if child:
parent[child] = node
stack.append(child)
seen, ring, minutes = {origin}, [origin], 0
while True:
nxt = []
for node in ring:
for near in (node.left, node.right, parent[node]):
if near and near not in seen:
seen.add(near)
nxt.append(near)
if not nxt:
return minutes
ring, minutes = nxt, minutes + 1
On the problem's tree it took 3 minutes from 5, 3 from the root and 5 from the leaf 7, which has to burn up to the root before coming down the other side.
Now you spread the fire
Below, the fire starts at the node you pick. Each minute, click every node it reaches next: every unburnt child or parent of a node that caught the minute before. A node the fire can't reach yet is refused, with the minute it will catch. Count the minutes, then try a start where the fire has to climb.

Spotting it, and when it breaks
The clues. "Vertical", "top view", "bottom view", "from the left", "from the right": give each node a column and walk in level order. "Boundary" or "the outline": three walks. "Lowest common ancestor", "where two paths meet", "the first shared ancestor": return what each call found. "Distance", "k away", "burn", "infect", "spread", "time to reach": parent links and a breadth-first search.
The near-misses. The left and right side views are level order alone, no columns needed (Tree Traversals: Depth-First and Breadth-First). The lowest common ancestor in a binary search tree uses the values to walk straight down (Binary Search Trees). Distances in a graph with cycles need the same breadth-first search with the visited set doing more work (Graphs).
When it breaks. The answer assumes both nodes are in the tree; if one may be missing, return a count of finds as well. The recursions go as deep as the tree, so a chain needs the stack versions. And on a deep tree, measuring each distance from root paths is far slower than one search.
Bugs that cost a submission
The top view from a depth-first walk.
def top_view(root):
top = {}
def walk(node, col):
if node is None:
return
top.setdefault(col, node.val) # wrong: the first visit isn't the highest node
walk(node.left, col - 1)
walk(node.right, col + 1)
walk(root, 0)
return [top[c] for c in sorted(top)]
On [1, 2, 3, null, 4, null, null, null, 5] it returned [2, 1, 5]; the top view is [2, 1, 3]. The walk went down the left side first and reached column 1 at the deep 5 before the shallow 3. Walk in level order, or keep each column's depth and compare.
Not ordering by value inside a cell.
def vertical_traversal(root):
"""Every node's (column, row, value), sorted: columns left to right, then rows top down, then values."""
cells = []
def collect(node, col, row):
if node is None:
return
cells.append((col, row, node.val))
collect(node.left, col - 1, row + 1) # a left child is one column left
collect(node.right, col + 1, row + 1) # a right child one column right
collect(root, 0, 0)
cells.sort(key=lambda c: (c[0], c[1])) # wrong: two nodes sharing a row stay in visit order
columns = []
for col, row, val in cells:
if not columns or col != last:
columns.append([])
last = col
columns[-1].append(val)
return columns
On [1, 2, 3, 4, 6, 5, 7] it returned [[4], [2], [1, 6, 5], [3], [7]]; the answer is [[4], [2], [1, 5, 6], [3], [7]]. 6 and 5 share column 0 and row 2, and the problem orders them by value.
The search-tree rule on a plain tree.
def lowest_common_ancestor(node, p, q):
if p.val < node.val and q.val < node.val: # wrong: the BST rule, on a tree with no order
return lowest_common_ancestor(node.left, p, q)
if p.val > node.val and q.val > node.val:
return lowest_common_ancestor(node.right, p, q)
return node
For 6 and 8 on the problem's tree it returned 8; the answer is 3. Both values are bigger than the root's 3, so it walked right, but 6 is on the left. The rule only holds in a binary search tree.
Rings with no record of what's reached.
def distance_k(root, target, k):
"""Every node exactly k edges from target, moving up through parents as well as down."""
parent, stack = {root: None}, [root]
while stack: # one walk to record every node's parent
node = stack.pop()
for child in (node.left, node.right):
if child:
parent[child] = node
stack.append(child)
seen, ring = {target}, [target]
for _ in range(k): # k rings out from the target
nxt = []
for node in ring:
for near in (node.left, node.right, parent[node]):
if near: # wrong: nothing stops the walk turning back
nxt.append(near)
ring = nxt
return [node.val for node in ring]
From 5 with k = 2 it returned [1, 4, 5, 5, 5, 7]; the answer is [1, 4, 7]. Every child stepped back up to 5, and 3 stepped back down to it: in a graph, an edge can be walked both ways.
Walking the boundary's edge into its leaf.
def boundary(root):
"""The root, the left edge down, the leaves left to right, the right edge back up; no node twice."""
def is_leaf(node):
return node.left is None and node.right is None
out = [root.val]
if is_leaf(root):
return out
node = root.left # the left edge, stopping before its leaf
while node: # wrong: walks on into the leaf
out.append(node.val)
node = node.left if node.left else node.right
def leaves(node):
if node is None:
return
if is_leaf(node):
out.append(node.val)
leaves(node.left)
leaves(node.right)
leaves(root)
right, node = [], root.right # the right edge, stopping before its leaf, then reversed
while node and not is_leaf(node):
right.append(node.val)
node = node.right if node.right else node.left
return out + right[::-1]
On [1, 2, 3, 4] it returned [1, 2, 4, 4, 3]; the answer is [1, 2, 4, 3]. The 4 went in once as part of the left edge and again as a leaf.
Check yourself
Quiz
1 / 6Multiple choice
Multiple choice
Multiple choice
Multiple choice
Multiple choice
Multiple choice
Practice problems
In this order:
- Top View of a Binary Tree: the first node in each column, in level order.
- Bottom View of Binary Tree: the last node in each column.
- Boundary of Binary Tree: the left edge, the leaves, the right edge.
- Lowest Common Ancestor of a Binary Tree: each call reports what it found.
- All Nodes Distance K in Binary Tree: parent links and rings.
- Vertical Order Traversal of a Binary Tree: (column, row, value), sorted.
- Time to Burn Entire Tree from a Node: the rings, run to the end.
Next, Building and Serializing Trees: rebuilding a tree from its traversals, and writing one down so it can be read back.