All Nodes Distance K in Binary Tree
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
root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, k = 2[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
| 1 | from collections import deque |
| 2 | |
| 3 | class 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
Annotate Each Node with Its Parent to Enable Bidirectional Traversal
| 5 | def dfs(node, parent): |
| 6 | if not node: |
| 7 | return |
| 9 | node.parent = parent |
| 10 | dfs(node.left, node) |
| 11 | dfs(node.right, node) |
| 13 | dfs(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.
Perform BFS from Target to Collect Nodes at Distance K
| 14 | queue = deque([target]) |
| 15 | seen = {target} |
| 16 | distance = 0 |
| 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) |
| 27 | distance += 1 |
| 29 | return [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.
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.
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.
seen.add(neighbor)
Mark the neighbor as visited.
Adding to the visited set immediately upon enqueueing prevents duplicate entries and cycles.
return
Return immediately if the node is null.
Ensures the recursion unwinds correctly without processing invalid nodes.
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.
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.
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.
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.
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.
seen = {target}
Initialize the visited set with the target node to avoid revisiting.
Tracking visited nodes prevents cycles and redundant processing during BFS.
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.
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.
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.
for _ in range(current_length):
Iterate over all nodes at the current BFS layer.
Processing nodes layer by layer ensures correct distance tracking.
node = queue.popleft()
Dequeue the next node to process.
Dequeuing nodes in FIFO order maintains BFS's layer-by-layer traversal.
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.
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.
queue.append(neighbor)
Enqueue the neighbor for future BFS processing.
Enqueuing neighbors expands the BFS frontier to the next layer of nodes.
distance += 1
Increment the distance counter after processing the current layer.
This increment reflects moving one step further from the target node in BFS.
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