272. Closest Binary Search Tree Value II 🔒
Problem Description
You are given a binary search tree (BST), a target value, and an integer k
. Your task is to find the k
values in the BST that are closest to the target value.
The problem requires you to:
- Traverse the BST and examine all node values
- Calculate the distance (absolute difference) between each node value and the target
- Return exactly
k
values that have the smallest distances to the target - The values can be returned in any order
Key points about the problem:
- The tree is a valid BST, meaning left subtree values are less than the root, and right subtree values are greater than the root
- The target can be any floating-point number (not necessarily in the tree)
- You are guaranteed that there is exactly one unique set of
k
closest values (no ties at the k-th position)
For example, if you have a BST with values [1, 2, 3, 4, 5]
, target = 3.7
, and k = 2
, the two closest values would be [3, 4]
since they have the smallest absolute differences from 3.7 (distances of 0.7 and 0.3 respectively).
The solution uses an in-order traversal of the BST (which visits nodes in ascending order) combined with a deque (double-ended queue) to maintain a sliding window of the k
closest values. As it traverses:
- It first fills the deque with the first
k
values - For subsequent values, it compares the distance of the new value with the distance of the leftmost value in the deque
- If the new value is closer, it removes the leftmost value and adds the new value
- The traversal can terminate early when values become too far from the target
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary search tree is a special type of graph where each node has at most two children (left and right). The tree structure represents relationships between nodes through parent-child connections.
Is it a tree?
- Yes: The problem explicitly states we're working with a Binary Search Tree (BST), which is a hierarchical tree data structure.
DFS
- The flowchart leads us directly to DFS (Depth-First Search) when we confirm it's a tree structure.
Conclusion: The flowchart suggests using a DFS approach for this problem.
This makes perfect sense for "Closest Binary Search Tree Value II" because:
- We need to traverse the entire tree (or most of it) to examine all node values
- DFS allows us to leverage the BST property - we can perform an in-order traversal which visits nodes in sorted order
- The sorted order from in-order DFS traversal helps us maintain a sliding window of the k closest values efficiently
- We can potentially optimize by terminating the traversal early once we know remaining values will be farther from the target
The solution indeed uses DFS through recursive in-order traversal, visiting left subtree first, then processing the current node, and finally the right subtree. This pattern is the classic DFS approach for tree problems.
Intuition
The key insight comes from understanding two important properties: the BST structure and the nature of finding "k closest" values.
In a BST, an in-order traversal gives us values in sorted ascending order. This is crucial because when values are sorted, the closest values to any target must be contiguous in this sorted sequence. Think about it - if you have sorted values [1, 2, 3, 4, 5]
and target 3.2
, the closest values will form a continuous window around the target, not scattered randomly.
This leads us to a sliding window approach. As we traverse the tree in-order:
- We maintain a window of size
k
containing our current best candidates - For each new value we encounter, we compare distances to decide if it should replace an existing value in our window
The clever part is using a deque (double-ended queue) to maintain this window. Why? Because as we traverse in ascending order:
- Early in the traversal, all values are smaller than or equal to the target
- Later in the traversal, values become larger than the target
- The leftmost value in our deque (the smallest) will always be the farthest from the target among our current candidates when we encounter larger values
This means we only need to compare the new value's distance with the leftmost value's distance. If the new value is closer, we slide the window by removing from the left and adding to the right.
An optimization emerges: once the incoming values become so large that they're farther from the target than our leftmost value, all subsequent values (being even larger in the sorted order) will also be too far. This allows us to terminate the traversal early rather than visiting the entire tree.
This approach elegantly combines the BST property with a sliding window technique to solve the problem in O(n)
time with O(k)
extra space for the deque.
Solution Approach
The implementation uses DFS with in-order traversal combined with a deque to maintain the k closest values:
Data Structures Used:
deque
: A double-ended queue to maintain a sliding window of k values- Recursive call stack for DFS traversal
Algorithm Steps:
-
Initialize the deque: Create an empty deque
q
that will store up tok
values. -
Perform in-order DFS traversal: The
dfs
function recursively traverses the tree:dfs(root.left) # Visit left subtree first # Process current node dfs(root.right) # Visit right subtree last
-
Process each node during traversal:
-
If deque has less than k elements: Simply append the current value
if len(q) < k: q.append(root.val)
-
If deque is full (has k elements):
- Compare the distance of the current value with the leftmost value in deque
- Since we're traversing in ascending order, the leftmost value is the smallest
- If
abs(root.val - target) >= abs(q[0] - target)
, we can stop early because:- The current value is farther from target than our worst candidate
- All subsequent values (being larger) will be even farther
- Otherwise, slide the window:
q.popleft() # Remove the farthest value (leftmost) q.append(root.val) # Add the closer value
-
-
Early termination optimization: When we encounter a value that's farther from the target than our worst candidate in the deque, we return immediately. This prevents unnecessary traversal of the remaining larger values in the tree.
-
Return the result: Convert the deque to a list and return.
Time Complexity: O(n)
in worst case where we traverse all nodes, but often better due to early termination.
Space Complexity: O(k)
for the deque plus O(h)
for the recursion stack, where h is the height of the tree.
The elegance of this solution lies in leveraging the sorted nature of in-order traversal to maintain a sliding window efficiently, requiring only one comparison per node after the deque is full.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding the k=2 closest values to target=3.8 in this BST:
4 / \ 2 5 / \ 1 3
Initial Setup:
- Target = 3.8
- k = 2
- deque q = []
In-order Traversal (visits nodes in ascending order: 1→2→3→4→5):
Step 1: Visit node 1
- q is empty (len < k), so append 1
- q = [1]
Step 2: Visit node 2
- q has 1 element (len < k), so append 2
- q = [1, 2]
- Now q is full with k=2 elements
Step 3: Visit node 3
- q is full, need to compare distances:
- Distance of new value 3 from target: |3 - 3.8| = 0.8
- Distance of leftmost value 1 from target: |1 - 3.8| = 2.8
- Since 0.8 < 2.8, value 3 is closer than value 1
- Slide the window: remove 1 from left, add 3 to right
- q = [2, 3]
Step 4: Visit node 4
- q is full, compare distances:
- Distance of new value 4 from target: |4 - 3.8| = 0.2
- Distance of leftmost value 2 from target: |2 - 3.8| = 1.8
- Since 0.2 < 1.8, value 4 is closer than value 2
- Slide the window: remove 2 from left, add 4 to right
- q = [3, 4]
Step 5: Visit node 5
- q is full, compare distances:
- Distance of new value 5 from target: |5 - 3.8| = 1.2
- Distance of leftmost value 3 from target: |3 - 3.8| = 0.8
- Since 1.2 > 0.8, value 5 is NOT closer than value 3
- Early termination triggered! Since we're traversing in ascending order and value 5 is already farther than our worst candidate (3), all subsequent values would be even farther.
- Return immediately with q = [3, 4]
Result: [3, 4] are the 2 closest values to 3.8
The key insight is that the deque maintains a sliding window of the k closest values seen so far, and the sorted nature of in-order traversal allows us to stop early once values become too far from the target.
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 deque
9from typing import List, Optional
10
11class Solution:
12 def closestKValues(self, root: Optional[TreeNode], target: float, k: int) -> List[int]:
13 """
14 Find k values in a BST that are closest to the target value.
15
16 Uses in-order traversal to visit nodes in sorted order and maintains
17 a sliding window of k closest values using a deque.
18
19 Args:
20 root: Root of the binary search tree
21 target: Target value to find closest values to
22 k: Number of closest values to return
23
24 Returns:
25 List of k values closest to the target
26 """
27
28 def inorder_traverse(node: Optional[TreeNode]) -> None:
29 """
30 Perform in-order traversal of the BST while maintaining
31 a window of k closest values to the target.
32 """
33 if node is None:
34 return
35
36 # Traverse left subtree first (smaller values in BST)
37 inorder_traverse(node.left)
38
39 # Process current node
40 if len(closest_values_queue) < k:
41 # Queue not full yet, add current value
42 closest_values_queue.append(node.val)
43 else:
44 # Queue is full, check if current value is closer than the first value in queue
45 # Since we're doing in-order traversal, values are processed in ascending order
46 # The first element in queue is the smallest and potentially furthest from target
47 if abs(node.val - target) >= abs(closest_values_queue[0] - target):
48 # Current value is not closer than the furthest value in queue
49 # All subsequent values will be even further (due to BST property)
50 return
51
52 # Current value is closer, remove the furthest and add current
53 closest_values_queue.popleft()
54 closest_values_queue.append(node.val)
55
56 # Traverse right subtree (larger values in BST)
57 inorder_traverse(node.right)
58
59 # Initialize deque to store k closest values
60 closest_values_queue = deque()
61
62 # Start in-order traversal from root
63 inorder_traverse(root)
64
65 # Convert deque to list and return
66 return list(closest_values_queue)
67
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
18 // List to store the k closest values to target
19 private List<Integer> result;
20
21 // Target value to find closest values to
22 private double target;
23
24 // Number of closest values to find
25 private int k;
26
27 /**
28 * Finds k values in the BST that are closest to the target value.
29 *
30 * @param root The root node of the binary search tree
31 * @param target The target value to find closest values to
32 * @param k The number of closest values to return
33 * @return A list containing k closest values to the target
34 */
35 public List<Integer> closestKValues(TreeNode root, double target, int k) {
36 // Initialize result list as LinkedList for efficient removal from front
37 this.result = new LinkedList<>();
38 this.target = target;
39 this.k = k;
40
41 // Perform in-order traversal to process nodes in sorted order
42 inorderTraversal(root);
43
44 return result;
45 }
46
47 /**
48 * Performs in-order traversal of the BST and maintains a sliding window
49 * of k closest values to the target.
50 *
51 * @param node The current node being processed
52 */
53 private void inorderTraversal(TreeNode node) {
54 // Base case: if node is null, return
55 if (node == null) {
56 return;
57 }
58
59 // Process left subtree first (smaller values in BST)
60 inorderTraversal(node.left);
61
62 // Process current node
63 if (result.size() < k) {
64 // If we haven't collected k values yet, add current value
65 result.add(node.val);
66 } else {
67 // We have k values, check if current value is closer than the first
68 // value in our result list (which is the furthest among collected values)
69
70 // If current node's distance to target is greater than or equal to
71 // the first element's distance, we can stop (early termination)
72 // because all subsequent nodes will be even further from target
73 if (Math.abs(node.val - target) >= Math.abs(result.get(0) - target)) {
74 return;
75 }
76
77 // Current value is closer, remove the furthest value and add current
78 result.remove(0);
79 result.add(node.val);
80 }
81
82 // Process right subtree (larger values in BST)
83 inorderTraversal(node.right);
84 }
85}
86
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 {
13private:
14 queue<int> valueQueue; // Queue to maintain k closest values
15 double targetValue; // Target value to find closest elements to
16 int kCount; // Number of closest values to find
17
18public:
19 /**
20 * Find k values in BST that are closest to target
21 * @param root - Root of the binary search tree
22 * @param target - Target value to find closest elements to
23 * @param k - Number of closest values to return
24 * @return Vector containing k closest values to target
25 */
26 vector<int> closestKValues(TreeNode* root, double target, int k) {
27 // Initialize member variables
28 this->targetValue = target;
29 this->kCount = k;
30
31 // Perform in-order traversal to process nodes in sorted order
32 inorderTraversal(root);
33
34 // Transfer queue contents to result vector
35 vector<int> result;
36 while (!valueQueue.empty()) {
37 result.push_back(valueQueue.front());
38 valueQueue.pop();
39 }
40
41 return result;
42 }
43
44private:
45 /**
46 * In-order traversal of BST to maintain k closest values
47 * Uses a sliding window approach with a queue
48 * @param node - Current node being processed
49 */
50 void inorderTraversal(TreeNode* node) {
51 // Base case: null node
52 if (!node) {
53 return;
54 }
55
56 // Process left subtree first (smaller values in BST)
57 inorderTraversal(node->left);
58
59 // Process current node
60 if (valueQueue.size() < kCount) {
61 // Queue not full yet, add current value
62 valueQueue.push(node->val);
63 } else {
64 // Queue is full, check if current value is closer than the oldest value
65 // Since we're doing in-order traversal, values are processed in ascending order
66 // If current value is not closer, all subsequent values won't be closer either
67 if (abs(node->val - targetValue) >= abs(valueQueue.front() - targetValue)) {
68 return; // Early termination optimization for BST property
69 }
70
71 // Current value is closer, replace oldest value
72 valueQueue.pop();
73 valueQueue.push(node->val);
74 }
75
76 // Process right subtree (larger values in BST)
77 inorderTraversal(node->right);
78 }
79};
80
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// Global variables to maintain state during traversal
16let valueQueue: number[] = []; // Array used as queue to maintain k closest values
17let targetValue: number = 0; // Target value to find closest elements to
18let kCount: number = 0; // Number of closest values to find
19
20/**
21 * Find k values in BST that are closest to target
22 * @param root - Root of the binary search tree
23 * @param target - Target value to find closest elements to
24 * @param k - Number of closest values to return
25 * @return Array containing k closest values to target
26 */
27function closestKValues(root: TreeNode | null, target: number, k: number): number[] {
28 // Initialize global variables for this execution
29 valueQueue = [];
30 targetValue = target;
31 kCount = k;
32
33 // Perform in-order traversal to process nodes in sorted order
34 inorderTraversal(root);
35
36 // Return the queue contents as result
37 return valueQueue;
38}
39
40/**
41 * In-order traversal of BST to maintain k closest values
42 * Uses a sliding window approach with a queue
43 * @param node - Current node being processed
44 */
45function inorderTraversal(node: TreeNode | null): void {
46 // Base case: null node
47 if (!node) {
48 return;
49 }
50
51 // Process left subtree first (smaller values in BST)
52 inorderTraversal(node.left);
53
54 // Process current node
55 if (valueQueue.length < kCount) {
56 // Queue not full yet, add current value
57 valueQueue.push(node.val);
58 } else {
59 // Queue is full, check if current value is closer than the oldest value
60 // Since we're doing in-order traversal, values are processed in ascending order
61 // If current value is not closer, all subsequent values won't be closer either
62 if (Math.abs(node.val - targetValue) >= Math.abs(valueQueue[0] - targetValue)) {
63 return; // Early termination optimization for BST property
64 }
65
66 // Current value is closer, replace oldest value
67 valueQueue.shift(); // Remove first element (oldest value)
68 valueQueue.push(node.val); // Add current value
69 }
70
71 // Process right subtree (larger values in BST)
72 inorderTraversal(node.right);
73}
74
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree.
The algorithm performs an in-order traversal of the BST using DFS. In the worst case, it visits every node in the tree exactly once. Even though there's an early termination condition (if abs(root.val - target) >= abs(q[0] - target): return
), this optimization only helps when we've found k
values and the remaining nodes are farther from the target. In the worst case scenario where all nodes are very close to the target or when the target is at one extreme of the tree's value range, we still need to traverse all n
nodes.
Space Complexity: O(h + k)
where h
is the height of the tree and k
is the number of closest values to find.
The space complexity consists of two components:
- The recursive call stack for DFS traversal uses
O(h)
space, whereh
is the height of the tree. In the worst case of a skewed tree,h = n
, making itO(n)
. In the best case of a balanced tree,h = log(n)
. - The deque
q
stores at mostk
elements at any time, contributingO(k)
space. - The final list conversion also takes
O(k)
space, but this doesn't add to the asymptotic complexity.
Therefore, the total space complexity is O(h + k)
.
Common Pitfalls
1. Incorrect Early Termination Logic
The Pitfall: A common mistake is using the wrong comparison for early termination. Some might incorrectly write:
if abs(node.val - target) > abs(closest_values_queue[0] - target):
return
Instead of using >=
. This subtle difference can cause the algorithm to miss valid candidates when the distances are equal.
Why It's Wrong: When distances are equal, we should still terminate because:
- We already have k values with distances less than or equal to the current distance
- Since we're traversing in ascending order, all subsequent values will have distances greater than or equal to the current one
- Missing the equal case means unnecessary traversal continues
The Fix:
Always use >=
in the comparison to handle the edge case where distances are equal:
if abs(node.val - target) >= abs(closest_values_queue[0] - target):
return
2. Assuming Target is an Integer
The Pitfall: Treating the target as an integer when it's actually a float, which can lead to incorrect distance calculations:
# Wrong: assuming target is int
def closestKValues(self, root: Optional[TreeNode], target: int, k: int) -> List[int]:
Why It's Wrong: The target can be any floating-point number (e.g., 3.7, 2.5), and treating it as an integer would either cause type errors or incorrect results due to implicit conversion.
The Fix: Always declare target as float and handle floating-point arithmetic properly:
def closestKValues(self, root: Optional[TreeNode], target: float, k: int) -> List[int]:
3. Not Handling the Case When k = 0
The Pitfall: The code doesn't explicitly handle when k = 0, which should return an empty list:
# Current code will work but isn't explicit about this edge case
if len(closest_values_queue) < k: # When k=0, this never executes
closest_values_queue.append(node.val)
Why It Matters: While the current implementation technically handles k=0 correctly (returns empty list), it's not immediately obvious and could confuse maintainers.
The Fix: Add an explicit check at the beginning:
def closestKValues(self, root: Optional[TreeNode], target: float, k: int) -> List[int]:
if k == 0 or root is None:
return []
# Rest of the implementation...
4. Misunderstanding the Sliding Window Direction
The Pitfall: Some might think they should remove from the right side of the deque instead of the left:
# Wrong approach: closest_values_queue.pop() # Removes from right closest_values_queue.append(node.val)
Why It's Wrong: In in-order traversal, we visit nodes in ascending order. The leftmost element in the deque is the smallest value, and as we encounter larger values, we need to check if they're closer to the target than the smallest (leftmost) value. Removing from the right would break this logic.
The Fix: Always remove from the left when the deque is full:
closest_values_queue.popleft() # Correct: removes the smallest/leftmost value closest_values_queue.append(node.val)
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
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!