2476. Closest Nodes Queries in a Binary Search Tree
Problem Description
You have a binary search tree and need to find the closest values for multiple queries.
Given:
- The
root
of a binary search tree - An array
queries
containingn
positive integers
For each query value in queries[i]
, you need to find two values from the tree:
-
The floor value (
min_i
): The largest value in the tree that is less than or equal toqueries[i]
. If no such value exists, use-1
. -
The ceiling value (
max_i
): The smallest value in the tree that is greater than or equal toqueries[i]
. If no such value exists, use-1
.
Return a 2D array answer
where each answer[i] = [min_i, max_i]
corresponds to the floor and ceiling values for queries[i]
.
Example walkthrough:
If the BST contains values [1, 4, 6, 9]
and you query for 5
:
- The floor (largest value ≤ 5) would be
4
- The ceiling (smallest value ≥ 5) would be
6
- Result:
[4, 6]
If you query for 10
in the same tree:
- The floor (largest value ≤ 10) would be
9
- The ceiling (smallest value ≥ 10) doesn't exist, so it's
-1
- Result:
[9, -1]
The solution leverages the BST property by performing an in-order traversal to get a sorted array of all values, then uses binary search (bisect_left
) to efficiently find the floor and ceiling values for each query.
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), forming a hierarchical structure with nodes and edges.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary search tree, which is a tree data structure.
DFS
- Result: Based on the flowchart, when we have a tree structure, the algorithm directs us to use DFS (Depth-First Search).
Conclusion: The flowchart suggests using DFS for this problem. This aligns perfectly with the solution approach, which uses DFS (specifically, in-order traversal) to traverse the binary search tree and extract all values in sorted order. The DFS traversal visits nodes recursively by going deep into the left subtree first, then processing the current node, and finally traversing the right subtree. This in-order DFS traversal of a BST naturally gives us a sorted array, which can then be used with binary search to efficiently answer the queries for floor and ceiling values.
Intuition
The key insight is recognizing that a binary search tree has a special property: an in-order traversal always gives us values in sorted order. This is because in a BST, all values in the left subtree are smaller than the root, and all values in the right subtree are larger than the root.
When we need to find the floor (largest value ≤ query) and ceiling (smallest value ≥ query) for multiple queries, we could traverse the tree for each query separately. However, this would be inefficient, especially with many queries.
Instead, we can leverage the BST's sorted property. By performing a single DFS in-order traversal upfront, we extract all values into a sorted array. This transforms our tree search problem into an array search problem.
Once we have a sorted array, finding floor and ceiling values becomes a classic binary search problem:
- For the floor value: We need to find the rightmost element that is ≤ the query value
- For the ceiling value: We need to find the leftmost element that is ≥ the query value
The bisect_left
function helps us here. For a query value x
:
bisect_left(nums, x)
gives us the index wherex
would be inserted to keep the array sorted - this is exactly the ceiling positionbisect_left(nums, x + 1) - 1
gives us the index of the largest value ≤x
- this is the floor position
This approach is efficient because we traverse the tree only once (O(n) time) and then each query takes only O(log n) time using binary search, rather than traversing the tree for each query which would take O(n * q) time where q is the number of queries.
Learn more about Tree, Depth-First Search, Binary Search Tree, Binary Search and Binary Tree patterns.
Solution Approach
The solution follows a two-phase approach: first extracting values from the BST, then efficiently answering queries.
Phase 1: Extract Values using DFS In-order Traversal
We implement a recursive DFS function that performs in-order traversal:
def dfs(root: Optional[TreeNode]):
if root is None:
return
dfs(root.left) # Visit left subtree
nums.append(root.val) # Process current node
dfs(root.right) # Visit right subtree
This traversal pattern (left → root → right) guarantees that values are collected in ascending order due to the BST property. We store these values in the nums
array.
Phase 2: Binary Search for Each Query
For each query value x
, we need to find:
- The floor: largest value ≤
x
- The ceiling: smallest value ≥
x
The implementation uses Python's bisect_left
function strategically:
i = bisect_left(nums, x + 1) - 1 # Index for floor j = bisect_left(nums, x) # Index for ceiling
Here's why this works:
bisect_left(nums, x)
returns the leftmost position wherex
could be inserted. Ifx
exists in the array, this gives us its index (the ceiling). Ifx
doesn't exist, it gives us the index of the smallest value greater thanx
.bisect_left(nums, x + 1) - 1
finds wherex + 1
would be inserted, then backs up one position. This gives us the largest value that is ≤x
(the floor).
Handling Edge Cases
The solution checks if the computed indices are valid:
mi = nums[i] if 0 <= i < len(nums) else -1
mx = nums[j] if 0 <= j < len(nums) else -1
- If
i
is out of bounds (negative), no floor exists, so we use-1
- If
j
is out of bounds (≥ array length), no ceiling exists, so we use-1
Time Complexity:
- DFS traversal: O(n) where n is the number of nodes
- Binary search for q queries: O(q × log n)
- Total: O(n + q × log n)
Space Complexity: O(n) for storing the sorted array of values.
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 a concrete example to illustrate the solution approach.
Given BST:
4 / \ 2 7 / / \ 1 5 8
Queries: [3, 6, 9]
Phase 1: Extract values using in-order traversal
Starting the DFS in-order traversal:
- Go to leftmost node (1), visit left subtree (none), add 1, visit right (none)
- Back to node 2, add 2, visit right subtree (none)
- Back to root 4, add 4
- Visit right subtree starting at 7
- Visit left of 7 (node 5), add 5
- Add 7
- Visit right of 7 (node 8), add 8
Result: nums = [1, 2, 4, 5, 7, 8]
(sorted array)
Phase 2: Process each query using binary search
Query 1: x = 3
- Find floor:
bisect_left(nums, 4) - 1 = 2 - 1 = 1
nums[1] = 2
(largest value ≤ 3)
- Find ceiling:
bisect_left(nums, 3) = 2
nums[2] = 4
(smallest value ≥ 3)
- Result:
[2, 4]
Query 2: x = 6
- Find floor:
bisect_left(nums, 7) - 1 = 4 - 1 = 3
nums[3] = 5
(largest value ≤ 6)
- Find ceiling:
bisect_left(nums, 6) = 4
nums[4] = 7
(smallest value ≥ 6)
- Result:
[5, 7]
Query 3: x = 9
- Find floor:
bisect_left(nums, 10) - 1 = 6 - 1 = 5
nums[5] = 8
(largest value ≤ 9)
- Find ceiling:
bisect_left(nums, 9) = 6
- Index 6 is out of bounds (array length is 6), so ceiling = -1
- Result:
[8, -1]
Final Answer: [[2, 4], [5, 7], [8, -1]]
This example demonstrates how the solution efficiently converts the tree problem into an array binary search problem, handling both standard cases and edge cases where no ceiling exists.
Solution Implementation
1from typing import Optional, List
2from bisect import bisect_left
3
4# Definition for a binary tree node.
5class TreeNode:
6 def __init__(self, val=0, left=None, right=None):
7 self.val = val
8 self.left = left
9 self.right = right
10
11class Solution:
12 def closestNodes(
13 self, root: Optional[TreeNode], queries: List[int]
14 ) -> List[List[int]]:
15 """
16 Find the closest smaller-or-equal and greater-or-equal values in a BST for each query.
17
18 Args:
19 root: Root node of the binary search tree
20 queries: List of values to search for
21
22 Returns:
23 List of [min_val, max_val] pairs where:
24 - min_val is the largest value <= query (or -1 if none exists)
25 - max_val is the smallest value >= query (or -1 if none exists)
26 """
27
28 def inorder_traversal(node: Optional[TreeNode]) -> None:
29 """
30 Perform in-order traversal to collect all values in sorted order.
31
32 Args:
33 node: Current node being visited
34 """
35 if node is None:
36 return
37
38 # Traverse left subtree
39 inorder_traversal(node.left)
40
41 # Process current node
42 sorted_values.append(node.val)
43
44 # Traverse right subtree
45 inorder_traversal(node.right)
46
47 # Collect all BST values in sorted order
48 sorted_values = []
49 inorder_traversal(root)
50
51 # Process each query
52 result = []
53 for query_value in queries:
54 # Find the index for the largest value <= query_value
55 # bisect_left(sorted_values, query_value + 1) finds the insertion point for (query_value + 1)
56 # Subtracting 1 gives us the index of the largest value <= query_value
57 max_smaller_or_equal_idx = bisect_left(sorted_values, query_value + 1) - 1
58
59 # Find the index for the smallest value >= query_value
60 min_greater_or_equal_idx = bisect_left(sorted_values, query_value)
61
62 # Get the actual values or -1 if index is out of bounds
63 min_val = sorted_values[max_smaller_or_equal_idx] if 0 <= max_smaller_or_equal_idx < len(sorted_values) else -1
64 max_val = sorted_values[min_greater_or_equal_idx] if 0 <= min_greater_or_equal_idx < len(sorted_values) else -1
65
66 # Append the pair to results
67 result.append([min_val, max_val])
68
69 return result
70
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 // List to store BST values in sorted order
18 private List<Integer> sortedValues = new ArrayList<>();
19
20 /**
21 * Finds the closest nodes for each query in a BST.
22 * For each query, returns [maxValueLessThanOrEqual, minValueGreaterThanOrEqual]
23 *
24 * @param root The root of the binary search tree
25 * @param queries List of query values to search for
26 * @return List of pairs containing the closest smaller and larger values for each query
27 */
28 public List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
29 // Perform in-order traversal to get sorted values from BST
30 inOrderTraversal(root);
31
32 // Result list to store answer pairs for each query
33 List<List<Integer>> result = new ArrayList<>();
34
35 // Process each query
36 for (int queryValue : queries) {
37 // Find the position where (queryValue + 1) would be inserted
38 // This helps find the largest value <= queryValue
39 int indexForLargerValue = Collections.binarySearch(sortedValues, queryValue + 1);
40
41 // Find the position where queryValue would be inserted
42 // This helps find the smallest value >= queryValue
43 int indexForCurrentValue = Collections.binarySearch(sortedValues, queryValue);
44
45 // Convert binary search results to actual indices
46 // If not found, binarySearch returns -(insertion point) - 1
47 int maxLessOrEqualIndex = indexForLargerValue < 0 ?
48 -indexForLargerValue - 2 : indexForLargerValue - 1;
49 int minGreaterOrEqualIndex = indexForCurrentValue < 0 ?
50 -indexForCurrentValue - 1 : indexForCurrentValue;
51
52 // Get the actual values or -1 if index is out of bounds
53 int maxLessOrEqual = (maxLessOrEqualIndex >= 0 && maxLessOrEqualIndex < sortedValues.size()) ?
54 sortedValues.get(maxLessOrEqualIndex) : -1;
55 int minGreaterOrEqual = (minGreaterOrEqualIndex >= 0 && minGreaterOrEqualIndex < sortedValues.size()) ?
56 sortedValues.get(minGreaterOrEqualIndex) : -1;
57
58 // Add the pair to result
59 result.add(List.of(maxLessOrEqual, minGreaterOrEqual));
60 }
61
62 return result;
63 }
64
65 /**
66 * Performs in-order traversal of the BST to collect values in sorted order
67 *
68 * @param node Current node being processed
69 */
70 private void inOrderTraversal(TreeNode node) {
71 // Base case: if node is null, return
72 if (node == null) {
73 return;
74 }
75
76 // Traverse left subtree
77 inOrderTraversal(node.left);
78
79 // Process current node
80 sortedValues.add(node.val);
81
82 // Traverse right subtree
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 {
13public:
14 vector<vector<int>> closestNodes(TreeNode* root, vector<int>& queries) {
15 // Store all values from BST in sorted order
16 vector<int> sortedValues;
17
18 // In-order traversal to get sorted values from BST
19 function<void(TreeNode*)> inorderTraversal = [&](TreeNode* node) {
20 if (!node) {
21 return;
22 }
23
24 // Traverse left subtree
25 inorderTraversal(node->left);
26
27 // Process current node
28 sortedValues.push_back(node->val);
29
30 // Traverse right subtree
31 inorderTraversal(node->right);
32 };
33
34 // Perform in-order traversal to populate sorted values
35 inorderTraversal(root);
36
37 // Result vector to store [min, max] pairs for each query
38 vector<vector<int>> result;
39 int arraySize = sortedValues.size();
40
41 // Process each query
42 for (int& queryValue : queries) {
43 // Find the largest value <= queryValue
44 // lower_bound(x+1) - 1 gives us the last element <= x
45 int maxIndexLessOrEqual = lower_bound(sortedValues.begin(), sortedValues.end(), queryValue + 1)
46 - sortedValues.begin() - 1;
47
48 // Find the smallest value >= queryValue
49 // lower_bound(x) gives us the first element >= x
50 int minIndexGreaterOrEqual = lower_bound(sortedValues.begin(), sortedValues.end(), queryValue)
51 - sortedValues.begin();
52
53 // Get the actual values or -1 if index is out of bounds
54 int minValue = (maxIndexLessOrEqual >= 0 && maxIndexLessOrEqual < arraySize)
55 ? sortedValues[maxIndexLessOrEqual] : -1;
56 int maxValue = (minIndexGreaterOrEqual >= 0 && minIndexGreaterOrEqual < arraySize)
57 ? sortedValues[minIndexGreaterOrEqual] : -1;
58
59 // Add the pair [minValue, maxValue] to result
60 result.push_back({minValue, maxValue});
61 }
62
63 return result;
64 }
65};
66
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Finds the closest nodes in a BST for each query value
17 * @param root - The root of the binary search tree
18 * @param queries - Array of values to find closest nodes for
19 * @returns Array of [min, max] pairs where min <= query <= max
20 */
21function closestNodes(root: TreeNode | null, queries: number[]): number[][] {
22 // Array to store all node values in sorted order
23 const sortedValues: number[] = [];
24
25 /**
26 * Performs in-order traversal to collect all node values in sorted order
27 * @param node - Current node being traversed
28 */
29 const inOrderTraversal = (node: TreeNode | null): void => {
30 if (!node) {
31 return;
32 }
33
34 // Traverse left subtree
35 inOrderTraversal(node.left);
36
37 // Process current node
38 sortedValues.push(node.val);
39
40 // Traverse right subtree
41 inOrderTraversal(node.right);
42 };
43
44 /**
45 * Binary search to find the leftmost position where target can be inserted
46 * @param target - Value to search for
47 * @returns Index of the first element >= target
48 */
49 const binarySearch = (target: number): number => {
50 let left: number = 0;
51 let right: number = sortedValues.length;
52
53 while (left < right) {
54 const mid: number = Math.floor((left + right) / 2);
55
56 if (sortedValues[mid] >= target) {
57 // Target could be at mid or to the left
58 right = mid;
59 } else {
60 // Target must be to the right
61 left = mid + 1;
62 }
63 }
64
65 return left;
66 };
67
68 // Collect all values from the BST in sorted order
69 inOrderTraversal(root);
70
71 // Process each query to find closest nodes
72 const result: number[][] = [];
73
74 for (const queryValue of queries) {
75 // Find the index of the largest element <= queryValue
76 const maxLessOrEqualIndex: number = binarySearch(queryValue + 1) - 1;
77
78 // Find the index of the smallest element >= queryValue
79 const minGreaterOrEqualIndex: number = binarySearch(queryValue);
80
81 // Determine the maximum value <= queryValue (or -1 if none exists)
82 const maxLessOrEqual: number = maxLessOrEqualIndex >= 0
83 ? sortedValues[maxLessOrEqualIndex]
84 : -1;
85
86 // Determine the minimum value >= queryValue (or -1 if none exists)
87 const minGreaterOrEqual: number = minGreaterOrEqualIndex < sortedValues.length
88 ? sortedValues[minGreaterOrEqualIndex]
89 : -1;
90
91 result.push([maxLessOrEqual, minGreaterOrEqual]);
92 }
93
94 return result;
95}
96
Time and Space Complexity
Time Complexity: O(n + m × log n)
The time complexity consists of two main parts:
O(n)
for the DFS traversal to collect all node values from the BST, wheren
is the number of nodes. The DFS visits each node exactly once.O(m × log n)
for processing all queries, wherem
is the number of queries. For each query:bisect_left(nums, x + 1)
takesO(log n)
time (binary search on sorted array)bisect_left(nums, x)
takesO(log n)
time (binary search on sorted array)- Array indexing and appending to result takes
O(1)
time
Since we perform 2 × log n
operations for each of the m
queries, the total query processing time is O(m × log n)
.
Therefore, the overall time complexity is O(n + m × log n)
.
Space Complexity: O(n)
The space complexity breaks down as follows:
O(n)
for thenums
array that stores all node values from the BSTO(n)
for the recursion call stack in the worst case (skewed tree)O(m)
for theans
array storing the results, but this is typically considered as output space
The dominant space usage is O(n)
for storing the sorted array of node values and the recursion stack, giving us an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Search Logic for Floor Values
A common mistake is using bisect_left(sorted_values, query_value)
for finding the floor value and then simply subtracting 1. This fails when the query value exists in the array.
Incorrect approach:
# This fails when query_value exists in the array floor_idx = bisect_left(sorted_values, query_value) - 1
Why it fails: If query_value = 5
and sorted_values = [1, 3, 5, 7]
, bisect_left(sorted_values, 5)
returns 2 (index of 5). Subtracting 1 gives index 1 (value 3), but the correct floor should be 5 itself!
Correct solution:
# Use query_value + 1 to ensure we get the correct floor floor_idx = bisect_left(sorted_values, query_value + 1) - 1
2. Not Handling Empty Trees
The code assumes the tree has at least one node. An empty tree will result in an empty sorted_values
array, causing issues with binary search.
Problem scenario:
root = None queries = [5, 10] # sorted_values will be empty []
Solution - Add explicit check:
def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
if root is None:
return [[-1, -1] for _ in queries]
# Rest of the code...
3. Misunderstanding bisect_left Behavior
Developers often confuse bisect_left
with bisect_right
. Using bisect_right
for ceiling values would give incorrect results when the query value exists in the array.
Incorrect:
# Using bisect_right for ceiling - WRONG! ceiling_idx = bisect_right(sorted_values, query_value)
Why it fails: If query_value = 5
exists in the array, bisect_right
would skip over it and return the index of the next larger value, missing that 5 itself is a valid ceiling.
Correct:
# bisect_left correctly finds the ceiling ceiling_idx = bisect_left(sorted_values, query_value)
4. Off-by-One Errors in Index Validation
A subtle pitfall is incorrectly checking index bounds, especially forgetting that valid indices range from 0 to len(sorted_values) - 1
.
Common mistake:
# Forgetting to check upper bound properly min_val = sorted_values[floor_idx] if floor_idx >= 0 else -1 # Missing upper bound check!
Correct validation:
min_val = sorted_values[floor_idx] if 0 <= floor_idx < len(sorted_values) else -1
5. Inefficient Query Processing Without Sorted Array
Some might attempt to search the BST directly for each query without first extracting values, leading to O(q × h) complexity where h is tree height, which could be O(q × n) for unbalanced trees.
Inefficient approach:
def find_floor_ceiling(node, query):
# Searching BST for each query individually
# This is O(h) per query, potentially O(n) for unbalanced trees
Better approach (as in the solution):
- Extract all values once: O(n)
- Use binary search for queries: O(q × log n)
- Total: O(n + q × log n) - much better for multiple queries
In a binary min heap, the maximum element can be found in:
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
Binary Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Want a Structured Path to Master System Design Too? Don’t Miss This!