Unique Binary Search Trees II
MEDIUMDescription
Given an integer n, return all the structurally unique BST's (binary search trees), which has exactly n nodes of unique values from 1 to n. Return the answer in any order.
Example 1:
Input: n = 3 Output: [[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
Example 2:
Input: n = 1 Output: [[1]]
Constraints:
1 <= n <= 8
Approaches
Checkout 2 different approaches to solve Unique Binary Search Trees II. Click on different approaches to view the approach and algorithm in detail.
Brute-force Recursion
This approach uses a straightforward recursive method to construct all possible Binary Search Trees. The core idea is based on the definition of a BST: for any chosen root i from the range [1, n], all values smaller than i must go into the left subtree, and all values larger than i must go into the right subtree. We can recursively generate all possible left and right subtrees and then combine them for each choice of root i.
Algorithm
- Define a recursive function, say
generate(start, end), that returns a list of all unique BSTs for values in the range[start, end]. - The main function calls
generate(1, n). - In
generate(start, end):- If
start > end, it signifies an empty subtree. Return a list containing a singlenullelement to represent this. - Initialize an empty list,
all_trees, to store the generated BSTs for the current range. - Iterate with a loop variable
ifromstarttoend. Thisiwill serve as the root of a BST. - Make a recursive call to
generate(start, i-1)to get all possible left subtrees. - Make another recursive call to
generate(i+1, end)to get all possible right subtrees. - Nest two loops to iterate through each
left_treefrom the list of left subtrees and eachright_treefrom the list of right subtrees. - Inside the loops, for each pair of
(left_tree, right_tree), create a newTreeNodewith valuei. Set itsleftchild toleft_treeand itsrightchild toright_tree. - Add this newly constructed tree to the
all_treeslist.
- If
- After the loops complete, return the
all_treeslist.
The algorithm defines a helper function, generateSubtrees(start, end), which is responsible for generating all BSTs using numbers from start to end. For each number i in this range, we consider it as the root. Then, we recursively call the function to generate all possible left subtrees from the range [start, i-1] and all possible right subtrees from [i+1, end]. Finally, we combine each left subtree with each right subtree to form a new tree rooted at i.
The base case for the recursion is when start > end, which represents an empty range. In this case, we return a list containing null to signify an empty subtree. This null is crucial for the loops that combine subtrees, ensuring they execute correctly even when one side is empty.
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new ArrayList<>();
}
return generateSubtrees(1, n);
}
private List<TreeNode> generateSubtrees(int start, int end) {
List<TreeNode> allTrees = new ArrayList<>();
if (start > end) {
allTrees.add(null);
return allTrees;
}
// Pick each number in the range as a root
for (int i = start; i <= end; i++) {
// Generate all possible left subtrees
List<TreeNode> leftSubtrees = generateSubtrees(start, i - 1);
// Generate all possible right subtrees
List<TreeNode> rightSubtrees = generateSubtrees(i + 1, end);
// Combine each left subtree with each right subtree
for (TreeNode left : leftSubtrees) {
for (TreeNode right : rightSubtrees) {
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
allTrees.add(root);
}
}
}
return allTrees;
}
}
Complexity Analysis
Pros and Cons
- The logic is simple and directly follows the mathematical definition of constructing BSTs.
- It's relatively easy to implement.
- Highly inefficient due to massive re-computation of the same subproblems. For example, generating trees for a range of a certain length is done multiple times with different value offsets.
Code Solutions
Checking out 3 solutions in different languages for Unique Binary Search Trees II. Click on different languages to view the code.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public List < TreeNode > generateTrees ( int n ) { return dfs ( 1 , n ); } private List < TreeNode > dfs ( int i , int j ) { List < TreeNode > ans = new ArrayList <>(); if ( i > j ) { ans . add ( null ); return ans ; } for ( int v = i ; v <= j ; ++ v ) { var left = dfs ( i , v - 1 ); var right = dfs ( v + 1 , j ); for ( var l : left ) { for ( var r : right ) { ans . add ( new TreeNode ( v , l , r )); } } } return ans ; } }Video Solution
Watch the video walkthrough for Unique Binary Search Trees II
Similar Questions
5 related questions you might find useful
Patterns:
Data Structures:
Companies:
Subscribe to Scale Engineer newsletter
Learn about System Design, Software Engineering, and interview experiences every week.
No spam, unsubscribe at any time.