Topics › Binary Search Trees › Practice Problems
Floor and Ceil in a BST
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given the root of a binary search tree and an integer key, find:
- the floor of
key: the largest key in the tree that is<= key, and - the ceil of
key: the smallest key in the tree that is>= key.
Return them as {floor, ceil}. If no key qualifies for one of them, report -1 for it. As in the usual judge versions, keys are non-negative, so -1 cannot be confused with a real key.
Constraints: up to 10^5 nodes; keys and key are between 0 and 10^9.
Example 1
8
/ \
4 12
/ \ / \
2 6 10 14
key = 11 answer = {10, 12}
10 is the largest key not above 11, and 12 is the smallest key not below 11.
Example 2
same tree, key = 1 answer = {-1, 2}
Every key is above 1, so there is no floor; the smallest key, 2, is the ceil. If key is in the tree, both answers equal key.
Approach 1: Brute force, scan the sorted keys
Intuition. An inorder traversal of a BST lists the keys in increasing order. On a sorted list, the floor is the last value that is <= key and the ceil is the first value that is >= key. One pass over the list finds both. This is simple and obviously correct, but it touches every node and stores every key, even though only a few of them can matter.
Algorithm.
- Collect the keys with an inorder traversal into
sorted. - Start with
floor = -1,ceil = -1. - For each value
vin order: ifv <= key, setfloor = v(later values overwrite earlier ones, so the last one wins); ifv >= keyand no ceil has been set yet, setceil = v. - Return
{floor, ceil}.
static int[] bruteForce(TreeNode root, int key) {
List<Integer> sorted = new ArrayList<>();
inorder(root, sorted);
int floor = -1, ceil = -1;
for (int v : sorted) {
if (v <= key) floor = v; // the last value <= key wins
if (v >= key && ceil == -1) ceil = v; // the first value >= key wins
}
return new int[]{floor, ceil};
}
static void inorder(TreeNode node, List<Integer> out) {
if (node == null) return;
inorder(node.left, out);
out.add(node.val);
inorder(node.right, out);
}
Complexity. Time O(n) for the traversal and the scan. Space O(n) for the list, plus O(h) recursion.
Once the keys are in a sorted list you could binary search instead of scanning, but building the list already costs O(n), so that would not change the total.
Approach 2: Optimal, walk down from the root
Intuition for the floor. Stand at a node cur and compare it with key.
- If
cur.val == key, the floor iskeyitself, and nothing can beat it. - If
cur.val > key, thencuris too big, and so is everything in its right subtree, because those keys are even larger. The floor, if it exists below here, must be in the left subtree. - If
cur.val < key, thencuris a valid candidate. Can there be a better one, larger thancur.valbut still<= key? Inside this subtree, keys larger thancur.vallive only in the right subtree, so recordcur.valand move right to look for something closer.
The walk never needs to backtrack, because each comparison rules out one whole subtree. Better still, every candidate we record is larger than the previous one: after recording cur.val we only ever move into the right subtree of cur, where all keys exceed cur.val. So the most recent candidate is always the best, and a plain assignment is enough; no max is needed.
Intuition for the ceil. It is the mirror image. A node with cur.val < key is too small together with its whole left subtree, so go right. A node with cur.val > key is a candidate, and anything closer to key must be smaller, so record it and go left.
Algorithm.
findFloor:floor = -1,cur = root. Whilecuris notnull: returnkeyon an exact match; ifcur.val < key, setfloor = cur.valand go right; otherwise go left. Returnfloor.findCeil:ceil = -1,cur = root. Whilecuris notnull: returnkeyon an exact match; ifcur.val > key, setceil = cur.valand go left; otherwise go right. Returnceil.- Return
{findFloor(root, key), findCeil(root, key)}.
static int[] optimal(TreeNode root, int key) {
return new int[]{findFloor(root, key), findCeil(root, key)};
}
static int findFloor(TreeNode root, int key) {
int floor = -1;
TreeNode cur = root;
while (cur != null) {
if (cur.val == key) return key;
if (cur.val < key) {
floor = cur.val; // a candidate; anything closer is to its right
cur = cur.right;
} else {
cur = cur.left; // too big, and so is its whole right subtree
}
}
return floor;
}
static int findCeil(TreeNode root, int key) {
int ceil = -1;
TreeNode cur = root;
while (cur != null) {
if (cur.val == key) return key;
if (cur.val > key) {
ceil = cur.val; // a candidate; anything closer is to its left
cur = cur.left;
} else {
cur = cur.right; // too small, and so is its whole left subtree
}
}
return ceil;
}
Dry run on Example 1 with key = 11:
| walk | cur | comparison | action | best so far |
|---|---|---|---|---|
| floor | 8 | 8 < 11 | record, go right | 8 |
| floor | 12 | 12 > 11 | go left | 8 |
| floor | 10 | 10 < 11 | record, go right | 10 |
| floor | null | stop | 10 | |
| ceil | 8 | 8 < 11 | go right | -1 |
| ceil | 12 | 12 > 11 | record, go left | 12 |
| ceil | 10 | 10 < 11 | go right | 12 |
| ceil | null | stop | 12 |
Notice that the two walks visit exactly the same nodes. Both go left when cur.val > key and right when cur.val < key, which is the search path of key. So the floor and the ceil are always the closest keys on either side of that path, and the two loops could be fused into one. They are kept separate here because each is useful on its own.
Complexity. Time O(h) for each walk, so O(h) overall: O(log n) for a balanced tree, O(n) for a chain. Space O(1).
Edge cases and pitfalls
- Key present. Both answers are
key. Returning on an exact match is required for the floor walk; without it, a node equal tokeywould be treated as “too big” and skipped. - Key smaller than every key. No floor: the walk only ever goes left and never records a candidate, so
-1survives. Symmetrically, a key above every key has no ceil. - Empty tree. Both walks skip the loop and return
-1. - Key
0in the tree.0is a real answer and must not be mistaken for “missing”; only-1means missing. This is also why the-1convention needs non-negative keys. With arbitraryintkeys, returnIntegerand usenullfor “none”. - The answer can be an ancestor. For
key = 9the floor is the root8, recorded at the very first step, while the ceil10is only found at the third level. Do not assume the answer is a leaf or a node near the end of the path. - No arithmetic. Only comparisons are used, so there is no overflow risk even for keys near
10^9.
Related problems
- Floor in a Binary Search Tree asks for the floor alone and adds a recursive version.
- Floor and Ceil in Sorted Array is the same question on a sorted array, solved with binary search.
- Search in a Binary Search Tree walks the same path without remembering candidates.
- Inorder Successor/Predecessor in BST is the strict version: the smallest key
> keyand the largest key< key. - Introduction to BST explains why the inorder traversal used in Approach 1 is sorted.