LeetCode 95. 不同的二叉搜索树 II
题目描述

题意分析
使用
1..n中的每个值各一次,生成所有结构不同的二叉搜索树,返回它们的根节点,顺序不限。每棵树都必须包含全部值,并满足左子树值小于根、右子树值大于根。
解法:递归枚举 + 记忆化
核心思路
[!blue]
定义
build(l, r)返回恰好使用值域[l, r]的全部二叉搜索树。选定根值root后,搜索树性质强制所有较小值放在左子树,所有较大值放在右子树,所以左右子问题分别为[l, root - 1]和[root + 1, r]。左右结构相互独立,将左侧每个候选与右侧每个候选逐对组合,每一对新建一个根节点并连接两棵子树。任意合法树都有唯一根值和唯一的左右结构,因此枚举所有根、所有左右配对既不会遗漏,也不会产生重复结构。
当
l > r时,当前一侧没有节点,但“选择空树”仍是一种合法选择,所以必须返回含一个空节点的列表。这样另一侧的每个候选都能与空树配对;如果返回空列表,双重循环一次也不会执行,连叶节点都无法生成。不同父问题会请求同一值域的全部结构,用
(l, r)缓存即可避免重复生成。组合时只新建根,直接引用已生成的左右子树,之后不再修改这些子树。因此多个结果可能共享子树节点,这份实现按只读方式使用已构造结果。
解题步骤
- 从
build(1, n)开始;代码对n = 0直接返回空结果列表。- 用区间两个端点作为缓存键,若已有结果则直接返回。
- 空区间构造仅含
null/nil的候选列表并缓存。- 对非空区间枚举每个根,递归取得左右候选,遍历其笛卡尔积,每对候选新建根并加入结果。
- 缓存当前区间的全部结果后返回,递归区间每次缩小,最终都到达空区间。
代码实现
// build(l, r) 返回区间内所有 BST。
class Solution {
private final Map<String, List<TreeNode>> memo = new HashMap<>();
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new ArrayList<>();
}
return build(1, n);
}
private List<TreeNode> build(int l, int r) {
String key = l + "," + r;
if (memo.containsKey(key)) {
return memo.get(key);
}
List<TreeNode> res = new ArrayList<>();
if (l > r) {
// 空区间提供一种空树候选,不能返回没有方案的空列表
res.add(null);
memo.put(key, res);
return res;
}
for (int root = l; root <= r; root++) {
List<TreeNode> left = build(l, root - 1);
List<TreeNode> right = build(root + 1, r);
for (TreeNode a : left) {
for (TreeNode b : right) {
// 每个左右配对新建根,已完成子树只读复用
TreeNode node = new TreeNode(root);
node.left = a;
node.right = b;
res.add(node);
}
}
}
memo.put(key, res);
return res;
}
}
// build(l, r) 返回区间内所有 BST。
func generateTrees(n int) []*TreeNode {
if n == 0 {
return []*TreeNode{
}
}
memo := make(map[[2]int][]*TreeNode)
return buildTrees(1, n, memo)
}
func buildTrees(l int, r int, memo map[[2]int][]*TreeNode) []*TreeNode {
key := [2]int{
l,
r,
}
if val, ok := memo[key]; ok {
return val
}
res := make([]*TreeNode, 0)
if l > r {
// 空区间提供一种空树候选,不能返回没有方案的空列表
res = append(res, nil)
memo[key] = res
return res
}
for root := l; root <= r; root++ {
left := buildTrees(l, root-1, memo)
right := buildTrees(root+1, r, memo)
for _, a := range left {
for _, b := range right {
// 每个左右配对新建根,已完成子树只读复用
node := &TreeNode{Val: root, Left: a, Right: b}
res = append(res, node)
}
}
}
memo[key] = res
return res
}
复杂度分析
设
C_n为第n个卡特兰数,也就是最终返回的树的数量。
- 时间复杂度:可用 $O(nC_n)$ 作为保守上界。必须枚举全部结果;当前实现缓存各区间,每次左右配对只创建一个根,并未为每个结果深拷贝全部
n个节点。- 空间复杂度:$O(nC_n)$ 保守上界,计入结果节点和区间缓存;递归栈深度为 $O(n)$。共享子树使实际节点分配量小于逐棵独立复制的实现。
关键点总结
[!green]
- 根值确定左右值域,左右候选的所有配对构成当前根的全部结构。
- 含一个空节点的列表表示一种空树选择,空列表则表示没有可组合方案。
- 缓存按完整区间区分,组合时新建根、只读复用子树。
易错点总结
[!yellow]
- 空区间若返回空列表,会让所有需要空孩子的树都无法参与组合。
- 每个左右配对必须创建新根;反复修改同一个根对象会覆盖已加入结果的树。
- 只按一个端点缓存,无法区分使用不同值域的子问题。
- 已缓存的子树可能被多个结果引用,不能在组合后继续修改其节点或左右链接。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 96. 不同的二叉搜索树 | 中等 | 分割根节点并组合左右子树的结构相同,原题只计数,本题需要构造所有不同树。 |
| 894. 所有可能的真二叉树 | 中等 | 同样递归组合所有子树形状,原题要求每个节点要么无孩子要么有两个孩子。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!