Topics › Binary Search Trees › Practice Problems
Largest BST in Binary Tree
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given the root of a binary tree (not necessarily a BST). Return the number of nodes in the largest subtree that is a valid binary search tree. A subtree here means a node together with all of its descendants; you may not cut off part of it. The BST rule is strict: every key in a left subtree is smaller than the node, every key in a right subtree is larger, so duplicates break it. An empty tree has answer 0.
Constraints (LeetCode version): up to 10^4 nodes with keys in [-10^4, 10^4]. The code below is correct for any 32-bit keys.
Example 1
10
/ \
5 15
/ \ \
1 8 7
answer = 3 // the subtree {5, 1, 8}
The whole tree is not a BST because 7 is in the right subtree of 15 but smaller than it. The subtree at 15 fails for the same reason. The subtree at 5 is a BST of size 3, and every leaf is a BST of size 1.
Example 2
3
/ \
2 4
/
1
answer = 2 // the subtree {4, 1}
At the root, 2 < 3 < 4 looks fine, but 1 sits in the right subtree of 3. The subtree at 4 is a BST, since 1 < 4, and has size 2.
Approach 1: Brute force, validate every subtree
Intuition. We already know how to check whether one tree is a BST: pass an allowed range down the recursion. So try every node as the root of a candidate subtree. Work from the top: if the subtree at root is a BST, nothing inside it can be bigger, so its size is the answer for this branch. Otherwise the answer lies entirely in the left or the right subtree, and we recurse.
Algorithm.
- An empty tree has answer
0. - If the subtree at
rootpassesisBSTwith range(-∞, +∞), return its node count. - Otherwise return the larger of the answers for the two children.
static int bruteForce(TreeNode root) {
if (root == null) return 0;
if (isBST(root, Long.MIN_VALUE, Long.MAX_VALUE)) return count(root); // nothing below can be bigger
return Math.max(bruteForce(root.left), bruteForce(root.right));
}
static boolean isBST(TreeNode node, long low, long high) {
if (node == null) return true;
if (node.val <= low || node.val >= high) return false;
return isBST(node.left, low, node.val) && isBST(node.right, node.val, high);
}
static int count(TreeNode node) {
return node == null ? 0 : 1 + count(node.left) + count(node.right);
}
Complexity. Time O(n^2) in the worst case. Each isBST call can scan the whole subtree before finding a violation. Picture a left chain whose bottom leaf breaks the order: every node’s check scans down to the bottom, about n^2 / 2 steps in total. Space O(h) for the recursion.
The waste is that a node’s validation re-examines every node below it, which its children’s validations already did.
Approach 2: Optimal, one post-order pass with subtree summaries
Intuition. Turn the question around and go bottom-up. A subtree rooted at node is a BST exactly when:
- its left subtree is a BST,
- its right subtree is a BST,
- the largest key on the left is smaller than
node.val, and the smallest key on the right is larger.
If each child reports (min key, max key, size), the parent can decide all three in O(1). So each subtree returns a small summary:
min,max: the smallest and largest key in the subtree,size: the size of the largest BST found inside the subtree, which is the whole subtree when it is a BST.
Two sentinel tricks keep the code free of special cases:
- An empty subtree reports
min = +∞,max = -∞. Thenleft.max < node.valandnode.val < right.minare automatically true on an empty side, so a leaf is a BST of size 1. - A subtree that is not a BST reports
min = -∞,max = +∞. Then any parent’s check fails: on the left,+∞ < node.valis false; on the right,node.val < -∞is false. “Not a BST” therefore propagates upward on its own, whilesizekeeps carrying the best answer found below.
The sentinels are long values outside the int range, so a real key such as Integer.MAX_VALUE can never collide with them.
Algorithm.
summarize(null)returns(+∞, -∞, 0).- For a node, summarize the left and right children first (post-order).
- If
left.max < node.val < right.min, return(min(left.min, node.val), max(right.max, node.val), left.size + right.size + 1). - Otherwise return
(-∞, +∞, max(left.size, right.size)). - The answer is
summarize(root).size.
static class Info {
final long min, max; // smallest and largest key in the subtree
final int size; // size of the largest BST inside the subtree
Info(long min, long max, int size) { this.min = min; this.max = max; this.size = size; }
}
The pass itself is a post-order recursion that builds a summary from the two child summaries:
static int optimal(TreeNode root) {
return summarize(root).size;
}
static Info summarize(TreeNode node) {
if (node == null) return new Info(Long.MAX_VALUE, Long.MIN_VALUE, 0); // empty: fits under any parent
Info left = summarize(node.left);
Info right = summarize(node.right);
if (left.max < node.val && node.val < right.min) { // node joins two BSTs into one
return new Info(Math.min(left.min, node.val), Math.max(right.max, node.val), left.size + right.size + 1);
}
return new Info(Long.MIN_VALUE, Long.MAX_VALUE, Math.max(left.size, right.size));
}
When the subtree is a BST, left.size and right.size are the full sizes of the two children, because a child that is not a BST would have made the check fail. That is why adding them plus one gives the size of the whole subtree.
The min(left.min, node.val) matters when the left side is empty: left.min is +∞, so the minimum must come from node.val. The same holds for the maximum on the right.
Dry run on Example 1 (∞ written for the sentinels):
| node | left summary | right summary | BST here? | returned (min, max, size) |
|---|---|---|---|---|
| 1 | empty | empty | yes | (1, 1, 1) |
| 8 | empty | empty | yes | (8, 8, 1) |
| 5 | (1, 1, 1) | (8, 8, 1) | 1 < 5 < 8, yes | (1, 8, 3) |
| 7 | empty | empty | yes | (7, 7, 1) |
| 15 | empty | (7, 7, 1) | 15 < 7? no | (-∞, +∞, 1) |
| 10 | (1, 8, 3) | (-∞, +∞, 1) | 10 < -∞? no | (-∞, +∞, 3) |
The answer is 3.
Complexity. Time O(n): every node is summarised once with constant work. Space O(h) for the recursion; each call allocates one small Info object, which is garbage as soon as the parent reads it.
Edge cases and pitfalls
- Local checks are not enough. Example 2 passes “left child < node < right child” at the root and is still not a BST. The
min/maxsummaries carry the whole-subtree information. - Whole subtrees only. You cannot drop the bad leaf
7in Example 1 to make{10, 5, 15, 1, 8}count. A subtree includes all descendants. - Duplicates.
[1, 1]gives1. The strict<comparisons reject equal keys. - Integer extremes.
[Integer.MIN_VALUE, null, Integer.MAX_VALUE]is a BST of size 2. Withintsentinels,Integer.MAX_VALUE < Integer.MAX_VALUEwould wrongly fail. - Empty tree and single node.
0and1. - Answer is the whole tree.
[2, 1, 3]returns3; the brute force returns immediately at the root. - Related variant. “Maximum sum BST in a binary tree” uses the same summary with a
sumfield in place ofsize, and the answer is the best sum seen (at least0).
Related problems
- Check if a tree is a BST or not is the single-tree check used by the brute force.
- Check for balanced binary tree uses the same bottom-up “child reports a summary, failure propagates” pattern.
- Diameter of Binary Tree is another post-order pass where each node combines its children’s answers.
- Maximum path sum also keeps a global best while returning a different value to the parent.
- Correct BST with two nodes swapped deals with another tree that is “almost” a BST.