A clear explanation of Largest BST Subtree using postorder traversal and subtree state propagation.
Problem Restatement
We are given the root of a binary tree.
We need to find the size of the largest subtree that is also a valid Binary Search Tree (BST).
The size of a subtree means the number of nodes inside it.
A BST follows these rules:
| Rule | Meaning |
|---|---|
| Left subtree | All values are smaller than the root |
| Right subtree | All values are larger than the root |
| Recursive property | Both left and right subtrees must also be BSTs |
The subtree does not need to include the original root. Any connected subtree inside the tree may be the answer.
Input and Output
| Item | Meaning |
|---|---|
| Input | Root of a binary tree |
| Output | Size of the largest BST subtree |
| BST condition | Left values < root < right values |
Function shape:
def largestBSTSubtree(root: Optional[TreeNode]) -> int:
...Examples
Example 1:
Input:
10
/ \
5 15
/ \ \
1 8 7
Output: 3The subtree:
5
/ \
1 8is a valid BST of size 3.
The whole tree is not a BST because:
7 < 15but 7 appears in the right subtree of 15.
So the answer is 3.
Example 2:
Input:
2
/ \
1 3
Output: 3The whole tree is a valid BST.
First Thought: Validate Every Subtree
A direct idea is:
- For every node, treat it as the root of a subtree.
- Check whether that subtree is a BST.
- Count its nodes.
- Keep the maximum size.
To validate a BST, we can recursively check value ranges.
But this repeats work many times.
For example, the same subtree may be validated again and again from different ancestors.
The worst-case complexity becomes:
O(n^2)We need one DFS traversal that computes everything bottom-up.
Key Insight
Use postorder traversal.
For each subtree, return enough information so the parent can decide whether the current subtree forms a BST.
For every node, we need four pieces of information:
| Value | Meaning |
|---|---|
is_bst | Whether the subtree is a BST |
size | Size of the subtree if it is a BST |
min_value | Smallest value in the subtree |
max_value | Largest value in the subtree |
Suppose we already know the information for the left subtree and the right subtree.
The current subtree is a BST if:
left subtree is BST
right subtree is BST
left.max_value < current node value
current node value < right.min_valueIf true:
current size =
left size + right size + 1Otherwise, the current subtree is not a BST.
This is a classic bottom-up tree DP.
Algorithm
Run DFS in postorder.
For each node:
- Recursively process the left subtree.
- Recursively process the right subtree.
- Check whether the current subtree satisfies BST conditions.
- If valid:
- Compute subtree size.
- Update the global answer.
- Return subtree information.
- Otherwise:
- Return information marking the subtree as invalid.
For null nodes:
empty subtree is a valid BSTwith:
size = 0
min_value = +infinity
max_value = -infinityThese extreme values simplify comparisons.
Walkthrough
Use:
10
/ \
5 15
/ \ \
1 8 7Start from leaves.
Node 1:
BST
size = 1
min = 1
max = 1Node 8:
BST
size = 1
min = 8
max = 8Now process node 5.
Conditions:
1 < 5 < 8So subtree rooted at 5 is a BST.
Size:
1 + 1 + 1 = 3Now process node 15.
Its right child is 7.
Condition:
15 < 7is false.
So subtree rooted at 15 is not a BST.
Finally process node 10.
Its right subtree is already invalid, so the whole tree is invalid.
The largest BST found was the subtree rooted at 5, with size 3.
Correctness
The DFS processes every subtree after processing its children.
For a null subtree, the algorithm returns that it is a valid BST with size 0. This serves as the base case.
Suppose the DFS has already correctly computed information for the left and right subtrees of a node.
The current subtree is a BST exactly when:
left subtree is BST
right subtree is BST
left maximum < current value
current value < right minimumThese are precisely the recursive BST conditions.
If they hold, then combining the left subtree, current node, and right subtree forms a valid BST. The subtree size is:
left size + right size + 1The subtree minimum becomes the smallest value in the left subtree or the current node. The subtree maximum becomes the largest value in the right subtree or the current node.
If any BST condition fails, then the current subtree cannot be a BST. Returning an invalid marker prevents ancestors from incorrectly using this subtree as part of a larger BST.
Because every subtree is processed exactly once, and every valid BST subtree updates the global maximum size, the final answer equals the size of the largest BST subtree in the tree.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Every node is processed once |
| Space | O(h) | DFS recursion stack, where h is tree height |
In the worst case of a skewed tree:
h = nImplementation
from typing import Optional
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def largestBSTSubtree(self, root: Optional[TreeNode]) -> int:
self.answer = 0
def dfs(node):
if node is None:
return True, 0, float("inf"), float("-inf")
left_is_bst, left_size, left_min, left_max = dfs(node.left)
right_is_bst, right_size, right_min, right_max = dfs(node.right)
if (
left_is_bst
and right_is_bst
and left_max < node.val < right_min
):
size = left_size + right_size + 1
self.answer = max(self.answer, size)
return (
True,
size,
min(left_min, node.val),
max(right_max, node.val),
)
return False, 0, 0, 0
dfs(root)
return self.answerCode Explanation
The global variable stores the best BST size found so far:
self.answer = 0The DFS returns:
(is_bst, size, min_value, max_value)For a null node:
return True, 0, float("inf"), float("-inf")This means:
| Value | Meaning |
|---|---|
True | Empty subtree is a BST |
0 | No nodes |
+inf | Neutral minimum |
-inf | Neutral maximum |
Then recursively process children:
left_is_bst, left_size, left_min, left_max = dfs(node.left)
right_is_bst, right_size, right_min, right_max = dfs(node.right)Check BST validity:
left_max < node.val < right_minIf valid:
size = left_size + right_size + 1Update the global maximum:
self.answer = max(self.answer, size)Return updated subtree information.
If invalid:
return False, 0, 0, 0The exact values do not matter because ancestors will reject invalid subtrees immediately.
Testing
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def run_tests():
s = Solution()
root = TreeNode(
10,
TreeNode(
5,
TreeNode(1),
TreeNode(8),
),
TreeNode(
15,
None,
TreeNode(7),
),
)
assert s.largestBSTSubtree(root) == 3
root = TreeNode(
2,
TreeNode(1),
TreeNode(3),
)
assert s.largestBSTSubtree(root) == 3
root = TreeNode(1)
assert s.largestBSTSubtree(root) == 1
root = TreeNode(
5,
TreeNode(4),
TreeNode(6, TreeNode(3), TreeNode(7)),
)
assert s.largestBSTSubtree(root) == 3
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Mixed valid/invalid tree | Standard example |
| Full BST | Entire tree valid |
| Single node | Minimum non-empty tree |
| Invalid root but valid subtree | Largest BST not at root |