LeetCode 95 - Unique Binary Search Trees II
The problem asks us to generate all structurally unique binary search trees (BSTs) that contain exactly n nodes labeled from 1 to n.
Difficulty: 🟡 Medium
Topics: Dynamic Programming, Backtracking, Tree, Binary Search Tree, Binary Tree
Solution
Problem Understanding
The problem asks us to generate all structurally unique binary search trees (BSTs) that contain exactly n nodes labeled from 1 to n. A BST is a binary tree where for every node, all values in the left subtree are smaller than the node, and all values in the right subtree are larger. The input n is a single integer representing the number of nodes, and the output should be a list of all possible BSTs represented as root nodes of these trees.
The constraints indicate that n can range from 1 to 8. This means the total number of unique BSTs grows rapidly, but is manageable for backtracking or recursive generation techniques. Since the problem guarantees n >= 1, we do not need to handle negative numbers or zero.
Important edge cases include n = 1 (only one tree possible) and n = 2 (multiple small variations). Naive solutions may struggle due to combinatorial explosion if they attempt to generate all permutations without respecting BST properties.
Approaches
The brute-force approach would be to generate all permutations of numbers 1 to n and try to build a BST from each permutation, keeping only valid trees. While this produces correct results, it is extremely inefficient because most permutations do not correspond to unique BST structures, and the number of permutations is n!, which grows very fast.
The optimal approach leverages recursive generation and the BST property. For each number i from 1 to n, we treat i as the root. All numbers less than i form the left subtree, and all numbers greater than i form the right subtree. We recursively generate all possible left and right subtrees and combine them. This is essentially a form of divide and conquer, and ensures every tree generated is a valid BST. Memoization can be added to improve efficiency but is optional given the constraints.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n! * n) | O(n!) | Generate all permutations and filter valid BSTs. Very inefficient. |
| Optimal | O(4^n / √n) | O(4^n / √n) | Recursive generation using BST properties. Catalan number of trees generated. |
Algorithm Walkthrough
- Define a recursive function
generate(start, end)that generates all unique BSTs with node values ranging fromstarttoend. - If
start > end, return[None]because an empty subtree is a valid base case. - Iterate through all values
ifromstarttoend. Treatias the root. - Recursively generate all left subtrees by calling
generate(start, i - 1). - Recursively generate all right subtrees by calling
generate(i + 1, end). - For each combination of left and right subtrees, create a new tree with root
i, left child from left subtree, and right child from right subtree. Append this tree to the result list. - Return the result list containing all trees generated for
starttoend.
Why it works: This approach works because it systematically considers every value as the root and generates all valid left and right subtrees independently, ensuring all combinations are captured. The recursion guarantees that each subtree is itself a valid BST.
Python Solution
from typing import List, Optional
# 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 []
def generate(start: int, end: int) -> List[Optional[TreeNode]]:
if start > end:
return [None]
all_trees = []
for i in range(start, end + 1):
left_trees = generate(start, i - 1)
right_trees = generate(i + 1, end)
for l in left_trees:
for r in right_trees:
root = TreeNode(i)
root.left = l
root.right = r
all_trees.append(root)
return all_trees
return generate(1, n)
Explanation: The generateTrees function first handles n = 0 as a trivial edge case. The inner function generate(start, end) recursively produces all BSTs for the given range. We iterate through all possible roots, recursively compute left and right subtrees, and combine them. Each resulting tree is appended to the result list and returned.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func generateTrees(n int) []*TreeNode {
if n == 0 {
return []*TreeNode{}
}
var generate func(start, end int) []*TreeNode
generate = func(start, end int) []*TreeNode {
if start > end {
return []*TreeNode{nil}
}
var allTrees []*TreeNode
for i := start; i <= end; i++ {
leftTrees := generate(start, i-1)
rightTrees := generate(i+1, end)
for _, l := range leftTrees {
for _, r := range rightTrees {
root := &TreeNode{Val: i, Left: l, Right: r}
allTrees = append(allTrees, root)
}
}
}
return allTrees
}
return generate(1, n)
}
Go-specific notes: The main difference is handling slices and nil pointers for empty subtrees. Slices are used instead of lists, and appending is explicit. Recursive structure is identical to the Python version.
Worked Examples
Example 1: n = 3
We call generate(1, 3):
- Root = 1, left =
generate(1, 0) = [None], right =generate(2, 3)→ produces trees[2,null,3]and[3,2,null]. - Root = 2, left =
generate(1, 1) = [1], right =generate(3, 3) = [3]→ produces tree[2,1,3]. - Root = 3, left =
generate(1, 2)→[1,null,2]and[2,1,null], right =generate(4, 3) = [None].
All combinations yield 5 unique BSTs, matching the example output.
Example 2: n = 1
Only one root possible: 1, with left and right subtrees empty. Output is [1].
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(4^n / √n) | The number of unique BSTs for n nodes is the nth Catalan number, which is approximately 4^n / √n. Each tree construction is O(n) for copying nodes in recursive calls. |
| Space | O(4^n / √n) | Space is dominated by the number of BSTs stored and the recursion stack depth, which is O(n) per recursive call. |
The Catalan number growth is typical for generating all unique BSTs, and it explains why recursive backtracking is feasible for small n but would be intractable for large n.
Test Cases
# test cases
sol = Solution()
# Example cases
assert len(sol.generateTrees(3)) == 5 # multiple unique BSTs
assert len(sol.generateTrees(1)) == 1 # single node
# Boundary and edge cases
assert sol.generateTrees(0) == [] # no nodes
assert len(sol.generateTrees(2)) == 2 # two-node trees
# Stress case
assert len(sol.generateTrees(4)) == 14 # Catalan number for n=4
assert len(sol.generateTrees(5)) == 42 # Catalan number for n=5
| Test | Why |
|---|---|
| n = 3 | Validates multiple tree combinations |
| n = 1 | Single node tree, minimal input |
| n = 0 | Edge case with zero nodes |
| n = 2 | Small multiple trees, checks basic recursion |
| n = 4,5 | Stress test for Catalan number growth |
Edge Cases
The first important edge case is n = 0. Although the problem states 1 <= n <= 8, a robust implementation may handle n = 0. The correct behavior is returning an empty list, representing no trees.
The second edge case is n = 1, which is trivial but tests the base case handling in recursion. The algorithm correctly returns a single node tree.
The third edge case is the maximum value n = 8. Here, the algorithm must generate 14, 42, up to 429 unique BSTs (Catalan numbers). Proper memory management and recursion depth handling are necessary. The recursive approach with explicit combination generation ensures all trees are created without duplication, satisfying constraints.
This implementation safely handles all edge cases by using empty lists to represent empty subtrees and correctly