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 thememodictionary. If it does, return the corresponding value from thememodictionary.
Step 6: Initialize Trees List​
- Initialize an empty list called
trees. This list will store the generated trees for the current range. - If
startis greater thanend, appendNoneto thetreeslist, indicating an empty subtree, and return thetreeslist.
Step 7: Loop Through Root Values​
- Loop through each value
root_valin the range[start, end](inclusive):- Recursively call the
generate_treesfunction for the left subtree with the range[start, root_val - 1]and store the result inleft_trees. - Recursively call the
generate_treesfunction 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_treeinleft_treesandright_treeinright_trees:- Create a new
TreeNodeinstance withroot_valas the value,left_treeas the left child, andright_treeas the right child. - Append the new
TreeNodeto thetreeslist.
- Create a new
Step 9: Memoize and Return Trees​
- Store the
treeslist in thememodictionary with the key(start, end). - Return the
treeslist.
Step 10: Generate All Trees​
- Outside the
generate_treesfunction, callgenerate_treesinitially with arguments1andnto generate all unique binary search trees withnnodes.
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.