776. Split BST 🔒
Problem Description
You're given a binary search tree (BST) and an integer target
. Your task is to split the tree into two separate subtrees based on the target value:
- First subtree: Contains all nodes with values less than or equal to
target
- Second subtree: Contains all nodes with values greater than
target
The key requirement is to preserve the original parent-child relationships as much as possible. This means if two nodes (parent and child) from the original tree end up in the same subtree after splitting, they should maintain their parent-child relationship.
The tree might not contain a node with the exact target
value - you're simply using it as a threshold for splitting.
Example walkthrough:
If you have a BST and target = 4
:
- All nodes with values ≤ 4 go to the first tree
- All nodes with values > 4 go to the second tree
- The BST property is maintained in both resulting trees
The solution uses a recursive approach that traverses the tree and makes decisions at each node:
-
If
root.val <= target
: This node belongs to the left (smaller) tree. We recursively split the right subtree since it might contain both smaller and larger values. The left part of the split becomes the new right child of the current node. -
If
root.val > target
: This node belongs to the right (larger) tree. We recursively split the left subtree since it might contain both smaller and larger values. The right part of the split becomes the new left child of the current node.
The function returns an array [left_tree_root, right_tree_root]
where:
left_tree_root
is the root of the tree with all values ≤target
right_tree_root
is the root of the tree with all values >target
Intuition
The key insight comes from understanding the BST property: for any node, all values in its left subtree are smaller, and all values in its right subtree are larger.
When we encounter a node, we need to decide which resulting tree it belongs to based on comparing its value with target
. But here's the crucial observation: the decision we make at each node affects how we handle its subtrees.
Let's think through the scenarios:
Case 1: Current node value ≤ target
- This node definitely belongs to the "smaller" tree
- Its entire left subtree also belongs to the "smaller" tree (BST property guarantees all left values are even smaller)
- But its right subtree is interesting - it might contain both values ≤
target
and values >target
- So we need to recursively split the right subtree
Case 2: Current node value > target
- This node definitely belongs to the "larger" tree
- Its entire right subtree also belongs to the "larger" tree (BST property guarantees all right values are even larger)
- But its left subtree might contain both values ≤
target
and values >target
- So we need to recursively split the left subtree
The elegance of the solution emerges from this pattern: when we split a subtree recursively, we get back two parts [l, r]
. We then "stitch" the appropriate part back to maintain valid BST structure:
- If current node goes to the smaller tree (
root.val <= target
), we attach the smaller part of the split right subtree as our new right child - If current node goes to the larger tree (
root.val > target
), we attach the larger part of the split left subtree as our new left child
This recursive approach naturally preserves the parent-child relationships because we're only modifying connections where nodes need to be separated into different trees. Nodes that stay together maintain their original structure.
Learn more about Tree, Binary Search Tree, Recursion and Binary Tree patterns.
Solution Approach
The solution implements a recursive depth-first search (DFS) approach to split the BST. Let's walk through the implementation step by step:
Base Case:
if root is None: return [None, None]
When we reach a null node, we return [None, None]
indicating empty trees for both splits.
Recursive Logic:
The function examines each node and makes a decision based on comparing root.val
with target
:
When root.val <= target
:
l, r = dfs(root.right) root.right = l return [root, r]
- The current node belongs to the "smaller or equal" tree
- We recursively split the right subtree by calling
dfs(root.right)
- This returns
[l, r]
wherel
contains nodes ≤target
andr
contains nodes >target
- We attach
l
as the new right child of the current node (replacing the original right subtree) - We return
[root, r]
whereroot
is now the root of the smaller tree, andr
is the root of the larger tree from the split
When root.val > target
:
l, r = dfs(root.left) root.left = r return [l, root]
- The current node belongs to the "greater than" tree
- We recursively split the left subtree by calling
dfs(root.left)
- This returns
[l, r]
wherel
contains nodes ≤target
andr
contains nodes >target
- We attach
r
as the new left child of the current node (replacing the original left subtree) - We return
[l, root]
wherel
is the root of the smaller tree from the split, androot
is now the root of the larger tree
Pattern Recognition:
This solution follows the "divide and conquer" pattern where:
- We make a local decision at each node
- Recursively solve subproblems (splitting subtrees)
- Combine the results by adjusting pointers
The algorithm modifies the tree in-place, reusing existing nodes rather than creating new ones. This is efficient in terms of space complexity.
Time Complexity: O(n)
where n is the number of nodes, as we visit each node exactly once.
Space Complexity: O(h)
where h is the height of the tree, due to the recursive call stack. In the worst case (skewed tree), this could be O(n)
.
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 splitting a BST with target = 4:
Original BST: Target = 4 5 / \ 3 7 / \ \ 2 4 8
Step-by-step execution:
-
Start at root (5): Since 5 > 4, this node belongs to the "greater" tree. We need to split its left subtree.
- Recursively call
dfs(node 3)
- Recursively call
-
At node 3: Since 3 ≤ 4, this node belongs to the "smaller or equal" tree. We need to split its right subtree.
- Recursively call
dfs(node 4)
- Recursively call
-
At node 4: Since 4 ≤ 4, this node belongs to the "smaller or equal" tree. Both children are null, so we recursively get
[None, None]
from the right subtree.- Set
node4.right = None
(no change) - Return
[node4, None]
- Set
-
Back at node 3: We get
[node4, None]
from splitting the right subtree.- Set
node3.right = node4
(maintaining the connection) - Return
[node3, None]
(entire subtree rooted at 3 goes to smaller tree)
- Set
-
Back at root (5): We get
[node3, None]
from splitting the left subtree.- Set
node5.left = None
(disconnect from smaller tree) - Return
[node3, node5]
- Set
Result:
Smaller/Equal Tree (≤4): Greater Tree (>4): 3 5 / \ \ 2 4 7 \ 8
The algorithm correctly:
- Kept nodes 2, 3, 4 together in the smaller tree (all ≤ 4)
- Kept nodes 5, 7, 8 together in the greater tree (all > 4)
- Preserved parent-child relationships where nodes stayed in the same tree
- Maintained BST property in both resulting trees
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 List, Optional
9
10class Solution:
11 def splitBST(
12 self, root: Optional[TreeNode], target: int
13 ) -> List[Optional[TreeNode]]:
14 """
15 Split a Binary Search Tree into two subtrees where one contains all nodes <= target
16 and the other contains all nodes > target.
17
18 Args:
19 root: Root node of the BST
20 target: The value to split the BST by
21
22 Returns:
23 A list of two TreeNodes: [smaller_or_equal_tree, greater_tree]
24 """
25
26 def dfs(node: Optional[TreeNode]) -> List[Optional[TreeNode]]:
27 """
28 Recursively split the BST.
29
30 Returns:
31 [left_subtree, right_subtree] where left_subtree contains values <= target
32 and right_subtree contains values > target
33 """
34 # Base case: empty node returns two empty trees
35 if node is None:
36 return [None, None]
37
38 if node.val <= target:
39 # Current node belongs to the left (smaller/equal) tree
40 # Split the right subtree since it may contain values on both sides
41 left_split, right_split = dfs(node.right)
42
43 # Attach the left part of the split to current node's right
44 node.right = left_split
45
46 # Return current node as root of left tree, and right_split as right tree
47 return [node, right_split]
48 else:
49 # Current node belongs to the right (greater) tree
50 # Split the left subtree since it may contain values on both sides
51 left_split, right_split = dfs(node.left)
52
53 # Attach the right part of the split to current node's left
54 node.left = right_split
55
56 # Return left_split as left tree, and current node as root of right tree
57 return [left_split, node]
58
59 return dfs(root)
60
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 // Target value for splitting the BST
18 private int targetValue;
19
20 /**
21 * Splits a Binary Search Tree into two subtrees.
22 * First subtree contains all nodes with values <= target.
23 * Second subtree contains all nodes with values > target.
24 *
25 * @param root The root of the BST to split
26 * @param target The target value for splitting
27 * @return Array of two TreeNodes: [smaller/equal subtree, larger subtree]
28 */
29 public TreeNode[] splitBST(TreeNode root, int target) {
30 this.targetValue = target;
31 return splitBSTHelper(root);
32 }
33
34 /**
35 * Recursive helper function to split the BST.
36 *
37 * @param currentNode The current node being processed
38 * @return Array containing two subtrees:
39 * [0] - subtree with values <= target
40 * [1] - subtree with values > target
41 */
42 private TreeNode[] splitBSTHelper(TreeNode currentNode) {
43 // Base case: if node is null, return two null subtrees
44 if (currentNode == null) {
45 return new TreeNode[] {null, null};
46 }
47
48 // If current node value is less than or equal to target
49 if (currentNode.val <= targetValue) {
50 // Split the right subtree recursively
51 TreeNode[] rightSplitResult = splitBSTHelper(currentNode.right);
52
53 // Attach the smaller part of right subtree to current node's right
54 currentNode.right = rightSplitResult[0];
55
56 // Current node becomes root of the smaller/equal tree
57 rightSplitResult[0] = currentNode;
58
59 return rightSplitResult;
60 } else {
61 // Current node value is greater than target
62 // Split the left subtree recursively
63 TreeNode[] leftSplitResult = splitBSTHelper(currentNode.left);
64
65 // Attach the larger part of left subtree to current node's left
66 currentNode.left = leftSplitResult[1];
67
68 // Current node becomes root of the larger tree
69 leftSplitResult[1] = currentNode;
70
71 return leftSplitResult;
72 }
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 /**
15 * Splits a BST into two subtrees where one contains all nodes <= target
16 * and the other contains all nodes > target
17 * @param root - root of the BST to split
18 * @param target - the value to split the BST by
19 * @return vector with two elements: [smaller_tree, larger_tree]
20 * smaller_tree: BST with all nodes <= target
21 * larger_tree: BST with all nodes > target
22 */
23 vector<TreeNode*> splitBST(TreeNode* root, int target) {
24 return splitBSTHelper(root, target);
25 }
26
27private:
28 /**
29 * Recursive helper function to split the BST
30 * @param node - current node being processed
31 * @param target - the split value
32 * @return vector with [smaller_tree, larger_tree]
33 */
34 vector<TreeNode*> splitBSTHelper(TreeNode* node, int target) {
35 // Base case: if node is null, return two null trees
36 if (!node) {
37 return {nullptr, nullptr};
38 }
39
40 // If current node value <= target, it belongs to the smaller tree
41 if (node->val <= target) {
42 // Recursively split the right subtree
43 vector<TreeNode*> splitResult = splitBSTHelper(node->right, target);
44
45 // Connect the smaller part of right subtree to current node's right
46 node->right = splitResult[0];
47
48 // Current node becomes the root of the smaller tree
49 splitResult[0] = node;
50
51 // Return [smaller tree with current node as root, larger tree]
52 return splitResult;
53 }
54 // If current node value > target, it belongs to the larger tree
55 else {
56 // Recursively split the left subtree
57 vector<TreeNode*> splitResult = splitBSTHelper(node->left, target);
58
59 // Connect the larger part of left subtree to current node's left
60 node->left = splitResult[1];
61
62 // Current node becomes the root of the larger tree
63 splitResult[1] = node;
64
65 // Return [smaller tree, larger tree with current node as root]
66 return splitResult;
67 }
68 }
69};
70
1/**
2 * Definition for a binary tree node.
3 */
4interface TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8}
9
10/**
11 * Splits a Binary Search Tree into two subtrees based on a target value.
12 *
13 * @param root - The root of the BST to split
14 * @param target - The target value to split by
15 * @returns A tuple where:
16 * - First element: BST with all nodes <= target
17 * - Second element: BST with all nodes > target
18 */
19function splitBST(root: TreeNode | null, target: number): [TreeNode | null, TreeNode | null] {
20 // Initialize result array with two null trees
21 let result: [TreeNode | null, TreeNode | null] = [null, null];
22
23 // Base case: if root is null, return empty trees
24 if (!root) {
25 return result;
26 }
27
28 // If current node value is less than or equal to target,
29 // it belongs to the smaller tree
30 if (root.val <= target) {
31 // Recursively split the right subtree
32 result = splitBST(root.right, target);
33
34 // Attach the smaller part of right subtree to current node
35 root.right = result[0];
36
37 // Current node becomes the root of smaller tree
38 result[0] = root;
39 } else {
40 // If current node value is greater than target,
41 // it belongs to the larger tree
42
43 // Recursively split the left subtree
44 result = splitBST(root.left, target);
45
46 // Attach the larger part of left subtree to current node
47 root.left = result[1];
48
49 // Current node becomes the root of larger tree
50 result[1] = root;
51 }
52
53 return result;
54}
55
Time and Space Complexity
Time Complexity: O(h)
where h
is the height of the BST. In the worst case of a skewed tree, this becomes O(n)
where n
is the number of nodes.
The algorithm performs a single traversal from the root down to a leaf, following only one path through the tree. At each node visited, it performs constant time operations (comparisons and pointer reassignments). Since BST properties guide the traversal to follow either the left or right subtree (never both), the maximum number of nodes visited equals the height of the tree.
Space Complexity: O(h)
where h
is the height of the BST. In the worst case of a skewed tree, this becomes O(n)
.
The space complexity comes from the recursive call stack. The maximum depth of recursion equals the height of the tree, as the algorithm follows a single path from root to leaf. Each recursive call adds a frame to the call stack with constant space for local variables l
and r
. No additional data structures are used, and the tree modification is done in-place by adjusting pointers.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Assigning Split Results
One of the most common mistakes is confusing which part of the split result to attach to the current node. When splitting a subtree, developers often mix up the assignment logic.
Incorrect Implementation:
if node.val <= target: left_split, right_split = dfs(node.right) node.right = right_split # WRONG! Should be left_split return [node, left_split] # WRONG! Should be [node, right_split]
Why this happens: It's counterintuitive that when the current node belongs to the "smaller" tree and we split its right subtree, we keep the "smaller" part of that split (left_split) attached to it.
Correct Logic:
- When
node.val <= target
: The node stays in the smaller tree, so we need to keep only the nodes from its right subtree that are also ≤ target (which isleft_split
) - When
node.val > target
: The node stays in the larger tree, so we need to keep only the nodes from its left subtree that are also > target (which isright_split
)
2. Modifying the Original Tree Without Creating a Copy
The current solution modifies the original tree in-place. If you need to preserve the original tree structure, you'll run into issues.
Problem Scenario:
# Original tree is modified after splitting original_root = create_bst() result = splitBST(original_root, 5) # original_root is now corrupted and no longer represents the original tree
Solution - Create New Nodes:
def splitBST(self, root: Optional[TreeNode], target: int) -> List[Optional[TreeNode]]:
def dfs(node: Optional[TreeNode]) -> List[Optional[TreeNode]]:
if node is None:
return [None, None]
# Create a copy of the current node
node_copy = TreeNode(node.val)
if node.val <= target:
left_split, right_split = dfs(node.right)
# Copy the left subtree reference
node_copy.left = node.left
node_copy.right = left_split
return [node_copy, right_split]
else:
left_split, right_split = dfs(node.left)
node_copy.left = right_split
# Copy the right subtree reference
node_copy.right = node.right
return [left_split, node_copy]
return dfs(root)
3. Handling Edge Cases Incorrectly
Developers sometimes forget to handle special cases properly:
Missing Edge Case Handling:
def splitBST(self, root: Optional[TreeNode], target: int) -> List[Optional[TreeNode]]:
# Forgetting to handle None root
if root.val <= target: # This will crash if root is None
# ... rest of code
Proper Edge Case Handling:
- Always check for
None
nodes first - Consider trees with single nodes
- Handle cases where all nodes are either ≤ target or all > target
4. Confusion About BST Properties After Split
Some developers worry about maintaining BST properties and try to rebalance or resort nodes unnecessarily.
Key Understanding: The split operation naturally maintains BST properties because:
- We're only breaking parent-child links between nodes that belong to different groups
- Within each group, the relative ordering is preserved
- No node values are changed, only tree structure is modified
The algorithm guarantees both resulting trees remain valid BSTs without any additional balancing or sorting operations.
A heap is a ...?
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!