All Nodes Distance K in Binary Tree
MedPrompt
Given the root of a binary tree, the value of a target node target, and an integer k, return an array of the values of all nodes that have a distance k from the target node.
You can return the answer in any order.
Example 1:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, k = 2
Output: [7,4,1]
Explanation: The nodes that are a distance 2 from the target node (with value 5) have values 7, 4, and 1.Example 2:
Input: root = [1], target = 1, k = 3
Output: []
Constraints:
- The number of nodes in the tree is in the range
[1, 500]. 0 <= Node.val <= 500- All the values
Node.valare unique. targetis the value of one of the nodes in the tree.0 <= k <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach transforms the tree traversal problem into a graph traversal problem. Since a standard binary tree only allows downward traversal (from parent to child), we first need a way to traverse upwards. We can achieve this by creating a mapping from each node to its parent. Once we have this parent mapping, we can treat the tree as an undirected graph and perform a Breadth-First Search (BFS) starting from the target node to find all nodes at exactly distance k.
Algorithm
- Step 1: Annotate Parents & Find Target.
- Create a
Map<TreeNode, TreeNode> parentMap. - Traverse the tree using DFS (or BFS). For each
node, store its parent in the map:parentMap.put(node, parent).
- Create a
- Step 2: BFS from Target.
- Initialize a
Queue<TreeNode> queueand aSet<TreeNode> visited. - Add the
targetnode to both thequeueand thevisitedset. - Initialize
distance = 0. - Loop while the
queueis not empty: a. Ifdistance == k, stop the loop. The nodes currently in the queue are part of the answer. b. Get the number of nodes at the current level:levelSize = queue.size(). c. Process all nodes at the current level: Forifrom 0 tolevelSize - 1: i. DequeuecurrentNode. ii. For each neighbor ofcurrentNode(left child, right child, and parent fromparentMap): - If the neighbor exists and has not been visited, add it to thequeueand thevisitedset. d. Incrementdistance.
- Initialize a
- Step 3: Collect Results.
- After the loop terminates, drain the
queueand add the value of each node to a result list. - Return the result list.
- After the loop terminates, drain the
Walkthrough
The method involves two main phases. First, we traverse the entire tree to build a data structure, typically a HashMap, that maps each node to its parent. This allows us to move upwards in the tree, which is not possible by default. During this initial traversal, we can also locate the target node.
Once we have the parent pointers and the target node, we start a Breadth-First Search (BFS) from the target. BFS is ideal for finding the shortest path in an unweighted graph, which is what our tree has become. We use a queue to manage the nodes to visit and a set to keep track of visited nodes to prevent cycles (e.g., moving from a child to its parent and back). The BFS proceeds in levels, where each level corresponds to an increase in distance from the target. We continue the BFS until we have reached level k. All nodes at this level are our answer.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { public List<Integer> distanceK(TreeNode root, TreeNode target, int k) { Map<TreeNode, TreeNode> parentMap = new HashMap<>(); findParents(root, null, parentMap); Queue<TreeNode> queue = new LinkedList<>(); Set<TreeNode> visited = new HashSet<>(); queue.offer(target); visited.add(target); int currentDistance = 0; while (!queue.isEmpty()) { if (currentDistance == k) { break; } int levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { TreeNode currentNode = queue.poll(); // Add left child if (currentNode.left != null && !visited.contains(currentNode.left)) { queue.offer(currentNode.left); visited.add(currentNode.left); } // Add right child if (currentNode.right != null && !visited.contains(currentNode.right)) { queue.offer(currentNode.right); visited.add(currentNode.right); } // Add parent TreeNode parent = parentMap.get(currentNode); if (parent != null && !visited.contains(parent)) { queue.offer(parent); visited.add(parent); } } currentDistance++; } List<Integer> result = new ArrayList<>(); while (!queue.isEmpty()) { result.add(queue.poll().val); } return result; } private void findParents(TreeNode node, TreeNode parent, Map<TreeNode, TreeNode> parentMap) { if (node == null) { return; } parentMap.put(node, parent); findParents(node.left, node, parentMap); findParents(node.right, node, parentMap); }}Complexity
Time
O(N), where N is the number of nodes in the tree. The `findParents` traversal takes O(N) time. The BFS also visits each node and edge at most once, which takes O(N) time.
Space
O(N), where N is the number of nodes in the tree. The `parentMap` can store up to N-1 entries. The `visited` set can store up to N nodes. The `queue` in the worst case (a complete binary tree) can hold up to N/2 nodes. Thus, the space complexity is dominated by these data structures.
Trade-offs
Pros
Conceptually simple and easy to understand if familiar with graph traversal algorithms like BFS.
Correctly handles all cases by converting the tree into a general graph.
Cons
Requires extra space proportional to the number of nodes (O(N)) for the parent map, visited set, and queue, which can be significant for large trees.
This approach avoids creating any auxiliary graph-like data structures (like a parent map or an adjacency list) and instead uses the call stack of a recursive Depth-First Search (DFS) to implicitly handle the upward traversal from the target node. It finds the nodes at distance k in a single pass over the tree.
Algorithm
- Step 1: Main Recursive Function
dfs(node, target, k)- Base Case: If
nodeis null, return -1 (target not found). - Target Found: If
nodeis thetarget, we must find all nodes in its own subtree at distancek. Call a helperfindSubtreeNodes(node, k). Then, return 0 to signal the distance from the target to itself. - Recursive Step: Recursively call
dfson the left and right children.leftDist = dfs(node.left, ...)rightDist = dfs(node.right, ...)
- Base Case: If
- Step 2: Process Distances and Propagate Upwards
- Target in Left Subtree: If
leftDistis not -1: a. The distance from the currentnodeto thetargetisleftDist + 1. b. If this distance equalsk, addnode.valto the results. c. Search in the other (right) subtree for nodes. The required distance isk - (leftDist + 1) - 1. CallfindSubtreeNodes(node.right, k - leftDist - 2). d. ReturnleftDist + 1to the parent. - Target in Right Subtree: Perform a symmetric operation if
rightDistis not -1. - Target Not Found: If both
leftDistandrightDistare -1, return -1.
- Target in Left Subtree: If
- Step 3: Helper
findSubtreeNodes(node, dist)- A simple DFS that collects all nodes at a specific depth
distfromnode. - Base Case: If
nodeis null ordist < 0, return. - Distance Match: If
dist == 0, addnode.valto results. - Recurse: Call
findSubtreeNodeson children withdist - 1.
- A simple DFS that collects all nodes at a specific depth
Walkthrough
The core of this method is a recursive function, let's call it dfs, that traverses the tree and serves two purposes. First, it searches for the target node. Second, it uses its return value to communicate the distance from the current node to the target back up the call stack.
When the dfs function is at a given node:
- If
nodeis thetarget, it means we've found the starting point. We then trigger a separate downward search (findSubtreeNodes) from thistargetnode to find all its descendants at distancek. The function returns 0 to its parent, indicating the distance. - If
nodeis not thetarget, it recursively callsdfson its children. If a recursive call (say, from the left child) returns a non-negative valued, it means thetargetis in the left subtree at a distancedfrom the left child. Therefore, thetargetis at distanced + 1from the currentnode. - Now, from this current
node, we know the path to thetargetgoes through its left child. We check if thenodeitself is at distancek. We also need to find nodes in the other subtree (the right one) that are at the required distance. The path to these nodes goes up to the currentnodeand then down into the right subtree. We can calculate the remaining distance and callfindSubtreeNodeson the right child.
This process effectively explores all paths from the target: downwards into its own subtree, and upwards to each ancestor, branching off into the ancestor's other subtrees.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */class Solution { List<Integer> result; public List<Integer> distanceK(TreeNode root, TreeNode target, int k) { result = new ArrayList<>(); dfs(root, target, k); return result; } // Returns the distance from node to target. // Returns -1 if target is not in the subtree of node. private int dfs(TreeNode node, TreeNode target, int k) { if (node == null) { return -1; } if (node == target) { // Found the target node. Now find all nodes in its subtree at distance k. findSubtreeNodes(node, k); return 0; // Distance from target to itself is 0. } // Recurse on left subtree int leftDist = dfs(node.left, target, k); if (leftDist != -1) { // Target is in the left subtree int distFromNodeToTarget = leftDist + 1; if (distFromNodeToTarget == k) { result.add(node.val); } // Search in the right subtree for nodes at the required distance findSubtreeNodes(node.right, k - distFromNodeToTarget - 1); return distFromNodeToTarget; } // Recurse on right subtree int rightDist = dfs(node.right, target, k); if (rightDist != -1) { // Target is in the right subtree int distFromNodeToTarget = rightDist + 1; if (distFromNodeToTarget == k) { result.add(node.val); } // Search in the left subtree for nodes at the required distance findSubtreeNodes(node.left, k - distFromNodeToTarget - 1); return distFromNodeToTarget; } return -1; // Target not found in this subtree } // Helper to find nodes in the subtree of 'node' at a specific distance 'dist' private void findSubtreeNodes(TreeNode node, int dist) { if (node == null || dist < 0) { return; } if (dist == 0) { result.add(node.val); return; } findSubtreeNodes(node.left, dist - 1); findSubtreeNodes(node.right, dist - 1); }}Complexity
Time
O(N), where N is the number of nodes. Each node is visited a constant number of times. The main `dfs` function and the helper `findSubtreeNodes` together ensure that every node is processed at most twice.
Space
O(H), where H is the height of the tree. This space is used by the recursion call stack. In the worst case of a skewed tree, H can be N, making the space complexity O(N). However, for a balanced tree, H is O(log N), making this approach significantly more space-efficient.
Trade-offs
Pros
More space-efficient than the graph-based approach, especially for balanced trees, as its space complexity is tied to the tree's height.
Solves the problem in a single pass without needing to pre-process the tree to build parent pointers or an adjacency list.
Cons
The recursive logic can be more complex to reason about compared to the straightforward graph traversal.
In the worst-case (a skewed tree), the space complexity is still O(N) due to the recursion depth.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ class Solution { private Map < TreeNode , TreeNode > p ; private Set < Integer > vis ; private List < Integer > ans ; public List < Integer > distanceK ( TreeNode root , TreeNode target , int k ) { p = new HashMap <>(); vis = new HashSet <>(); ans = new ArrayList <>(); parents ( root , null ); dfs ( target , k ); return ans ; } private void parents ( TreeNode root , TreeNode prev ) { if ( root == null ) { return ; } p . put ( root , prev ); parents ( root . left , root ); parents ( root . right , root ); } private void dfs ( TreeNode root , int k ) { if ( root == null || vis . contains ( root . val )) { return ; } vis . add ( root . val ); if ( k == 0 ) { ans . add ( root . val ); return ; } dfs ( root . left , k - 1 ); dfs ( root . right , k - 1 ); dfs ( p . get ( root ), k - 1 ); } }/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: unordered_map < TreeNode * , TreeNode *> p ; unordered_set < int > vis ; vector < int > ans ; vector < int > distanceK ( TreeNode * root , TreeNode * target , int k ) { parents ( root , nullptr ); dfs ( target , k ); return ans ; } void parents ( TreeNode * root , TreeNode * prev ) { if ( ! root ) return ; p [ root ] = prev ; parents ( root -> left , root ); parents ( root -> right , root ); } void dfs ( TreeNode * root , int k ) { if ( ! root || vis . count ( root -> val )) return ; vis . insert ( root -> val ); if ( k == 0 ) { ans . push_back ( root -> val ); return ; } dfs ( root -> left , k - 1 ); dfs ( root -> right , k - 1 ); dfs ( p [ root ], k - 1 ); } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution : def distanceK ( self , root : TreeNode , target : TreeNode , k : int ) -> List [ int ]: def parents ( root , prev ): nonlocal p if root is None : return p [ root ] = prev parents ( root . left , root ) parents ( root . right , root ) def dfs ( root , k ): nonlocal ans , vis if root is None or root . val in vis : return vis . add ( root . val ) if k == 0 : ans . append ( root . val ) return dfs ( root . left , k - 1 ) dfs ( root . right , k - 1 ) dfs ( p [ root ], k - 1 ) p = {} parents ( root , None ) ans = [] vis = set () dfs ( target , k ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.