742. Closest Leaf in a Binary Tree 🔒
Problem Description
You are given a binary tree where every node has a unique value, and you need to find the value of the nearest leaf node to a target value k
.
The problem asks you to:
- Start from a node with value
k
in the tree - Find the leaf node that can be reached with the minimum number of edges from this starting node
- Return the value of that nearest leaf node
Key definitions:
- A leaf node is a node that has no children (both left and right children are null)
- Nearest to a leaf means the shortest path (minimum number of edges) needed to travel from the target node to reach any leaf
- The path can go through parent nodes, not just downward to children
For example, if you have a target node k
in the middle of the tree, the nearest leaf might be:
- A leaf in the subtree below the target node
- A leaf that requires going up to parent nodes and then down another branch
- The target node itself (if
k
is already a leaf)
The solution approach converts the tree into an undirected graph to allow traversal in any direction (up to parents or down to children), then uses BFS from the target node k
to find the nearest leaf. The first leaf encountered during the BFS traversal will be the nearest one due to the level-by-level nature of BFS.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While the input is a binary tree, the problem requires finding the shortest path to a leaf from a target node. The path can go through parent nodes (not just children), which means we need to treat the tree as an undirected graph where edges can be traversed in both directions.
Is it a tree?
- Yes: The original structure is a binary tree, and even when we convert it to an undirected graph for traversal purposes, it maintains tree properties (connected, acyclic).
DFS
- Yes: We use DFS to traverse the tree and build the undirected graph representation. This allows us to establish parent-child relationships in both directions.
Additional Consideration - Shortest Path:
After using DFS to build the graph, we need to find the shortest path from the target node k
to any leaf. While not explicitly shown in this flowchart path, the problem then requires:
- BFS: After constructing the graph with DFS, we use BFS starting from node
k
to find the nearest leaf, as BFS explores nodes level by level and guarantees finding the shortest path in an unweighted graph.
Conclusion: The flowchart correctly leads us to use DFS for the initial tree traversal and graph construction. The complete solution uses a hybrid approach: DFS for graph building, followed by BFS for finding the nearest leaf node from the target.
Intuition
The key insight is recognizing that finding the nearest leaf from a target node in a binary tree requires considering paths that go upward (to parent nodes) as well as downward (to children). In a standard tree representation, we can only traverse downward from parent to child, which limits our search space.
Consider a scenario where the target node k
is deep in the left subtree, but there's a leaf node just two edges away through its parent and the parent's right child. If we only search downward from k
, we might find a leaf that's much farther away in k
's subtree.
This limitation leads us to transform the problem: instead of working with a tree where we can only go down, we need a structure that allows bidirectional movement. By converting the tree into an undirected graph, we can traverse from any node to its adjacent nodes (parent, left child, right child).
Once we have this graph representation, finding the nearest leaf becomes a shortest path problem in an unweighted graph. BFS is the natural choice here because:
- It explores nodes level by level, ensuring we find the shortest path first
- The first leaf node we encounter during BFS traversal is guaranteed to be the nearest one
- We don't need complex distance calculations since all edges have equal weight (1)
The DFS phase builds our graph by establishing bidirectional connections between nodes, while the BFS phase efficiently finds the nearest leaf by radiating outward from the target node k
until hitting a leaf node (identified by having no children: node.left == node.right == None
).
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a two-phase approach: graph construction using DFS, followed by shortest path finding using BFS.
Phase 1: Graph Construction with DFS
We start by building an undirected graph representation of the tree using a recursive DFS traversal:
- Initialize a graph
g
usingdefaultdict(list)
where each node maps to its list of adjacent nodes - The
dfs
function takes two parameters: the current node (root
) and its parent node (fa
) - For each node, we add bidirectional edges:
- Add the parent to the current node's adjacency list:
g[root].append(fa)
- Add the current node to the parent's adjacency list:
g[fa].append(root)
- Add the parent to the current node's adjacency list:
- Recursively process left and right children, passing the current node as their parent
This creates an undirected graph where each node can reach its parent and children directly.
Phase 2: Finding Nearest Leaf with BFS
Once the graph is built, we use BFS to find the nearest leaf from the target node k
:
- Initialize a queue
q
with the target node (found by checkingnode.val == k
) - Maintain a visited set
vis
to avoid revisiting nodes - Process nodes level by level from the queue:
- Dequeue a node
- Check if it's a leaf using the condition
node.left == node.right
(both areNone
for leaves) - If it's a leaf, return its value immediately (guaranteed to be the nearest due to BFS)
- Otherwise, add all unvisited adjacent nodes to the queue
Key Implementation Details:
- The graph includes
None
as a node (representing the parent of root), but we filter it out when needed - The leaf check
node.left == node.right
is a concise way to verify both children areNone
- The
while 1
loop continues indefinitely because we're guaranteed to find a leaf (every tree has at least one leaf) - BFS ensures the first leaf encountered is the closest one, as it explores nodes in order of increasing distance from the starting point
The time complexity is O(n)
for both DFS traversal and BFS in the worst case, where n
is the number of nodes. The space complexity is O(n)
for storing the graph and visited set.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Consider this binary tree with target k = 2
:
1 / \ 2 3 / 4 / \ 5 6
Phase 1: Building the Graph with DFS
Starting from root (node 1) with parent None
:
- Process node 1: Add edges
1 ↔ None
- Recursively process left child (node 2) with parent 1:
- Add edges
2 ↔ 1
- Process node 4 with parent 2:
- Add edges
4 ↔ 2
- Process node 5 with parent 4: Add edges
5 ↔ 4
- Process node 6 with parent 4: Add edges
6 ↔ 4
- Add edges
- Add edges
- Recursively process right child (node 3) with parent 1:
- Add edges
3 ↔ 1
- Add edges
Resulting graph adjacency list:
None: [1] 1: [None, 2, 3] 2: [1, 4] 3: [1] 4: [2, 5, 6] 5: [4] 6: [4]
Phase 2: BFS from Target Node k = 2
Starting BFS from node 2:
-
Initialize: queue = [node_2], visited = {node_2}
-
Level 0: Process node 2
- Is it a leaf? No (has left child 4)
- Add unvisited neighbors to queue: [node_1, node_4]
- Mark as visited: {node_2, node_1, node_4}
-
Level 1: Process node 1
- Is it a leaf? No (has children)
- Add unvisited neighbor node 3 to queue: [node_4, node_3]
- Mark as visited: {node_2, node_1, node_4, node_3}
-
Level 1 (continued): Process node 4
- Is it a leaf? No (has children)
- Add unvisited neighbors node 5 and node 6 to queue: [node_3, node_5, node_6]
- Mark as visited: {node_2, node_1, node_4, node_3, node_5, node_6}
-
Level 2: Process node 3
- Is it a leaf? Yes! (no children)
- Return value: 3
The algorithm finds that node 3 is the nearest leaf to target node 2, requiring only 2 edges (2 → 1 → 3). Even though nodes 5 and 6 are also leaves, they would require 3 edges (2 → 4 → 5/6), making node 3 the correct answer.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from collections import defaultdict, deque
9from typing import Optional
10
11class Solution:
12 def findClosestLeaf(self, root: Optional[TreeNode], k: int) -> int:
13 """
14 Find the value of the closest leaf node to a target node with value k.
15
16 Args:
17 root: Root of the binary tree
18 k: Target node value to search from
19
20 Returns:
21 Value of the closest leaf node
22 """
23
24 def build_graph(node: Optional[TreeNode], parent: Optional[TreeNode]) -> None:
25 """
26 Build an undirected graph representation of the tree.
27 Each node is connected to its parent and children.
28
29 Args:
30 node: Current node being processed
31 parent: Parent of the current node
32 """
33 if node:
34 # Add bidirectional edge between node and its parent
35 graph[node].append(parent)
36 graph[parent].append(node)
37
38 # Recursively process left and right subtrees
39 build_graph(node.left, node)
40 build_graph(node.right, node)
41
42 # Initialize graph as adjacency list (node -> list of connected nodes)
43 graph = defaultdict(list)
44
45 # Build the graph from the tree structure
46 build_graph(root, None)
47
48 # Find the starting node with value k and initialize BFS queue
49 queue = deque(node for node in graph if node and node.val == k)
50
51 # Track visited nodes to avoid cycles
52 visited = set(queue)
53
54 # BFS to find the closest leaf node
55 while True:
56 current_node = queue.popleft()
57
58 if current_node:
59 # Check if current node is a leaf (both children are None)
60 if current_node.left == current_node.right: # Both are None
61 return current_node.val
62
63 # Explore all neighbors (parent and children)
64 for neighbor in graph[current_node]:
65 if neighbor not in visited:
66 visited.add(neighbor)
67 queue.append(neighbor)
68
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // Graph representation of the tree where each node maps to its adjacent nodes
18 private Map<TreeNode, List<TreeNode>> graph = new HashMap<>();
19
20 /**
21 * Finds the value of the closest leaf node to the node with value k
22 * @param root The root of the binary tree
23 * @param k The target value to find in the tree
24 * @return The value of the closest leaf node
25 */
26 public int findClosestLeaf(TreeNode root, int k) {
27 // Build undirected graph from the tree
28 buildGraph(root, null);
29
30 // BFS queue and visited set initialization
31 Deque<TreeNode> queue = new LinkedList<>();
32 Set<TreeNode> visited = new HashSet<>();
33
34 // Find the target node with value k and start BFS from it
35 for (TreeNode node : graph.keySet()) {
36 if (node != null && node.val == k) {
37 visited.add(node);
38 queue.offer(node);
39 break;
40 }
41 }
42
43 // BFS to find the closest leaf node
44 while (!queue.isEmpty()) {
45 TreeNode currentNode = queue.poll();
46
47 if (currentNode != null) {
48 // Check if current node is a leaf (both children are null)
49 if (currentNode.left == null && currentNode.right == null) {
50 return currentNode.val;
51 }
52
53 // Explore all adjacent nodes
54 for (TreeNode neighbor : graph.get(currentNode)) {
55 if (visited.add(neighbor)) {
56 queue.offer(neighbor);
57 }
58 }
59 }
60 }
61
62 // This line should never be reached given valid input
63 return -1;
64 }
65
66 /**
67 * DFS to build an undirected graph representation of the tree
68 * @param currentNode The current node being processed
69 * @param parentNode The parent of the current node
70 */
71 private void buildGraph(TreeNode currentNode, TreeNode parentNode) {
72 if (currentNode != null) {
73 // Add bidirectional edge between current node and its parent
74 graph.computeIfAbsent(currentNode, k -> new ArrayList<>()).add(parentNode);
75 graph.computeIfAbsent(parentNode, k -> new ArrayList<>()).add(currentNode);
76
77 // Recursively process left and right children
78 buildGraph(currentNode.left, currentNode);
79 buildGraph(currentNode.right, currentNode);
80 }
81 }
82}
83
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 int findClosestLeaf(TreeNode* root, int k) {
15 // Build an undirected graph representation of the tree
16 // Each node is connected to its parent and children
17 unordered_map<TreeNode*, vector<TreeNode*>> graph;
18
19 // DFS to build the graph by adding bidirectional edges
20 function<void(TreeNode*, TreeNode*)> buildGraph = [&](TreeNode* currentNode, TreeNode* parentNode) {
21 if (currentNode) {
22 // Add edge from current node to parent
23 graph[currentNode].push_back(parentNode);
24 // Add edge from parent to current node
25 graph[parentNode].push_back(currentNode);
26
27 // Recursively process left and right subtrees
28 buildGraph(currentNode->left, currentNode);
29 buildGraph(currentNode->right, currentNode);
30 }
31 };
32
33 // Build the graph starting from root with nullptr as parent
34 buildGraph(root, nullptr);
35
36 // BFS queue to find the closest leaf
37 queue<TreeNode*> bfsQueue;
38 // Set to track visited nodes to avoid cycles
39 unordered_set<TreeNode*> visited;
40
41 // Find the target node with value k and start BFS from it
42 for (auto& [node, neighbors] : graph) {
43 if (node && node->val == k) {
44 bfsQueue.push(node);
45 visited.insert(node);
46 break;
47 }
48 }
49
50 // BFS to find the closest leaf node
51 while (true) {
52 TreeNode* currentNode = bfsQueue.front();
53 bfsQueue.pop();
54
55 if (currentNode) {
56 // Check if current node is a leaf node
57 // A node is a leaf if both left and right children are nullptr
58 if (currentNode->left == currentNode->right) { // Both are nullptr
59 return currentNode->val;
60 }
61
62 // Explore all neighbors (parent and children)
63 for (TreeNode* neighbor : graph[currentNode]) {
64 // Skip if already visited
65 if (visited.count(neighbor)) {
66 continue;
67 }
68 // Add unvisited neighbor to queue
69 bfsQueue.push(neighbor);
70 visited.insert(neighbor);
71 }
72 }
73 }
74 }
75};
76
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.left = (left === undefined ? null : left);
11 this.right = (right === undefined ? null : right);
12 }
13}
14
15/**
16 * Finds the value of the closest leaf node to the node with value k in a binary tree.
17 * @param root - The root of the binary tree
18 * @param k - The value of the target node
19 * @returns The value of the closest leaf node
20 */
21function findClosestLeaf(root: TreeNode | null, k: number): number {
22 // Build an undirected graph representation of the tree
23 // Each node is connected to its parent and children
24 const graph = new Map<TreeNode | null, (TreeNode | null)[]>();
25
26 /**
27 * DFS helper function to build the graph by adding bidirectional edges
28 * @param currentNode - The current node being processed
29 * @param parentNode - The parent of the current node
30 */
31 const buildGraph = (currentNode: TreeNode | null, parentNode: TreeNode | null): void => {
32 if (currentNode) {
33 // Initialize graph entries if they don't exist
34 if (!graph.has(currentNode)) {
35 graph.set(currentNode, []);
36 }
37 if (!graph.has(parentNode)) {
38 graph.set(parentNode, []);
39 }
40
41 // Add edge from current node to parent
42 graph.get(currentNode)!.push(parentNode);
43 // Add edge from parent to current node
44 graph.get(parentNode)!.push(currentNode);
45
46 // Recursively process left and right subtrees
47 buildGraph(currentNode.left, currentNode);
48 buildGraph(currentNode.right, currentNode);
49 }
50 };
51
52 // Build the graph starting from root with null as parent
53 buildGraph(root, null);
54
55 // BFS queue to find the closest leaf
56 const bfsQueue: (TreeNode | null)[] = [];
57 // Set to track visited nodes to avoid cycles
58 const visited = new Set<TreeNode | null>();
59
60 // Find the target node with value k and start BFS from it
61 for (const [node, neighbors] of graph.entries()) {
62 if (node && node.val === k) {
63 bfsQueue.push(node);
64 visited.add(node);
65 break;
66 }
67 }
68
69 // BFS to find the closest leaf node
70 while (bfsQueue.length > 0) {
71 const currentNode = bfsQueue.shift()!;
72
73 if (currentNode) {
74 // Check if current node is a leaf node
75 // A node is a leaf if both left and right children are null
76 if (currentNode.left === null && currentNode.right === null) {
77 return currentNode.val;
78 }
79
80 // Explore all neighbors (parent and children)
81 const neighbors = graph.get(currentNode) || [];
82 for (const neighbor of neighbors) {
83 // Skip if already visited
84 if (visited.has(neighbor)) {
85 continue;
86 }
87 // Add unvisited neighbor to queue
88 bfsQueue.push(neighbor);
89 visited.add(neighbor);
90 }
91 }
92 }
93
94 // This should never be reached if the tree is valid and k exists
95 return -1;
96}
97
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of two main phases:
-
DFS traversal to build the graph: The
dfs
function visits each node exactly once to construct an undirected graph representation of the tree. This takesO(n)
time wheren
is the number of nodes. -
BFS traversal to find the closest leaf: Starting from the node with value
k
, the algorithm performs a BFS traversal. In the worst case, BFS visits all nodes in the graph exactly once (each node is added tovis
set only once and processed only once). This also takesO(n)
time.
Therefore, the total time complexity is O(n) + O(n) = O(n)
.
Space Complexity: O(n)
The space usage includes:
-
Graph representation (
g
): The defaultdict stores adjacency lists for the undirected graph. Each edge is stored twice (once for each direction), and there aren-1
edges in a tree, resulting in2(n-1)
entries. Additionally, the root has a connection toNone
. This usesO(n)
space. -
BFS queue (
q
): In the worst case, the queue can containO(n)
nodes. -
Visited set (
vis
): This set can contain up ton
nodes in the worst case, usingO(n)
space. -
Recursion stack for DFS: The recursive
dfs
function can go as deep as the height of the tree, which isO(n)
in the worst case (skewed tree).
The total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Leaf Node Detection
One of the most common pitfalls is incorrectly identifying leaf nodes during the BFS traversal. The code uses node.left == node.right
to check if both children are None
, but this relies on the assumption that the node is valid and not None
itself.
The Problem:
- When building the graph,
None
is added as a node (representing parent of root) - During BFS, if
None
gets processed from the queue, checkingNone.left
will cause an AttributeError - The current code partially handles this with
if current_node:
but doesn't properly filterNone
from being added to the queue in subsequent iterations
Solution:
# BFS to find the closest leaf node while queue: current_node = queue.popleft() # Skip None nodes entirely if not current_node: continue # Check if current node is a leaf if current_node.left is None and current_node.right is None: return current_node.val # Explore all neighbors, filtering out None for neighbor in graph[current_node]: if neighbor and neighbor not in visited: # Added None check visited.add(neighbor) queue.append(neighbor)
2. Target Node Not Found
The code assumes the target value k
exists in the tree, but if it doesn't, the initial queue will be empty, leading to an error when trying to popleft()
from an empty deque.
Solution:
# Find the starting node with value k start_nodes = [node for node in graph if node and node.val == k] if not start_nodes: # Handle case where k doesn't exist in tree # Could return -1, raise exception, or handle as needed return -1 queue = deque(start_nodes)
3. Memory Inefficiency with Graph Storage
The graph stores bidirectional edges for every parent-child relationship, including edges to/from None
. This creates unnecessary memory overhead and complicates traversal logic.
Better Approach:
def build_graph(node: Optional[TreeNode], parent: Optional[TreeNode]) -> None:
if not node:
return
# Only add edges between actual nodes (exclude None parent for root)
if parent:
graph[node].append(parent)
graph[parent].append(node)
# Still need to handle root's children
if node.left:
graph[node].append(node.left)
graph[node.left].append(node)
if node.right:
graph[node].append(node.right)
graph[node.right].append(node)
build_graph(node.left, node)
build_graph(node.right, node)
This approach avoids adding None
to the graph entirely, simplifying the BFS logic and reducing memory usage.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!