Unique Binary Search Trees II

Med
#0095Time: Exponential, O(3^N)Space: O(N * G_N)6 companies

Prompt

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

2 approaches with complexity analysis and trade-offs.

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

  1. Define a recursive function, say generate(start, end), that returns a list of all unique BSTs for values in the range [start, end].
  2. The main function calls generate(1, n).
  3. In generate(start, end):
    • If start > end, it signifies an empty subtree. Return a list containing a single null element to represent this.
    • Initialize an empty list, all_trees, to store the generated BSTs for the current range.
    • Iterate with a loop variable i from start to end. This i will 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_tree from the list of left subtrees and each right_tree from the list of right subtrees.
    • Inside the loops, for each pair of (left_tree, right_tree), create a new TreeNode with value i. Set its left child to left_tree and its right child to right_tree.
    • Add this newly constructed tree to the all_trees list.
  4. After the loops complete, return the all_trees list.

Walkthrough

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

Time

Exponential, O(3^N)

Space

O(N * G_N)

Trade-offs

Pros

  • The logic is simple and directly follows the mathematical definition of constructing BSTs.

  • It's relatively easy to implement.

Cons

  • 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.

Solutions

/** * 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 walkthrough

Newsletter

One sharp idea, every week

System design and interview prep — short enough to finish.

No spam. Unsubscribe anytime.

Practice

Same difficulty — related problems to reinforce the pattern.