96. Unique Binary Search Trees
Problem Description
You are given an integer n
. Your task is to find how many structurally unique Binary Search Trees (BSTs) can be formed using exactly n
nodes, where each node contains a unique value from 1
to n
.
A Binary Search Tree is a tree data structure where:
- Each node has at most two children (left and right)
- For any node, all values in its left subtree are less than the node's value
- For any node, all values in its right subtree are greater than the node's value
Two BSTs are considered structurally different if they have different shapes, even if they might contain the same values in different positions.
For example:
- When
n = 3
, you can form 5 different BST structures using values 1, 2, and 3 - When
n = 1
, there's only 1 possible BST structure
The problem asks you to count the total number of such unique BST structures possible with n
nodes containing values from 1
to n
.
Intuition
To understand how to count unique BST structures, let's think about how BSTs are formed. When we pick a root node, all smaller values must go to the left subtree, and all larger values must go to the right subtree.
Consider we have values from 1
to n
. If we choose value i
as the root:
- Values
1
toi-1
must form the left subtree - Values
i+1
ton
must form the right subtree
Here's the key insight: the number of unique structures for the left subtree depends only on how many nodes it has, not on the actual values. Whether we're arranging [1,2,3]
or [5,6,7]
, the number of possible BST structures is the same - it only depends on having 3 nodes.
This means if we know how many unique BSTs can be formed with j
nodes (left subtree) and how many can be formed with k
nodes (right subtree), we can multiply these counts to get all possible combinations when node i
is the root.
For example, with n=3
:
- Root = 1: left has 0 nodes, right has 2 nodes
- Root = 2: left has 1 node, right has 1 node
- Root = 3: left has 2 nodes, right has 0 nodes
For each choice of root, we multiply the number of possible left subtree structures by the number of possible right subtree structures.
This naturally leads to a dynamic programming approach where f[i]
represents the number of unique BSTs with i
nodes. We can build up from smaller subproblems: f[i] = sum of (f[j] * f[i-j-1])
for all valid splits, where j
is the size of the left subtree and i-j-1
is the size of the right subtree (accounting for the root node).
Learn more about Tree, Binary Search Tree, Math, Dynamic Programming and Binary Tree patterns.
Solution Approach
We implement the solution using dynamic programming with a bottom-up approach. Let's define f[i]
as the number of unique BSTs that can be formed with i
nodes.
Base Case:
f[0] = 1
- There's exactly one way to form an empty tree (no nodes)- Initialize all other values to 0
State Transition:
For each number of nodes i
from 1 to n
, we calculate f[i]
by considering all possible root positions:
f[i] = sum of (f[j] * f[i-j-1]) for j from 0 to i-1
Where:
j
represents the number of nodes in the left subtreei-j-1
represents the number of nodes in the right subtree (we subtract 1 for the root)f[j] * f[i-j-1]
gives us all combinations for this specific split
Implementation Steps:
-
Create an array
f
of sizen+1
initialized withf[0] = 1
and rest as 0 -
For each
i
from 1 ton
:- For each possible left subtree size
j
from 0 toi-1
:- Add
f[j] * f[i-j-1]
tof[i]
- This accounts for all BSTs where the left subtree has
j
nodes
- Add
- For each possible left subtree size
-
Return
f[n]
as the final answer
Example Walkthrough for n=3:
f[0] = 1
(base case)f[1] = f[0] * f[0] = 1 * 1 = 1
f[2] = f[0] * f[1] + f[1] * f[0] = 1*1 + 1*1 = 2
f[3] = f[0] * f[2] + f[1] * f[1] + f[2] * f[0] = 1*2 + 1*1 + 2*1 = 5
The time complexity is O(n²)
as we have two nested loops, and the space complexity is O(n)
for the DP array.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution for n = 4
to see how we count unique BST structures.
Step 1: Initialize DP array
- Create array
f
wheref[i]
= number of unique BSTs withi
nodes f[0] = 1
(empty tree), rest initialized to 0f = [1, 0, 0, 0, 0]
Step 2: Calculate f[1]
- With 1 node, we can only make one tree
- When root is chosen: left has 0 nodes, right has 0 nodes
f[1] = f[0] × f[0] = 1 × 1 = 1
f = [1, 1, 0, 0, 0]
Step 3: Calculate f[2]
- With 2 nodes (values 1,2), consider each as root:
- Root = 1: left has 0 nodes, right has 1 node →
f[0] × f[1] = 1 × 1 = 1
- Root = 2: left has 1 node, right has 0 nodes →
f[1] × f[0] = 1 × 1 = 1
- Root = 1: left has 0 nodes, right has 1 node →
f[2] = 1 + 1 = 2
f = [1, 1, 2, 0, 0]
Step 4: Calculate f[3]
- With 3 nodes (values 1,2,3), consider each as root:
- Root = 1: left has 0 nodes, right has 2 nodes →
f[0] × f[2] = 1 × 2 = 2
- Root = 2: left has 1 node, right has 1 node →
f[1] × f[1] = 1 × 1 = 1
- Root = 3: left has 2 nodes, right has 0 nodes →
f[2] × f[0] = 2 × 1 = 2
- Root = 1: left has 0 nodes, right has 2 nodes →
f[3] = 2 + 1 + 2 = 5
f = [1, 1, 2, 5, 0]
Step 5: Calculate f[4]
- With 4 nodes (values 1,2,3,4), consider each as root:
- Root = 1: left has 0 nodes, right has 3 nodes →
f[0] × f[3] = 1 × 5 = 5
- Root = 2: left has 1 node, right has 2 nodes →
f[1] × f[2] = 1 × 2 = 2
- Root = 3: left has 2 nodes, right has 1 node →
f[2] × f[1] = 2 × 1 = 2
- Root = 4: left has 3 nodes, right has 0 nodes →
f[3] × f[0] = 5 × 1 = 5
- Root = 1: left has 0 nodes, right has 3 nodes →
f[4] = 5 + 2 + 2 + 5 = 14
Final Answer: 14 unique BST structures can be formed with 4 nodes
The key insight is that for each possible root position, we multiply the number of ways to arrange the left subtree by the number of ways to arrange the right subtree. This works because the structural uniqueness depends only on the count of nodes in each subtree, not their actual values.
Solution Implementation
1class Solution:
2 def numTrees(self, n: int) -> int:
3 """
4 Calculate the number of structurally unique BSTs that can be formed with n nodes.
5 Uses dynamic programming to compute Catalan numbers.
6
7 Args:
8 n: Number of nodes (1 to n)
9
10 Returns:
11 Number of unique BST structures possible
12 """
13 # dp[i] represents the number of unique BSTs that can be formed with i nodes
14 # Initialize with dp[0] = 1 (empty tree) and rest as 0
15 dp = [1] + [0] * n
16
17 # Build up the solution for each number of nodes from 1 to n
18 for num_nodes in range(1, n + 1):
19 # For each possible root position (1 to num_nodes)
20 for root_position in range(1, num_nodes + 1):
21 # Calculate number of BSTs with this root
22 # Left subtree has (root_position - 1) nodes
23 # Right subtree has (num_nodes - root_position) nodes
24 left_subtree_count = dp[root_position - 1]
25 right_subtree_count = dp[num_nodes - root_position]
26
27 # Total combinations = left combinations × right combinations
28 dp[num_nodes] += left_subtree_count * right_subtree_count
29
30 return dp[n]
31
1class Solution {
2 /**
3 * Calculate the number of structurally unique BSTs that can be formed with n nodes
4 * containing values from 1 to n.
5 *
6 * This uses dynamic programming to compute Catalan numbers.
7 * The recurrence relation is: dp[n] = sum(dp[i] * dp[n-i-1]) for i from 0 to n-1
8 *
9 * @param n The number of nodes (1 to n)
10 * @return The number of unique BST structures possible
11 */
12 public int numTrees(int n) {
13 // dp[i] represents the number of unique BSTs that can be formed with i nodes
14 int[] dp = new int[n + 1];
15
16 // Base case: empty tree (0 nodes) has exactly one structure
17 dp[0] = 1;
18
19 // Calculate number of unique BSTs for each count from 1 to n
20 for (int nodeCount = 1; nodeCount <= n; nodeCount++) {
21 // For each possible root position (1 to nodeCount)
22 for (int leftSubtreeSize = 0; leftSubtreeSize < nodeCount; leftSubtreeSize++) {
23 // Calculate right subtree size
24 int rightSubtreeSize = nodeCount - leftSubtreeSize - 1;
25
26 // Number of unique BSTs =
27 // (unique BSTs in left subtree) * (unique BSTs in right subtree)
28 dp[nodeCount] += dp[leftSubtreeSize] * dp[rightSubtreeSize];
29 }
30 }
31
32 return dp[n];
33 }
34}
35
1class Solution {
2public:
3 int numTrees(int n) {
4 // dp[i] represents the number of unique BSTs that can be formed with i nodes
5 vector<int> dp(n + 1);
6
7 // Base case: empty tree has 1 structure
8 dp[0] = 1;
9
10 // Calculate number of unique BSTs for each count from 1 to n
11 for (int nodeCount = 1; nodeCount <= n; ++nodeCount) {
12 // Try each number as root (1 to nodeCount)
13 for (int rootPosition = 0; rootPosition < nodeCount; ++rootPosition) {
14 // Number of nodes in left subtree: rootPosition
15 // Number of nodes in right subtree: nodeCount - rootPosition - 1
16 // Total combinations = left subtree combinations * right subtree combinations
17 dp[nodeCount] += dp[rootPosition] * dp[nodeCount - rootPosition - 1];
18 }
19 }
20
21 return dp[n];
22 }
23};
24
1/**
2 * Calculate the number of structurally unique BSTs with n nodes
3 * Uses dynamic programming to compute Catalan numbers
4 * @param n - The number of nodes
5 * @returns The number of unique BSTs
6 */
7function numTrees(n: number): number {
8 // dp[i] represents the number of unique BSTs with i nodes
9 const dp: number[] = Array(n + 1).fill(0);
10
11 // Base case: empty tree has one structure
12 dp[0] = 1;
13
14 // Build up solutions for 1 to n nodes
15 for (let nodeCount = 1; nodeCount <= n; ++nodeCount) {
16 // Try each number as root (1 to nodeCount)
17 for (let rootPosition = 0; rootPosition < nodeCount; ++rootPosition) {
18 // Number of left subtree nodes = rootPosition
19 // Number of right subtree nodes = nodeCount - rootPosition - 1
20 // Total combinations = left subtree combinations * right subtree combinations
21 dp[nodeCount] += dp[rootPosition] * dp[nodeCount - rootPosition - 1];
22 }
23 }
24
25 return dp[n];
26}
27
Time and Space Complexity
The time complexity is O(n²)
, not O(n)
as stated in the reference answer. The code uses two nested loops - the outer loop runs from 0 to n (n+1 iterations), and for each iteration i
, the inner loop runs from 0 to i-1 (i iterations). The total number of operations is approximately 1 + 2 + 3 + ... + n = n(n+1)/2
, which gives us O(n²)
time complexity.
The space complexity is O(n)
. The code creates an array f
of size n+1 to store the intermediate results for dynamic programming, requiring linear space proportional to the input size n
.
Note: The reference answer appears to be incorrect regarding the time complexity. This algorithm computes Catalan numbers using dynamic programming, and the nested loop structure inherently results in quadratic time complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Initialization
A common mistake is forgetting to initialize dp[0] = 1
or incorrectly setting it to 0
. This breaks the entire calculation since an empty tree (0 nodes) is a valid structure that should count as 1 possibility.
Wrong approach:
dp = [0] * (n + 1) # Missing dp[0] = 1
Correct approach:
dp = [1] + [0] * n # Properly initialize dp[0] = 1
2. Off-by-One Errors in Loop Indices
The inner loop logic can be confusing. A frequent error is miscalculating the indices for left and right subtree sizes, especially when translating between "root position" and "subtree size".
Wrong approach:
for root_position in range(num_nodes): # Should be 1 to num_nodes+1
left = dp[root_position]
right = dp[num_nodes - root_position - 1]
Correct approach:
for root_position in range(1, num_nodes + 1):
left = dp[root_position - 1]
right = dp[num_nodes - root_position]
3. Integer Overflow in Other Languages
While Python handles large integers automatically, in languages like Java or C++, the result can overflow for larger values of n
. The Catalan numbers grow exponentially, and even for n=19
, the result exceeds 32-bit integer limits.
Solution for other languages:
- Use
long
orlong long
data types - In Java:
long[] dp = new long[n + 1];
- In C++:
vector<long long> dp(n + 1);
4. Misunderstanding the Problem - Counting Values vs. Structures
Some might think the problem asks for different value arrangements rather than different tree structures. Remember: we're counting structurally unique trees, not different ways to arrange values 1 to n in those structures.
Key insight: For a given structure, there's only one valid way to place values 1 to n to make it a BST. The problem counts structures, not value permutations.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
Binary Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Want a Structured Path to Master System Design Too? Don’t Miss This!