Topics › Binary Search Trees › Practice Problems
Check if a tree is a BST or not
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given the root of a binary tree. Decide whether it is a valid binary search tree (BST). A tree is a valid BST when, at every node:
- every key in the node’s left subtree is strictly smaller than the node’s key,
- every key in the node’s right subtree is strictly larger than the node’s key,
- both subtrees are valid BSTs themselves.
The empty tree is a valid BST. Constraints (LeetCode version): up to 10^4 nodes, and keys can be any 32-bit signed integer, including Integer.MIN_VALUE and Integer.MAX_VALUE. Duplicate keys make the tree invalid.
Example 1
2
/ \
1 3
answer = true // 1 < 2 < 3
Example 2
5
/ \
1 4
/ \
3 6
answer = false // 4 is in the right subtree of 5 but smaller than 5
Example 3 (the classic trap)
5
/ \
4 6
/ \
3 7
answer = false // 3 < 6 is fine locally, but 3 sits in the right subtree of 5
Example 3 is the one to keep in mind. Checking only that each node is larger than its left child and smaller than its right child passes this tree, yet it is not a BST. The rule is about whole subtrees, not about children.
Approach 1: Brute force
Intuition. Translate the definition directly. “Every key in the left subtree is smaller than the node” is the same as “the largest key in the left subtree is smaller than the node”. Likewise for the right side with the smallest key. So at each node, compute the maximum of the left subtree and the minimum of the right subtree, compare, and then recurse into both children. It is correct by definition, but it recomputes the same subtree extremes again and again.
Algorithm.
- An empty tree is valid.
- If the left child exists and
maxOf(left) >= root.val, returnfalse. - If the right child exists and
minOf(right) <= root.val, returnfalse. - Otherwise the answer is
bruteForce(left) && bruteForce(right).
static boolean bruteForce(TreeNode root) {
if (root == null) return true;
if (root.left != null && maxOf(root.left) >= root.val) return false;
if (root.right != null && minOf(root.right) <= root.val) return false;
return bruteForce(root.left) && bruteForce(root.right);
}
static int maxOf(TreeNode node) {
if (node == null) return Integer.MIN_VALUE;
return Math.max(node.val, Math.max(maxOf(node.left), maxOf(node.right)));
}
static int minOf(TreeNode node) {
if (node == null) return Integer.MAX_VALUE;
return Math.min(node.val, Math.min(minOf(node.left), minOf(node.right)));
}
Complexity. Time O(n^2) in the worst case: a node at depth d is scanned by the maxOf/minOf call of each of its d ancestors, so the total work is the sum of all depths. For a chain that is about n^2 / 2; for a balanced tree it is O(n log n). Space O(h) for the recursion, where h is the tree height.
maxOf and minOf return sentinels for an empty subtree, but a sentinel never decides the result: bruteForce calls them only on a non-empty child, and inside them a sentinel is always combined with a real key through Math.max or Math.min. So a real key equal to Integer.MIN_VALUE or Integer.MAX_VALUE is never confused with “empty”.
Approach 2: Better, in-order must be strictly increasing
Intuition. An in-order traversal visits the left subtree, then the node, then the right subtree. In a BST everything on the left is smaller and everything on the right is larger, so in-order lists the keys in sorted order. The converse also holds, and that is what makes this a complete test: if the in-order sequence is strictly increasing, the tree is a BST. For any node, its left subtree occupies a contiguous block of the sequence immediately before it and its right subtree a block immediately after it. A strictly increasing sequence therefore puts every left key below the node and every right key above it.
Algorithm.
- Run an in-order traversal and append each key to a list.
- Scan the list; if any key is
>=the next one, returnfalse. - Otherwise return
true.
static boolean better(TreeNode root) {
List<Integer> inorder = new ArrayList<>();
collectInorder(root, inorder);
for (int i = 1; i < inorder.size(); i++) {
if (inorder.get(i - 1) >= inorder.get(i)) return false; // equal keys are not allowed either
}
return true;
}
static void collectInorder(TreeNode node, List<Integer> out) {
if (node == null) return;
collectInorder(node.left, out);
out.add(node.val);
collectInorder(node.right, out);
}
Complexity. Time O(n): one traversal plus one scan. Space O(n) for the list, plus O(h) for recursion.
You can drop the list by remembering only the previously visited key during the traversal and failing as soon as the current key is not larger. That keeps the time at O(n) and the extra space at O(h). The next approach reaches the same bounds with a cleaner idea that also generalises to other “constraint flows down the tree” problems.
Approach 3: Optimal, pass down the allowed range
Intuition. Instead of asking each node “what is below you?”, tell each node “what range you must fall in”. The root may hold any key, so its range is (-∞, +∞). When we step into the left child of a node with key v, everything there must be below v, so the upper bound tightens to v. When we step right, the lower bound tightens to v. Every ancestor contributes one constraint of the form “below my key” or “above my key”, and only the tightest constraint of each kind matters, so two numbers carry everything the node needs to know.
This is exactly what catches Example 3: key 3 arrives at the left child of 6 with the range (5, 6), inherited from being in the right subtree of 5, and 3 is outside it.
Algorithm.
- Call
inRange(root, -∞, +∞). - In
inRange(node, low, high): an empty node is valid. Ifnode.val <= lowornode.val >= high, returnfalse. - Recurse left with
(low, node.val)and right with(node.val, high); both must be valid.
static boolean optimal(TreeNode root) {
return inRange(root, Long.MIN_VALUE, Long.MAX_VALUE); // long bounds: keys may be Integer.MIN_VALUE / MAX_VALUE
}
static boolean inRange(TreeNode node, long low, long high) {
if (node == null) return true;
if (node.val <= low || node.val >= high) return false;
return inRange(node.left, low, node.val) && inRange(node.right, node.val, high);
}
The bounds are long. If we used Integer.MIN_VALUE and Integer.MAX_VALUE as the infinities, a tree whose root really is Integer.MIN_VALUE would fail the test node.val <= low. Bounds strictly outside the int range cannot collide with any real key. (Another common fix is to pass Integer objects with null meaning “unbounded”.)
Dry run on Example 3:
| node | allowed range | inside? |
|---|---|---|
| 5 | (-∞, +∞) | yes |
| 4 | (-∞, 5) | yes |
| 6 | (5, +∞) | yes |
| 3 | (5, 6) | no, return false |
Key 7 is never visited because && short-circuits once the left side fails.
Complexity. Time O(n): each node is checked once in constant time. Space O(h) for the recursion: O(log n) for a balanced tree, O(n) for a chain.
Edge cases and pitfalls
- Checking only children. The most common wrong answer. Example 3 and the test
[10, 5, 15, null, null, 6, 20]both pass a parent-child check but are invalid. - Duplicates. This problem uses strict inequalities, so
[2, 2, 2]is not a BST. Some variants put equal keys on one side; then change exactly one of the two comparisons to allow equality on that side. - Integer extremes.
[Integer.MIN_VALUE, null, Integer.MAX_VALUE]is valid. Any approach that usesintsentinels as infinities rejects it. The Java tests include it. - Empty tree and single node. Both are valid. All three methods return
truewithout special handling beyond the null check. - Deep trees. A chain of
10^4nodes means10^4levels of recursion. That is fine for the default Java stack, but an iterative in-order with an explicit stack removes the risk entirely.
Related problems
- Inorder Traversal of Binary Tree is the traversal that Approach 2 relies on.
- Largest BST in Binary Tree runs this check for every subtree at once, bottom-up.
- Correct BST with two nodes swapped uses the “in-order must be increasing” view to locate the broken keys.
- Insert a given node in BST follows the same left-means-smaller rule while walking down.
- Introduction to BST covers the definition used here.