333. Largest BST Subtree 🔒
Problem Description
You are given the root of a binary tree. Your task is to find the largest subtree that is also a valid Binary Search Tree (BST). The "largest" subtree means the one with the most number of nodes.
A Binary Search Tree must satisfy these properties:
- All values in the left subtree of any node must be less than that node's value
- All values in the right subtree of any node must be greater than that node's value
- These properties must hold for every node in the BST
A subtree must include all of its descendants - you cannot pick and choose nodes; if you include a node, you must include all nodes below it.
For example, if you have a binary tree where only a portion forms a valid BST, you need to identify that valid BST portion and count its nodes. The answer should be the count of nodes in the largest such valid BST subtree found anywhere in the given tree.
The solution uses a post-order traversal approach where for each node, it:
- Checks if the left and right subtrees are valid BSTs
- Verifies if the current node can form a larger BST with its subtrees by checking if
max(left subtree) < node.val < min(right subtree)
- Returns the minimum value, maximum value, and size of the BST rooted at the current node
- Uses infinity values (
inf
and-inf
) as markers to indicate invalid BSTs
The function returns three values for each node:
- Minimum value in the subtree
- Maximum value in the subtree
- Number of nodes if it's a valid BST (0 if invalid)
The variable ans
keeps track of the maximum BST size found during the traversal.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: Although it's specifically a binary tree, a tree is a special type of graph (connected acyclic graph). The problem involves nodes with parent-child relationships.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure where we need to find the largest BST subtree.
DFS
- We arrive at DFS: For tree problems, DFS is the natural choice. We need to traverse the entire tree to examine each potential subtree and determine if it's a valid BST.
Why DFS is the right approach:
- We need to validate BST properties at each node, which requires information from both left and right subtrees
- Post-order traversal (a form of DFS) allows us to process children before parents, gathering necessary information about subtree validity and bounds
- Each node needs to know the min/max values and validity status of its subtrees to determine if it can form a larger BST
Conclusion: The flowchart correctly leads us to use DFS for this tree-based problem. The solution implements a post-order DFS traversal where each recursive call returns three pieces of information (min value, max value, and node count) to help parent nodes determine if they can form valid BSTs with their subtrees.
Intuition
When we need to find the largest BST subtree, we face a key challenge: how do we efficiently check if each subtree is a valid BST while also tracking its size?
The natural instinct might be to check each node as a potential root and validate if its entire subtree forms a BST. However, this would involve repeated traversals and redundant work.
Instead, let's think bottom-up. If we process the tree from the leaves upward, we can build our answer incrementally. For each node, we need to answer three questions:
- Are my left and right subtrees valid BSTs?
- Can I form a larger BST by including myself with my subtrees?
- What information should I pass to my parent?
The key insight is that for a node to form a valid BST with its subtrees, we need to ensure that:
- All values in the left subtree are less than the current node's value
- All values in the right subtree are greater than the current node's value
Rather than checking every single node in the subtrees, we only need to know the maximum value from the left subtree and the minimum value from the right subtree. If max(left) < node.val < min(right)
, then combining these subtrees with the current node creates a valid BST.
This leads us to track three pieces of information for each subtree:
- The minimum value in the subtree (to help parent nodes validate BST property)
- The maximum value in the subtree (to help parent nodes validate BST property)
- The size of the valid BST (0 if the subtree is not a valid BST)
By using impossible values like inf
and -inf
as sentinel values when a subtree isn't a valid BST, we create a clever mechanism where invalid subtrees automatically fail the BST check at their parent level (since no real value can be greater than inf
or less than -inf
).
This bottom-up approach ensures we visit each node exactly once while maintaining all necessary information to solve the problem efficiently.
Solution Approach
The solution implements a post-order DFS traversal using a recursive helper function dfs
that processes each node in the tree.
Core Implementation Details:
The dfs
function returns a tuple of three values for each node:
min_val
: The minimum value in the subtreemax_val
: The maximum value in the subtreesize
: The number of nodes if it's a valid BST, 0 otherwise
Base Case:
When we encounter a None
node (empty subtree), we return (inf, -inf, 0)
. This clever choice ensures that:
- An empty left subtree has
max = -inf
, which is always less than any parent node value - An empty right subtree has
min = inf
, which is always greater than any parent node value - The size is 0 since there are no nodes
Recursive Processing: For each non-null node, we:
- Recursively process the left subtree:
lmi, lmx, ln = dfs(root.left)
- Recursively process the right subtree:
rmi, rmx, rn = dfs(root.right)
BST Validation: We check if the current node can form a valid BST with its subtrees using the condition:
if lmx < root.val < rmi:
This verifies that:
- The maximum value in the left subtree (
lmx
) is less than the current node's value - The minimum value in the right subtree (
rmi
) is greater than the current node's value
When a Valid BST is Found: If the BST property is satisfied:
- Update the global answer:
ans = max(ans, ln + rn + 1)
where the size is left subtree nodes + right subtree nodes + current node - Return the updated bounds and size for the parent's validation:
- New minimum:
min(lmi, root.val)
- either the left subtree's minimum or current node value - New maximum:
max(rmx, root.val)
- either the right subtree's maximum or current node value - Total size:
ln + rn + 1
- New minimum:
When BST Property Fails:
If the current subtree is not a valid BST, return (-inf, inf, 0)
. These sentinel values ensure that:
- Any parent node trying to form a BST with this invalid subtree will fail the validation check
- No real node value can satisfy
inf < node.val < -inf
Global Answer Tracking:
The variable ans
is initialized to 0 and updated whenever we find a valid BST, keeping track of the maximum size encountered throughout the traversal.
The algorithm visits each node exactly once, making it O(n) in time complexity, where n is the number of nodes in the tree.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Consider this binary tree:
10 / \ 5 15 / \ \ 1 8 20
We'll trace through the post-order traversal and see how the algorithm identifies the largest BST subtree.
Step 1: Process leaf node 1
dfs(1)
is called- Left child is None: returns
(inf, -inf, 0)
- Right child is None: returns
(inf, -inf, 0)
- Check BST property:
-inf < 1 < inf
✓ (valid) - Return:
(1, 1, 1)
- min=1, max=1, size=1 - Update global
ans = 1
Step 2: Process leaf node 8
- Similar to node 1
- Return:
(8, 8, 1)
- min=8, max=8, size=1 - Global
ans
remains 1
Step 3: Process node 5
- Left subtree (node 1):
(1, 1, 1)
- Right subtree (node 8):
(8, 8, 1)
- Check BST property:
1 < 5 < 8
✓ (valid) - This forms a valid BST with 3 nodes!
- Return:
(1, 8, 3)
- min=1, max=8, size=3 - Update global
ans = 3
Step 4: Process leaf node 20
- Similar to other leaf nodes
- Return:
(20, 20, 1)
- min=20, max=20, size=1
Step 5: Process node 15
- Left child is None: returns
(inf, -inf, 0)
- Right subtree (node 20):
(20, 20, 1)
- Check BST property:
-inf < 15 < 20
✓ (valid) - Return:
(15, 20, 2)
- min=15, max=20, size=2 - Global
ans
remains 3
Step 6: Process root node 10
- Left subtree (node 5):
(1, 8, 3)
- Right subtree (node 15):
(15, 20, 2)
- Check BST property:
8 < 10 < 15
✓ (valid) - The entire tree is a valid BST with 6 nodes!
- Return:
(1, 20, 6)
- min=1, max=20, size=6 - Update global
ans = 6
Final Answer: 6
Now let's see what happens with an invalid BST. Consider this modified tree:
10 / \ 5 15 / \ \ 1 12 20
When we reach node 5:
- Left subtree (node 1):
(1, 1, 1)
- Right subtree (node 12):
(12, 12, 1)
- Check BST property:
1 < 5 < 12
✗ (invalid - 12 > 5 but it's in the left subtree of 10) - Actually wait, locally at node 5:
1 < 5 < 12
✓ (this is valid!) - Return:
(1, 12, 3)
When we reach node 10:
- Left subtree (node 5):
(1, 12, 3)
- Right subtree (node 15):
(15, 20, 2)
- Check BST property:
12 < 10 < 15
✗ (invalid - 12 > 10!) - Return:
(-inf, inf, 0)
- marking this subtree as invalid - The largest BST remains the subtree rooted at node 5 with size 3
This example demonstrates how the algorithm correctly identifies that while the subtree rooted at node 5 is a valid BST, the entire tree is not, because node 12 violates the BST property relative to the root node 10.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9from math import inf
10
11class Solution:
12 def largestBSTSubtree(self, root: Optional[TreeNode]) -> int:
13 """
14 Find the size of the largest Binary Search Tree (BST) subtree in a binary tree.
15
16 Args:
17 root: Root node of the binary tree
18
19 Returns:
20 Integer representing the number of nodes in the largest BST subtree
21 """
22
23 def dfs(node: Optional[TreeNode]) -> tuple[float, float, int]:
24 """
25 Recursively check if subtree is a valid BST and return its properties.
26
27 Args:
28 node: Current node being processed
29
30 Returns:
31 A tuple containing:
32 - min_val: Minimum value in the subtree (inf if not a valid BST)
33 - max_val: Maximum value in the subtree (-inf if not a valid BST)
34 - size: Size of the BST subtree (0 if not a valid BST)
35 """
36 # Base case: empty node is a valid BST with size 0
37 if node is None:
38 return inf, -inf, 0
39
40 # Recursively process left and right subtrees
41 left_min, left_max, left_size = dfs(node.left)
42 right_min, right_max, right_size = dfs(node.right)
43
44 # Check if current subtree forms a valid BST
45 # Valid BST condition: left_max < node.val < right_min
46 if left_max < node.val < right_min:
47 # Current subtree is a valid BST
48 current_size = left_size + right_size + 1
49 self.max_bst_size = max(self.max_bst_size, current_size)
50
51 # Return the range and size of current BST
52 # Min value is either left_min or node.val (if left subtree is empty)
53 # Max value is either right_max or node.val (if right subtree is empty)
54 return min(left_min, node.val), max(right_max, node.val), current_size
55
56 # Current subtree is not a valid BST
57 # Return invalid range markers and size 0
58 return -inf, inf, 0
59
60 # Initialize the maximum BST size found
61 self.max_bst_size = 0
62
63 # Start DFS traversal from root
64 dfs(root)
65
66 return self.max_bst_size
67
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 // Global variable to track the maximum BST subtree size
18 private int maxBSTSize;
19
20 /**
21 * Finds the size of the largest BST subtree in a binary tree.
22 *
23 * @param root The root of the binary tree
24 * @return The size of the largest BST subtree
25 */
26 public int largestBSTSubtree(TreeNode root) {
27 maxBSTSize = 0;
28 findLargestBST(root);
29 return maxBSTSize;
30 }
31
32 /**
33 * Helper method that performs post-order traversal to find BST properties.
34 * Returns an array with [minValue, maxValue, size] of the BST rooted at current node.
35 *
36 * @param node The current tree node being processed
37 * @return An array containing:
38 * - [0]: minimum value in the subtree (MAX_VALUE if empty)
39 * - [1]: maximum value in the subtree (MIN_VALUE if empty)
40 * - [2]: size of BST if valid, 0 otherwise
41 */
42 private int[] findLargestBST(TreeNode node) {
43 // Base case: empty node is a valid BST with size 0
44 if (node == null) {
45 // Return [MAX, MIN, 0] to maintain BST property check
46 return new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE, 0};
47 }
48
49 // Recursively get BST information from left and right subtrees
50 int[] leftBSTInfo = findLargestBST(node.left);
51 int[] rightBSTInfo = findLargestBST(node.right);
52
53 // Check if current node forms a valid BST with its subtrees
54 // Valid BST condition: left max < node.val < right min
55 if (leftBSTInfo[1] < node.val && node.val < rightBSTInfo[0]) {
56 // Current subtree is a valid BST
57 int currentBSTSize = leftBSTInfo[2] + rightBSTInfo[2] + 1;
58
59 // Update the global maximum BST size
60 maxBSTSize = Math.max(maxBSTSize, currentBSTSize);
61
62 // Return BST information for parent nodes
63 return new int[] {
64 Math.min(node.val, leftBSTInfo[0]), // Minimum value in current BST
65 Math.max(node.val, rightBSTInfo[1]), // Maximum value in current BST
66 currentBSTSize // Size of current BST
67 };
68 }
69
70 // Current subtree is not a valid BST
71 // Return invalid BST marker [MIN, MAX, 0]
72 return new int[] {Integer.MIN_VALUE, Integer.MAX_VALUE, 0};
73 }
74}
75
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 int largestBSTSubtree(TreeNode* root) {
15 int maxSize = 0;
16 postorderTraversal(root, maxSize);
17 return maxSize;
18 }
19
20private:
21 /**
22 * Performs post-order traversal to find the largest BST subtree
23 *
24 * @param node Current node being processed
25 * @param maxSize Reference to track the maximum BST size found
26 * @return Vector containing [minValue, maxValue, size] of the BST rooted at current node
27 * - minValue: Minimum value in the subtree (INT_MAX if empty)
28 * - maxValue: Maximum value in the subtree (INT_MIN if empty)
29 * - size: Number of nodes in BST (0 if not a valid BST)
30 * Returns [INT_MIN, INT_MAX, 0] when subtree is not a valid BST
31 */
32 vector<int> postorderTraversal(TreeNode* node, int& maxSize) {
33 // Base case: empty node represents a valid BST with size 0
34 // Return [INT_MAX, INT_MIN] to ensure parent comparisons work correctly
35 if (!node) {
36 return {INT_MAX, INT_MIN, 0};
37 }
38
39 // Recursively process left and right subtrees
40 vector<int> leftInfo = postorderTraversal(node->left, maxSize);
41 vector<int> rightInfo = postorderTraversal(node->right, maxSize);
42
43 // Check if current node forms a valid BST with its subtrees
44 // Conditions: left subtree's max < current value < right subtree's min
45 if (leftInfo[1] < node->val && node->val < rightInfo[0]) {
46 // Current subtree is a valid BST
47 int currentSize = leftInfo[2] + rightInfo[2] + 1;
48 maxSize = max(maxSize, currentSize);
49
50 // Return info for parent nodes to validate BST property
51 // Min value: either current node value or left subtree's min (handles empty left case)
52 // Max value: either current node value or right subtree's max (handles empty right case)
53 int minVal = min(node->val, leftInfo[0]);
54 int maxVal = max(node->val, rightInfo[1]);
55 return {minVal, maxVal, currentSize};
56 }
57
58 // Current subtree is not a valid BST
59 // Return invalid range to prevent parent from being valid BST
60 return {INT_MIN, INT_MAX, 0};
61 }
62};
63
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8 constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.left = (left === undefined ? null : left);
11 this.right = (right === undefined ? null : right);
12 }
13}
14
15/**
16 * Finds the size of the largest Binary Search Tree (BST) subtree in a binary tree
17 *
18 * @param root - The root node of the binary tree
19 * @returns The number of nodes in the largest BST subtree
20 */
21function largestBSTSubtree(root: TreeNode | null): number {
22 let maxSize = 0;
23
24 /**
25 * Performs post-order traversal to find the largest BST subtree
26 *
27 * @param node - Current node being processed
28 * @returns Array containing [minValue, maxValue, size] of the BST rooted at current node
29 * - minValue: Minimum value in the subtree (Infinity if empty)
30 * - maxValue: Maximum value in the subtree (-Infinity if empty)
31 * - size: Number of nodes in BST (0 if not a valid BST)
32 * Returns [-Infinity, Infinity, 0] when subtree is not a valid BST
33 */
34 function postorderTraversal(node: TreeNode | null): [number, number, number] {
35 // Base case: empty node represents a valid BST with size 0
36 // Return [Infinity, -Infinity] to ensure parent comparisons work correctly
37 if (!node) {
38 return [Infinity, -Infinity, 0];
39 }
40
41 // Recursively process left and right subtrees
42 const leftInfo = postorderTraversal(node.left);
43 const rightInfo = postorderTraversal(node.right);
44
45 // Check if current node forms a valid BST with its subtrees
46 // Conditions: left subtree's max < current value < right subtree's min
47 if (leftInfo[1] < node.val && node.val < rightInfo[0]) {
48 // Current subtree is a valid BST
49 const currentSize = leftInfo[2] + rightInfo[2] + 1;
50 maxSize = Math.max(maxSize, currentSize);
51
52 // Return info for parent nodes to validate BST property
53 // Min value: either current node value or left subtree's min (handles empty left case)
54 // Max value: either current node value or right subtree's max (handles empty right case)
55 const minVal = Math.min(node.val, leftInfo[0]);
56 const maxVal = Math.max(node.val, rightInfo[1]);
57 return [minVal, maxVal, currentSize];
58 }
59
60 // Current subtree is not a valid BST
61 // Return invalid range to prevent parent from being valid BST
62 return [-Infinity, Infinity, 0];
63 }
64
65 postorderTraversal(root);
66 return maxSize;
67}
68
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree.
The algorithm performs a depth-first search (DFS) traversal of the tree, visiting each node exactly once. At each node, the function performs constant-time operations:
- Comparing values (
lmx < root.val < rmi
) - Computing min/max values (
min(lmi, root.val)
,max(rmx, root.val)
) - Updating the answer variable
- Basic arithmetic operations (
ln + rn + 1
)
Since each node is visited once and only constant work is done per node, the overall time complexity is O(n)
.
Space Complexity: O(h)
where h
is the height of the binary tree.
The space complexity is determined by the recursive call stack. In the worst case:
- For a skewed tree (essentially a linked list), the height
h = n
, resulting inO(n)
space complexity - For a balanced tree, the height
h = log(n)
, resulting inO(log n)
space complexity
The algorithm doesn't use any additional data structures that scale with input size. Each recursive call only stores a constant amount of data (the return values and local variables), so the space complexity is proportional to the maximum depth of the recursion, which equals the height of the tree.
Common Pitfalls
1. Incorrectly Handling the Definition of "Subtree"
Pitfall: A common mistake is misunderstanding that a valid subtree must include ALL descendants of a node. Some developers might try to "skip" certain nodes to form a BST, which violates the problem constraints.
Example of incorrect thinking:
10 / \ 5 15 / \ / 1 8 7 # Node 7 violates BST property under 15 # WRONG: Trying to count nodes [1, 5, 8, 10, 15] by skipping node 7 # CORRECT: The largest valid BST is [1, 5, 8] with size 3
Solution: The post-order traversal approach naturally handles this by propagating invalid BST markers (-inf, inf, 0
) up the tree whenever a violation is found, ensuring parent nodes cannot form a BST with invalid subtrees.
2. Confusing BST Validation Logic with Sentinel Values
Pitfall: The use of inf
and -inf
as sentinel values can be confusing. Developers might accidentally swap them or use them incorrectly.
Common mistakes:
# WRONG: Returning (inf, -inf, 0) for invalid BST # This would make parent validation always succeed incorrectly # WRONG: Returning (-inf, inf, 0) for empty node # This would make all parent validations fail
Solution: Remember the logic:
- Empty node: Returns
(inf, -inf, 0)
- creates a "valid range" that any parent can work with - Invalid BST: Returns
(-inf, inf, 0)
- creates an "impossible range" that fails any parent validation
3. Incorrect Range Calculation for Valid BST
Pitfall: When a valid BST is found, calculating the new min/max values incorrectly.
Wrong implementation:
if left_max < node.val < right_min: # WRONG: Using left_min and right_max directly return left_min, right_max, current_size # Fails when subtree is empty!
Why it fails: When a subtree is empty, left_min = inf
or right_max = -inf
, which are sentinel values, not actual tree values.
Correct implementation:
if left_max < node.val < right_min:
# CORRECT: Use min/max to handle empty subtree cases
return min(left_min, node.val), max(right_max, node.val), current_size
4. Using Mutable Default Arguments or Global Variables Incorrectly
Pitfall: Using a list as default argument or improperly scoped variables:
Wrong approaches:
def largestBSTSubtree(self, root, ans=[0]): # WRONG: Mutable default
# This persists across multiple function calls!
def largestBSTSubtree(self, root):
ans = 0 # Local variable
def dfs(node):
# WRONG: Can't modify 'ans' without nonlocal declaration
ans = max(ans, current_size) # UnboundLocalError!
Correct solutions:
# Option 1: Use instance variable
self.max_bst_size = 0
# Option 2: Use nonlocal declaration
def largestBSTSubtree(self, root):
ans = 0
def dfs(node):
nonlocal ans
ans = max(ans, current_size)
# Option 3: Pass and return the answer
def dfs(node, current_max):
# ... calculate new_max ...
return min_val, max_val, size, new_max
5. Not Handling Edge Cases Properly
Pitfall: Forgetting to handle special cases:
Edge cases to consider:
- Empty tree (root is None)
- Single node tree
- Tree where entire tree is a valid BST
- Tree where no valid BST exists (each node forms BST of size 1)
Solution: The algorithm handles these naturally:
# Empty tree if root is None: return 0 # Add this check before calling dfs # Single node - automatically handled as BST of size 1 # Full tree BST - checked at root level # No valid BST - each node counted as size 1 BST
Which of the following uses divide and conquer strategy?
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!