Given a binary tree where each node has a unique value and a target integer k
, find the value of the nearest leaf node to the target k
. A leaf node is a node with no children. "Nearest" means the minimum number of edges to traverse from the target node to a leaf node.
This solution uses a two-step approach:
Depth-First Search (DFS) to build an adjacency graph: The DFS traversal creates an undirected graph representing the tree's connections. Each node is connected to its parent, left child, and right child. This allows us to easily traverse the tree from any node, regardless of the parent-child relationship.
Breadth-First Search (BFS) to find the closest leaf: Starting from the target node (k
), BFS systematically explores the graph level by level. The first leaf node encountered is the closest leaf.
DFS:
dfs
function traverses the tree.g
) connecting each node to its parent, left child, and right child. The graph is implemented using a dictionary (Python) or map (Java, C++, Go) where keys are nodes and values are lists of adjacent nodes.fa
) as an argument to connect each node to its parent.BFS:
q
with the target node (the node with value k
).vis
set to keep track of visited nodes during the BFS traversal. This prevents cycles and ensures efficient search.q
.q
and mark them as visited in vis
.g
and the vis
set. In the worst case (a skewed tree), the queue in BFS can also take O(N) space.The code implementations below demonstrate the algorithm in different programming languages. They all follow the same core logic. Note that the specific data structures (e.g., defaultdict
, HashMap
, unordered_map
) might differ slightly across languages.
from collections import defaultdict, deque
# ... (TreeNode definition) ...
class Solution:
def findClosestLeaf(self, root: Optional[TreeNode], k: int) -> int:
# DFS to build the graph
g = defaultdict(list)
def dfs(root, fa):
if root:
g[root].append(fa)
g[fa].append(root)
dfs(root.left, root)
dfs(root.right, root)
dfs(root, None)
# BFS to find the closest leaf
q = deque([node for node in g if node and node.val == k])
vis = {node for node in q} # Use a set for efficient membership checking
while q:
node = q.popleft()
if not node.left and not node.right: # Leaf node
return node.val
for neighbor in g[node]:
if neighbor not in vis:
vis.add(neighbor)
q.append(neighbor)
import java.util.*;
// ... (TreeNode definition) ...
class Solution {
public int findClosestLeaf(TreeNode root, int k) {
Map<TreeNode, List<TreeNode>> g = new HashMap<>();
dfs(root, null, g);
Queue<TreeNode> q = new LinkedList<>();
Set<TreeNode> vis = new HashSet<>();
for (TreeNode node : g.keySet()) {
if (node != null && node.val == k) {
q.offer(node);
vis.add(node);
break;
}
}
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node.left == null && node.right == null) {
return node.val;
}
for (TreeNode neighbor : g.get(node)) {
if (!vis.contains(neighbor)) {
vis.add(neighbor);
q.offer(neighbor);
}
}
}
return -1; //Should not reach here if k exists.
}
private void dfs(TreeNode root, TreeNode parent, Map<TreeNode, List<TreeNode>> g) {
if (root == null) return;
g.computeIfAbsent(root, k -> new ArrayList<>()).add(parent);
if(parent != null) g.computeIfAbsent(parent, k -> new ArrayList<>()).add(root);
dfs(root.left, root, g);
dfs(root.right, root, g);
}
}
#include <unordered_map>
#include <vector>
#include <queue>
#include <unordered_set>
// ... (TreeNode definition) ...
class Solution {
public:
int findClosestLeaf(TreeNode* root, int k) {
unordered_map<TreeNode*, vector<TreeNode*>> g;
dfs(root, nullptr, g);
queue<TreeNode*> q;
unordered_set<TreeNode*> vis;
for (auto const& [node, adj] : g) {
if (node && node->val == k) {
q.push(node);
vis.insert(node);
break;
}
}
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
if (!node->left && !node->right) {
return node->val;
}
for (TreeNode* neighbor : g[node]) {
if (vis.find(neighbor) == vis.end()) {
vis.insert(neighbor);
q.push(neighbor);
}
}
}
return -1; // Should not reach here
}
private:
void dfs(TreeNode* root, TreeNode* parent, unordered_map<TreeNode*, vector<TreeNode*>>& g) {
if (!root) return;
g[root].push_back(parent);
if (parent) g[parent].push_back(root);
dfs(root->left, root, g);
dfs(root->right, root, g);
}
};
// ... (TreeNode definition) ...
func findClosestLeaf(root *TreeNode, k int) int {
g := make(map[*TreeNode][]*TreeNode)
var dfs func(*TreeNode, *TreeNode)
dfs = func(root, parent *TreeNode) {
if root == nil {
return
}
g[root] = append(g[root], parent)
if parent != nil {
g[parent] = append(g[parent], root)
}
dfs(root.Left, root)
dfs(root.Right, root)
}
dfs(root, nil)
q := []*TreeNode{}
vis := make(map[*TreeNode]bool)
for node := range g {
if node != nil && node.Val == k {
q = append(q, node)
vis[node] = true
break
}
}
for len(q) > 0 {
node := q[0]
q = q[1:]
if node.Left == nil && node.Right == nil {
return node.Val
}
for _, neighbor := range g[node] {
if !vis[neighbor] {
vis[neighbor] = true
q = append(q, neighbor)
}
}
}
return -1 //Should not reach here
}
These code examples implement the DFS + BFS approach efficiently to solve the problem. Remember to handle edge cases (e.g., empty tree, target node not found). The choice of language primarily affects the syntax and specific data structures used, but the core algorithmic steps remain consistent.