Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

95. Unique Binary Search Trees II - Leetcode Solution

Problem Description

Given an integer n, you are asked to generate all structurally unique Binary Search Trees (BSTs) that store values 1 through n. Each tree must use each number from 1 to n exactly once, and the structure of the BSTs must be unique (no duplicate tree shapes, even if the values are arranged differently).

The function signature is typically: List<TreeNode> generateTrees(int n)

  • Each BST must use all numbers from 1 to n once and only once.
  • Return all possible unique BST structures (not just one solution).
  • Return an empty list if n == 0.

Thought Process

The problem asks us to generate all unique BSTs for a given number n, not just count them. A brute-force approach would try every possible combination of left and right subtrees for every root, but this quickly becomes infeasible as n grows because the number of trees grows exponentially.

The key insight is recognizing that for each possible root value, the numbers less than the root form the left subtree, and the numbers greater form the right subtree. By recursively generating all left and right subtrees for each root, and combining them, we can build all unique BSTs.

This is a classic recursive problem, and with memoization, we can avoid recomputing subproblems. However, since we need to generate all trees (not just count), memoization is less effective, but recursion is still the best fit.

Solution Approach

We'll use recursion to build all possible trees:

  • For a given range [start, end], we consider each number i in that range as the root.
  • For each root i, recursively generate all possible left subtrees from [start, i-1] and all possible right subtrees from [i+1, end].
  • Combine every left subtree with every right subtree and attach them to the current root node.
  • If start > end, return a list containing None (representing an empty tree).
  • The base case is when the range is invalid, which means we've reached a leaf node.

This approach ensures that all unique structures are generated. The recursive combination of left and right subtrees for each root guarantees that no structure is missed or duplicated.

Example Walkthrough

Let's walk through the process for n = 3:

  1. The possible root values are 1, 2, and 3.
  2. Root = 1:
    • Left subtree: empty (since no numbers less than 1)
    • Right subtree: BSTs built from [2,3]

    For [2,3], root can be 2 (with 3 on the right) or 3 (with 2 on the left).

  3. Root = 2:
    • Left subtree: BSTs from [1]
    • Right subtree: BSTs from [3]

    Both are single-node trees.

  4. Root = 3:
    • Left subtree: BSTs from [1,2]
    • Right subtree: empty

    For [1,2], root can be 1 (with 2 on the right) or 2 (with 1 on the left).

By combining all these, we get 5 unique BSTs for n = 3.

Time and Space Complexity

Brute-force: The number of structurally unique BSTs for n nodes is the Catalan number C_n, which grows exponentially as O(4^n / n^{3/2}). Brute-force would try all possible combinations, which is extremely slow and inefficient.

Optimized Recursive Approach:

  • Time Complexity: Still exponential, as we must generate every possible tree structure, and each structure requires copying nodes. There are C_n unique BSTs, and for each, we construct a tree, so time is at least O(C_n).
  • Space Complexity: Also exponential due to storing all unique trees. The recursion stack uses O(n) space at most.

Note: If only counting the number, dynamic programming can reduce time to O(n^2), but to generate all trees, we must enumerate them.

Summary

This problem elegantly demonstrates recursive tree construction. By considering each value as the root and recursively building all left and right subtrees, we can generate all unique BSTs for values 1 to n. The approach is simple, yet powerful, and highlights the beauty of recursive problem-solving for tree structures.

Code Implementation

# 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):
        if n == 0:
            return []
        
        def build(start, end):
            trees = []
            if start > end:
                return [None]
            for i in range(start, end + 1):
                left_trees = build(start, i - 1)
                right_trees = build(i + 1, end)
                for l in left_trees:
                    for r in right_trees:
                        root = TreeNode(i)
                        root.left = l
                        root.right = r
                        trees.append(root)
            return trees
        
        return build(1, n)
      
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<TreeNode*> generateTrees(int n) {
        if (n == 0) return {};
        return build(1, n);
    }
    
    vector<TreeNode*> build(int start, int end) {
        vector<TreeNode*> trees;
        if (start > end) {
            trees.push_back(nullptr);
            return trees;
        }
        for (int i = start; i <= end; ++i) {
            vector<TreeNode*> left = build(start, i - 1);
            vector<TreeNode*> right = build(i + 1, end);
            for (TreeNode* l : left) {
                for (TreeNode* r : right) {
                    TreeNode* root = new TreeNode(i);
                    root->left = l;
                    root->right = r;
                    trees.push_back(root);
                }
            }
        }
        return trees;
    }
};
      
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<TreeNode> generateTrees(int n) {
        if (n == 0) return new ArrayList<>();
        return build(1, n);
    }
    
    private List<TreeNode> build(int start, int end) {
        List<TreeNode> trees = new ArrayList<>();
        if (start > end) {
            trees.add(null);
            return trees;
        }
        for (int i = start; i <= end; i++) {
            List<TreeNode> left = build(start, i - 1);
            List<TreeNode> right = build(i + 1, end);
            for (TreeNode l : left) {
                for (TreeNode r : right) {
                    TreeNode root = new TreeNode(i);
                    root.left = l;
                    root.right = r;
                    trees.add(root);
                }
            }
        }
        return trees;
    }
}
      
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {number} n
 * @return {TreeNode[]}
 */
var generateTrees = function(n) {
    if (n === 0) return [];
    
    function build(start, end) {
        let trees = [];
        if (start > end) {
            trees.push(null);
            return trees;
        }
        for (let i = start; i <= end; i++) {
            let left = build(start, i - 1);
            let right = build(i + 1, end);
            for (let l of left) {
                for (let r of right) {
                    let root = new TreeNode(i);
                    root.left = l;
                    root.right = r;
                    trees.push(root);
                }
            }
        }
        return trees;
    }
    
    return build(1, n);
};