LeetCode第95题:不同的二叉搜索树 II
题目描述
给你一个整数 n
,请你生成并返回所有由 n
个节点组成且节点值从 1
到 n
互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。
难度
中等
问题链接
示例
示例 1:
ini
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
示例 2:
lua
输入:n = 1
输出:[[1]]
提示
1 <= n <= 8
解题思路
这道题要求我们生成所有可能的二叉搜索树(BST),其中包含 n
个节点,节点值从 1
到 n
。二叉搜索树的特性是对于任意节点,其左子树中的所有节点值都小于该节点的值,右子树中的所有节点值都大于该节点的值。
方法一:递归法
我们可以使用递归的方式来解决这个问题。对于给定的范围 [start, end]
,我们可以枚举每个数字 i
作为根节点,然后递归地构建左子树(范围为 [start, i-1]
)和右子树(范围为 [i+1, end]
)。最后,我们将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树。
方法二:动态规划 + 递归
我们可以使用动态规划来优化递归过程,避免重复计算。我们可以使用一个哈希表或者二维数组来存储已经计算过的结果,这样当我们需要再次计算相同范围的二叉搜索树时,可以直接从缓存中获取结果。
关键点
- 理解二叉搜索树的性质:左子树的所有节点值都小于根节点的值,右子树的所有节点值都大于根节点的值。
- 使用递归来构建所有可能的二叉搜索树。
- 可以使用动态规划来优化递归过程,避免重复计算。
算法步骤分析
递归法算法步骤
步骤 | 操作 | 说明 |
---|---|---|
1 | 定义递归函数 | 函数接收起始值 start 和结束值 end ,返回所有可能的二叉搜索树 |
2 | 处理基本情况 | 如果 start > end ,返回一个包含 null 的列表 |
3 | 枚举根节点 | 从 start 到 end 枚举每个数字 i 作为根节点 |
4 | 递归构建左子树 | 递归地构建左子树,范围为 [start, i-1] |
5 | 递归构建右子树 | 递归地构建右子树,范围为 [i+1, end] |
6 | 组合树 | 将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树 |
7 | 返回结果 | 返回所有可能的二叉搜索树 |
动态规划 + 递归算法步骤
步骤 | 操作 | 说明 |
---|---|---|
1 | 初始化缓存 | 创建一个缓存来存储已经计算过的结果 |
2 | 定义递归函数 | 函数接收起始值 start 和结束值 end ,返回所有可能的二叉搜索树 |
3 | 检查缓存 | 如果缓存中已经有结果,直接返回 |
4 | 处理基本情况 | 如果 start > end ,返回一个包含 null 的列表 |
5 | 枚举根节点 | 从 start 到 end 枚举每个数字 i 作为根节点 |
6 | 递归构建左子树 | 递归地构建左子树,范围为 [start, i-1] |
7 | 递归构建右子树 | 递归地构建右子树,范围为 [i+1, end] |
8 | 组合树 | 将所有可能的左子树和右子树组合起来,形成不同的二叉搜索树 |
9 | 更新缓存 | 将结果存入缓存 |
10 | 返回结果 | 返回所有可能的二叉搜索树 |
算法可视化
以示例 n = 3
为例,我们使用递归法来生成所有可能的二叉搜索树:
-
对于范围
[1, 3]
,我们枚举每个数字作为根节点:- 根节点为 1:左子树为空,右子树为范围
[2, 3]
的所有二叉搜索树 - 根节点为 2:左子树为范围
[1, 1]
的所有二叉搜索树,右子树为范围[3, 3]
的所有二叉搜索树 - 根节点为 3:左子树为范围
[1, 2]
的所有二叉搜索树,右子树为空
- 根节点为 1:左子树为空,右子树为范围
-
对于范围
[2, 3]
,我们枚举每个数字作为根节点:- 根节点为 2:左子树为空,右子树为范围
[3, 3]
的所有二叉搜索树 - 根节点为 3:左子树为范围
[2, 2]
的所有二叉搜索树,右子树为空
- 根节点为 2:左子树为空,右子树为范围
-
对于范围
[1, 2]
,我们枚举每个数字作为根节点:- 根节点为 1:左子树为空,右子树为范围
[2, 2]
的所有二叉搜索树 - 根节点为 2:左子树为范围
[1, 1]
的所有二叉搜索树,右子树为空
- 根节点为 1:左子树为空,右子树为范围
-
对于范围
[1, 1]
、[2, 2]
和[3, 3]
,只有一种可能的二叉搜索树,即只包含一个节点的树。 -
最终,我们得到以下 5 种不同的二叉搜索树:
markdown1 1 2 3 3 \ \ / \ / / 2 3 1 3 1 2 \ / \ / 3 2 2 1
代码实现
C# 实现
csharp
/**
* Definition for a binary tree node.
* public class TreeNode {
* public int val;
* public TreeNode left;
* public TreeNode right;
* public TreeNode(int val=0, TreeNode left=null, TreeNode right=null) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
public class Solution {
// 方法一:递归法
public IList<TreeNode> GenerateTrees(int n) {
if (n == 0) {
return new List<TreeNode>();
}
return GenerateTreesRecursive(1, n);
}
private IList<TreeNode> GenerateTreesRecursive(int start, int end) {
IList<TreeNode> result = new List<TreeNode>();
// 如果起始值大于结束值,返回一个包含 null 的列表
if (start > end) {
result.Add(null);
return result;
}
// 枚举每个数字作为根节点
for (int i = start; i <= end; i++) {
// 递归构建左子树
IList<TreeNode> leftTrees = GenerateTreesRecursive(start, i - 1);
// 递归构建右子树
IList<TreeNode> rightTrees = GenerateTreesRecursive(i + 1, end);
// 组合左子树和右子树
foreach (TreeNode left in leftTrees) {
foreach (TreeNode right in rightTrees) {
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
result.Add(root);
}
}
}
return result;
}
// 方法二:动态规划 + 递归
public IList<TreeNode> GenerateTreesDP(int n) {
if (n == 0) {
return new List<TreeNode>();
}
// 创建缓存
Dictionary<(int, int), IList<TreeNode>> memo = new Dictionary<(int, int), IList<TreeNode>>();
return GenerateTreesDP(1, n, memo);
}
private IList<TreeNode> GenerateTreesDP(int start, int end, Dictionary<(int, int), IList<TreeNode>> memo) {
IList<TreeNode> result = new List<TreeNode>();
// 如果起始值大于结束值,返回一个包含 null 的列表
if (start > end) {
result.Add(null);
return result;
}
// 检查缓存
if (memo.ContainsKey((start, end))) {
return memo[(start, end)];
}
// 枚举每个数字作为根节点
for (int i = start; i <= end; i++) {
// 递归构建左子树
IList<TreeNode> leftTrees = GenerateTreesDP(start, i - 1, memo);
// 递归构建右子树
IList<TreeNode> rightTrees = GenerateTreesDP(i + 1, end, memo);
// 组合左子树和右子树
foreach (TreeNode left in leftTrees) {
foreach (TreeNode right in rightTrees) {
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
result.Add(root);
}
}
}
// 更新缓存
memo[(start, end)] = result;
return result;
}
}
Python 实现
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[TreeNode]:
if n == 0:
return []
return self.generate_trees_recursive(1, n)
def generate_trees_recursive(self, start, end):
result = []
# 如果起始值大于结束值,返回一个包含 None 的列表
if start > end:
result.append(None)
return result
# 枚举每个数字作为根节点
for i in range(start, end + 1):
# 递归构建左子树
left_trees = self.generate_trees_recursive(start, i - 1)
# 递归构建右子树
right_trees = self.generate_trees_recursive(i + 1, end)
# 组合左子树和右子树
for left in left_trees:
for right in right_trees:
root = TreeNode(i)
root.left = left
root.right = right
result.append(root)
return result
# 方法二:动态规划 + 递归
def generateTreesDP(self, n: int) -> List[TreeNode]:
if n == 0:
return []
# 创建缓存
memo = {}
return self.generate_trees_dp(1, n, memo)
def generate_trees_dp(self, start, end, memo):
result = []
# 如果起始值大于结束值,返回一个包含 None 的列表
if start > end:
result.append(None)
return result
# 检查缓存
if (start, end) in memo:
return memo[(start, end)]
# 枚举每个数字作为根节点
for i in range(start, end + 1):
# 递归构建左子树
left_trees = self.generate_trees_dp(start, i - 1, memo)
# 递归构建右子树
right_trees = self.generate_trees_dp(i + 1, end, memo)
# 组合左子树和右子树
for left in left_trees:
for right in right_trees:
root = TreeNode(i)
root.left = left
root.right = right
result.append(root)
# 更新缓存
memo[(start, end)] = result
return result
C++ 实现
cpp
/**
* 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*>();
}
return generateTreesRecursive(1, n);
}
vector<TreeNode*> generateTreesRecursive(int start, int end) {
vector<TreeNode*> result;
// 如果起始值大于结束值,返回一个包含 nullptr 的列表
if (start > end) {
result.push_back(nullptr);
return result;
}
// 枚举每个数字作为根节点
for (int i = start; i <= end; i++) {
// 递归构建左子树
vector<TreeNode*> leftTrees = generateTreesRecursive(start, i - 1);
// 递归构建右子树
vector<TreeNode*> rightTrees = generateTreesRecursive(i + 1, end);
// 组合左子树和右子树
for (TreeNode* left : leftTrees) {
for (TreeNode* right : rightTrees) {
TreeNode* root = new TreeNode(i);
root->left = left;
root->right = right;
result.push_back(root);
}
}
}
return result;
}
// 方法二:动态规划 + 递归
vector<TreeNode*> generateTreesDP(int n) {
if (n == 0) {
return vector<TreeNode*>();
}
// 创建缓存
unordered_map<string, vector<TreeNode*>> memo;
return generateTreesDP(1, n, memo);
}
vector<TreeNode*> generateTreesDP(int start, int end, unordered_map<string, vector<TreeNode*>>& memo) {
vector<TreeNode*> result;
// 如果起始值大于结束值,返回一个包含 nullptr 的列表
if (start > end) {
result.push_back(nullptr);
return result;
}
// 检查缓存
string key = to_string(start) + "_" + to_string(end);
if (memo.find(key) != memo.end()) {
return memo[key];
}
// 枚举每个数字作为根节点
for (int i = start; i <= end; i++) {
// 递归构建左子树
vector<TreeNode*> leftTrees = generateTreesDP(start, i - 1, memo);
// 递归构建右子树
vector<TreeNode*> rightTrees = generateTreesDP(i + 1, end, memo);
// 组合左子树和右子树
for (TreeNode* left : leftTrees) {
for (TreeNode* right : rightTrees) {
TreeNode* root = new TreeNode(i);
root->left = left;
root->right = right;
result.push_back(root);
}
}
}
// 更新缓存
memo[key] = result;
return result;
}
};
执行结果
C# 执行结果
- 执行用时:92 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 90.00% 的 C# 提交
Python 执行结果
- 执行用时:52 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:16.1 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:12 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:13.5 MB,击败了 94.74% 的 C++ 提交
代码亮点
- 递归实现清晰:递归函数的实现清晰明了,易于理解和维护。
- 动态规划优化:使用动态规划来优化递归过程,避免重复计算,提高效率。
- 边界条件处理:代码中详细处理了各种边界情况,如空树和单节点树。
- 组合树的方法:通过嵌套循环来组合所有可能的左子树和右子树,形成不同的二叉搜索树。
- 缓存键的设计:在 C++ 实现中,使用字符串作为缓存的键,简化了哈希表的使用。
常见错误分析
- 忽略空树 :在递归过程中,需要考虑空树的情况,即当
start > end
时,应该返回一个包含null
的列表。 - 组合树错误:在组合左子树和右子树时,需要创建新的根节点,而不是修改原有的树结构。
- 缓存键冲突:在使用缓存时,需要确保缓存键的唯一性,避免冲突。
- 内存泄漏:在 C++ 实现中,需要注意内存管理,避免内存泄漏。
- 递归终止条件:在递归实现中,需要正确设置终止条件,避免无限递归。
解法比较
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
递归法 | O(4^n / n^(3/2)) | O(4^n / n^(3/2)) | 实现简单,直观 | 存在重复计算,效率较低 |
动态规划 + 递归 | O(4^n / n^(3/2)) | O(4^n / n^(3/2)) | 避免重复计算,效率高 | 实现稍复杂,需要额外的空间存储缓存 |
注意:时间复杂度和空间复杂度的表达式是基于卡特兰数的渐近表示,卡特兰数 C(n) 的渐近表示为 4^n / (n^(3/2) * sqrt(π))。