Skip to main content

Views, Ancestors and Distances

Editorial

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:

ProblemsEach node needsThen it's
Top View, Bottom View, Vertical Order Traversala column, and its levela level-order walk, or a sort
Boundary of Binary Treewhich edge of the tree it's onthree walks
Lowest Common Ancestor of a Binary Treewhat its call found below ita return value
All Nodes Distance K, Time to Burn Entire Treea link to its parenta 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:

The tree 1, 2, 3, 4, 5, 6, 7 standing on vertical bands numbered -2 to 2. In column 0, the bands show 1 at the top and 5 and 6 sharing the bottom row. Each band's top node is marked with a triangle pointing down from above, the top view 4, 2, 1, 3, 7; each band's lowest node with a triangle pointing up from below.

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.

The problem's tree with 6 and 4 marked. Under each node, a small tag shows what its call handed up: none from the leaves 7, 0 and 8, 6 from 6 itself, 4 from 4 itself and from 2, and from 5 a highlighted 5, where a 6 came from the left and a 4 from the right. 1 hands up none, and the root passes 5 on.

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.

The problem's tree with dashed arrows going up from each node to its parent, next to the usual downward edges. Around the target 5, rings are drawn: ring 1 holds 6, 2 and 3, the 3 reached by going up; ring 2 holds 7, 4 and 1. A label reads: with parents, it's a graph.

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:

NodesDistances from root pathsParent links and rings
50058.0 ms0.20 ms
1,000228.3 ms0.43 ms
2,000857.4 ms0.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.

Spread the Fire: burn the tree one ring a minute, up to parents as well as down

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 / 6

Multiple choice

A node in column 2 has a left child and a right child. Which columns are they in?

Practice problems

In this order:

  1. Top View of a Binary Tree: the first node in each column, in level order.
  2. Bottom View of Binary Tree: the last node in each column.
  3. Boundary of Binary Tree: the left edge, the leaves, the right edge.
  4. Lowest Common Ancestor of a Binary Tree: each call reports what it found.
  5. All Nodes Distance K in Binary Tree: parent links and rings.
  6. Vertical Order Traversal of a Binary Tree: (column, row, value), sorted.
  7. 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.