All Nodes Distance K in Binary Tree

Medium BFS

Problem

Given the root of a binary tree, a target node, and an integer k, return the values of all nodes that are exactly k edges away from the target node.

  • The number of nodes in the tree is in the range [1, 500].
  • 0 ≤ Node.val ≤ 500
  • All the values Node.val are unique.
  • target is a node in the tree.
  • 0 ≤ k ≤ 1000

Example

Input: root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, k = 2
Output: [7,4,1]

The algorithm first annotates each node with a reference to its parent, effectively converting the tree into an undirected graph. Starting from the target node (5), it performs a breadth-first search (BFS) to explore nodes at increasing distances. At distance 1, neighbors are nodes 6, 2, and 3 (parent). At distance 2, neighbors are nodes 7, 4, and 1. These nodes are collected and returned as the result.

Approach

Straightforward Solution

A naive approach would be to perform a DFS from the target node, but without parent pointers, upward traversal is impossible. Alternatively, one could attempt to find paths by repeated traversals, but this is inefficient and complex.

Core Observation

The problem reduces to finding all nodes at distance k in an undirected graph where edges connect parent and child nodes. The tree structure is inherently directed (parent to children), so adding parent pointers creates bidirectional edges, enabling BFS traversal in all directions.

Path to Optimal

The key insight is to augment each node with a parent pointer, transforming the tree into an undirected graph. This allows BFS starting from the target node to explore all neighbors (left child, right child, and parent) layer by layer. BFS naturally finds all nodes at distance k by expanding the frontier k times.

Optimal Approach

Perform a DFS to assign parent pointers to each node. Then, initiate a BFS from the target node, tracking visited nodes to avoid cycles. After k expansions, the nodes in the queue represent all nodes at distance k. Return their values.

Time

O(n)

Each node is visited once during the DFS to assign parent pointers and once during the BFS traversal, resulting in linear time relative to the number of nodes.

Space

O(n)

The parent pointers augment the tree with O(n) additional references, and the BFS queue and visited set each require O(n) space in the worst case.

Pattern Spotlight

BFS (Shortest Path on Unweighted Graph)

When a problem asks for nodes at a specific distance in a tree or graph, convert the structure into an undirected graph if needed, then use BFS to explore neighbors layer by layer, ensuring the first time nodes are reached corresponds to their shortest distance.

Solution

1from collections import deque
2
3class Solution:
4 def distanceK(self, root: TreeNode, target: TreeNode, k: int) -> List[int]:
5 def dfs(node, parent):
6 if not node:
7 return
8
9 node.parent = parent
10 dfs(node.left, node)
11 dfs(node.right, node)
12
13 dfs(root, None)
14 queue = deque([target])
15 seen = {target}
16 distance = 0
17
18 while queue and distance < k:
19 current_length = len(queue)
20 for _ in range(current_length):
21 node = queue.popleft()
22 for neighbor in [node.left, node.right, node.parent]:
23 if neighbor and neighbor not in seen:
24 seen.add(neighbor)
25 queue.append(neighbor)
26
27 distance += 1
28
29 return [node.val for node in queue]

Step-by-Step Solution

1

Annotate Each Node with Its Parent to Enable Bidirectional Traversal

5def dfs(node, parent):
6 if not node:
7 return
9 node.parent = parent
10 dfs(node.left, node)
11 dfs(node.right, node)
13dfs(root, None)

Objective

To traverse the tree and assign a parent pointer to each node, enabling upward traversal during BFS.

Key Insight

The tree is naturally directed from parent to children, which prevents moving upward during traversal. By performing a DFS that passes the parent node as a parameter and assigning it to each child's 'parent' attribute, the tree is effectively converted into an undirected graph. This transformation is crucial for BFS to explore all neighbors (left, right, and parent) without missing any nodes.

Interview Quick-Check

Core Logic

Assigning parent pointers during DFS creates bidirectional edges, allowing BFS to traverse both downwards and upwards from the target node.

State & Boundaries

The DFS terminates when a null node is reached, ensuring no invalid parent assignments.

Common Pitfalls & Bugs

Forgetting to assign parent pointers or incorrectly passing the parent node can cause incomplete graph construction and incorrect BFS results.

2

Perform BFS from Target to Collect Nodes at Distance K

14queue = deque([target])
15seen = {target}
16distance = 0
18while queue and distance < k:
19 current_length = len(queue)
20 for _ in range(current_length):
21 node = queue.popleft()
22 for neighbor in [node.left, node.right, node.parent]:
23 if neighbor and neighbor not in seen:
24 seen.add(neighbor)
25 queue.append(neighbor)
27 distance += 1
29return [node.val for node in queue]

Objective

To explore the graph layer by layer starting from the target node, collecting all nodes exactly k edges away.

Key Insight

BFS naturally discovers nodes in order of their distance from the start node. By maintaining a queue initialized with the target node and a visited set to avoid revisiting nodes, the algorithm expands the frontier one layer at a time. After k expansions, the queue contains all nodes at distance k. This approach leverages the undirected graph structure created by parent pointers to explore all possible paths.

Interview Quick-Check

Core Logic

BFS uses a queue and a visited set to explore neighbors (left, right, parent) without cycles, ensuring nodes are processed in increasing order of distance.

State & Boundaries

The BFS loop continues until either the queue is empty or the desired distance k is reached.

Common Pitfalls & Bugs

Failing to mark nodes as visited immediately upon enqueueing can cause duplicates and infinite loops.

Complexity

Each node is enqueued and dequeued at most once, ensuring O(n) time complexity for BFS.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 5 Critical
def dfs(node, parent):

Define a DFS helper function to assign parent pointers to each node.

This function is the foundation for converting the tree into an undirected graph by linking each node to its parent, enabling upward traversal.

Line 6 Critical
if not node:

Check if the current node is null to terminate recursion.

This base case prevents null pointer exceptions and stops the DFS from traversing beyond leaf nodes.

Line 24 Critical
seen.add(neighbor)

Mark the neighbor as visited.

Adding to the visited set immediately upon enqueueing prevents duplicate entries and cycles.

Line 7 Standard
return

Return immediately if the node is null.

Ensures the recursion unwinds correctly without processing invalid nodes.

Line 9 Standard
node.parent = parent

Assign the parent pointer to the current node.

This assignment creates a backward edge from the current node to its parent, enabling bidirectional traversal.

Line 10 Standard
dfs(node.left, node)

Recursively assign parent pointers to the left subtree.

This recursive call propagates the parent assignment down the left subtree, ensuring all nodes have correct parent references.

Line 11 Standard
dfs(node.right, node)

Recursively assign parent pointers to the right subtree.

Similarly, this call ensures the right subtree nodes are linked to their parents.

Line 13 Standard
dfs(root, None)

Initiate DFS from the root with no parent.

Starting the DFS here sets up the entire tree with parent pointers, with the root having no parent.

Line 14 Standard
queue = deque([target])

Initialize the BFS queue with the target node.

Starting BFS from the target node allows exploration of all nodes at increasing distances.

Line 15 Standard
seen = {target}

Initialize the visited set with the target node to avoid revisiting.

Tracking visited nodes prevents cycles and redundant processing during BFS.

Line 16 Standard
distance = 0

Initialize the distance counter to zero.

This counter tracks how many BFS layers have been expanded, corresponding to the distance from the target.

Line 18 Standard
while queue and distance < k:

Continue BFS while there are nodes to explore and distance is less than k.

This loop condition ensures BFS stops once nodes at distance k are reached or no more nodes remain.

Line 19 Standard
current_length = len(queue)

Record the number of nodes at the current BFS layer.

This count allows processing all nodes at the current distance before incrementing the distance.

Line 20 Standard
for _ in range(current_length):

Iterate over all nodes at the current BFS layer.

Processing nodes layer by layer ensures correct distance tracking.

Line 21 Standard
node = queue.popleft()

Dequeue the next node to process.

Dequeuing nodes in FIFO order maintains BFS's layer-by-layer traversal.

Line 22 Standard
for neighbor in [node.left, node.right, node.parent]:

Iterate over the current node's neighbors: left child, right child, and parent.

Exploring all neighbors ensures BFS covers all possible paths in the undirected graph.

Line 23 Standard
if neighbor and neighbor not in seen:

Check if the neighbor exists and has not been visited.

This condition prevents null pointer errors and avoids revisiting nodes, which could cause infinite loops.

Line 25 Standard
queue.append(neighbor)

Enqueue the neighbor for future BFS processing.

Enqueuing neighbors expands the BFS frontier to the next layer of nodes.

Line 27 Standard
distance += 1

Increment the distance counter after processing the current layer.

This increment reflects moving one step further from the target node in BFS.

Line 29 Standard
return [node.val for node in queue]

Return the values of all nodes currently in the queue, which are at distance k.

After k expansions, the queue contains exactly the nodes at distance k, so their values form the correct output.

Test Your Understanding

Why is BFS the appropriate traversal method for finding all nodes at distance k in this problem?

Because BFS explores nodes in order of their distance from the start node, ensuring that when nodes are first reached, it is via the shortest path. This property guarantees that after k expansions, the queue contains exactly the nodes at distance k.

Related Problems

BFS pattern

Don't just read it. Drill it.

Reconstruct All Nodes Distance K in Binary Tree from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the All Nodes Distance K in Binary Tree drill

or drill a free problem