Unique binary search tree II
Problem Description​
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​
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
Institution​
There 3 important things.
One is we try to create subtrees with some range between start(minimum) and end(maximum) value.
Second is calculation of the range. left range should be between start and current root - 1 as end value because all values of left side must be smaller than current root. right range should be between current root + 1 and end becuase all values on the right side should be greater than current root value.
Thrid is we call the same funtion recursively, so it's good idea to keep results of current start and end, so that we can use the results later. It's time saving.
Approach:​
Generating All Unique Binary Search Trees
This solution defines a class Solution
containing a method generateTrees
which generates all possible unique binary search trees (BSTs) with n
nodes. The algorithm utilizes memoization to store previously computed results and avoid redundant calculations, thus improving efficiency.
Implementation Steps​
Step 1: Define the Solution
Class​
Define a class Solution
containing a method generateTrees
which takes an integer n
as input and returns a list of optional TreeNode
objects.
Step 2: Base Case Check​
Check if n
is 0. If it is, return an empty list since there are no possible trees with 0 nodes.
Step 3: Initialize Memoization Dictionary​
Initialize an empty dictionary called memo
. This dictionary will be used to store previously computed results for specific ranges of values to avoid redundant calculations.
Step 4: Define the Inner Function​
Define an inner function called generate_trees
that takes two parameters: start
and end
, which represent the range of values for which binary search trees need to be generated.
Step 5: Memoization Check​
Inside the generate_trees
function:
- Check if the tuple
(start, end)
exists as a key in thememo
dictionary. If it does, return the corresponding value from thememo
dictionary.
Step 6: Initialize Trees List​
- Initialize an empty list called
trees
. This list will store the generated trees for the current range. - If
start
is greater thanend
, appendNone
to thetrees
list, indicating an empty subtree, and return thetrees
list.
Step 7: Loop Through Root Values​
- Loop through each value
root_val
in the range[start, end]
(inclusive):- Recursively call the
generate_trees
function for the left subtree with the range[start, root_val - 1]
and store the result inleft_trees
. - Recursively call the
generate_trees
function for the right subtree with the range[root_val + 1, end]
and store the result inright_trees
.
- Recursively call the
Step 8: Generate Trees​
- Nested loop through each combination of
left_tree
inleft_trees
andright_tree
inright_trees
:- Create a new
TreeNode
instance withroot_val
as the value,left_tree
as the left child, andright_tree
as the right child. - Append the new
TreeNode
to thetrees
list.
- Create a new
Step 9: Memoize and Return Trees​
- Store the
trees
list in thememo
dictionary with the key(start, end)
. - Return the
trees
list.
Step 10: Generate All Trees​
- Outside the
generate_trees
function, callgenerate_trees
initially with arguments1
andn
to generate all unique binary search trees withn
nodes.
Step 11: Return Result​
- Return the list of generated trees.
This algorithm generates all possible unique binary search trees with n
nodes by considering different ranges of root values and recursively generating left and right subtrees for each possible root value. The memo
dictionary is used to store previously computed results, reducing redundant calculations and improving the efficiency of the algorithm.
Code​
C++​
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<TreeNode*> generateTrees(int n) {
if (n == 0) {
return vector<TreeNode*>();
}
unordered_map<string, vector<TreeNode*>> memo;
return generateTreesHelper(1, n, memo);
}
private:
vector<TreeNode*> generateTreesHelper(int start, int end, unordered_map<string, vector<TreeNode*>>& memo) {
string key = to_string(start) + "-" + to_string(end);
if (memo.find(key) != memo.end()) {
return memo[key];
}
vector<TreeNode*> trees;
if (start > end) {
trees.push_back(nullptr);
return trees;
}
for (int rootVal = start; rootVal <= end; rootVal++) {
vector<TreeNode*> leftTrees = generateTreesHelper(start, rootVal - 1, memo);
vector<TreeNode*> rightTrees = generateTreesHelper(rootVal + 1, end, memo);
for (TreeNode* leftTree : leftTrees) {
for (TreeNode* rightTree : rightTrees) {
TreeNode* root = new TreeNode(rootVal);
root->left = leftTree;
root->right = rightTree;
trees.push_back(root);
}
}
}
memo[key] = trees;
return trees;
}
};
Python​
# 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 generateTrees(self, n: int) -> List[Optional[TreeNode]]:
if n == 0:
return []
memo = {}
def generate_trees(start, end):
if (start, end) in memo:
return memo[(start, end)]
trees = []
if start > end:
trees.append(None)
return trees
for root_val in range(start, end + 1):
left_trees = generate_trees(start, root_val - 1)
right_trees = generate_trees(root_val + 1, end)
for left_tree in left_trees:
for right_tree in right_trees:
root = TreeNode(root_val, left_tree, right_tree)
trees.append(root)
memo[(start, end)] = trees
return trees
return generate_trees(1, n)
Java​
/**
* 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<>();
}
Map<String, List<TreeNode>> memo = new HashMap<>();
return generateTreesHelper(1, n, memo);
}
private List<TreeNode> generateTreesHelper(int start, int end, Map<String, List<TreeNode>> memo) {
String key = start + "-" + end;
if (memo.containsKey(key)) {
return memo.get(key);
}
List<TreeNode> trees = new ArrayList<>();
if (start > end) {
trees.add(null);
return trees;
}
for (int rootVal = start; rootVal <= end; rootVal++) {
List<TreeNode> leftTrees = generateTreesHelper(start, rootVal - 1, memo);
List<TreeNode> rightTrees = generateTreesHelper(rootVal + 1, end, memo);
for (TreeNode leftTree : leftTrees) {
for (TreeNode rightTree : rightTrees) {
TreeNode root = new TreeNode(rootVal);
root.left = leftTree;
root.right = rightTree;
trees.add(root);
}
}
}
memo.put(key, trees);
return trees;
}
}
Conclusion​
Complexity​
-
Time complexity:
O(C(n))
C is Catalan number. -
Space complexity:
O(C(n))
C is Catalan number.