白菜Java自习室 涵盖核心知识
力扣原题
96. 不同的二叉搜索树
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
方法一:动态规划
思路与算法
给定一个有序序列 ,为了构建出一棵二叉搜索树,我们可以遍历每个数字 ,将该数字作为树根,将 序列作为左子树,将 序列作为右子树。接着我们可以按照同样的方式递归构建左子树和右子树。
在上述构建的过程中,由于根的值不同,因此我们能保证每棵二叉搜索树是唯一的。
由此可见,原问题可以分解成规模较小的两个子问题,且子问题的解可以复用。因此,我们可以想到使用动态规划来求解本题。
题目要求是计算不同二叉搜索树的个数。为此,我们可以定义两个函数:
- : 长度为 的序列能构成的不同二叉搜索树的个数。
- : 以 为根、序列长度为 的不同二叉搜索树个数 。
不同的二叉搜索树的总数 ,是对遍历所有 的 之和。
公式1:
于边界情况,当序列长度为 (只有根)或为 (空树)时,只有一种情况,即:
,
给定序列 ,我们选择数字 作为根,则根为 的所有二叉搜索树的集合是左子树集合和右子树集合的笛卡尔积,对于笛卡尔积中的每个元素,加上根节点之后形成完整的二叉搜索树。
举例说明
创建以 为根、长度为 的不同二叉搜索树,整个序列是 ,我们需要从左子序列 构建左子树,从右子序列 构建右子树,然后将它们组合(即笛卡尔积)。
对于这个例子,不同二叉搜索树的个数为 。我们将 构建不同左子树的数量表示为 , 从 构建不同右子树的数量表示为 ,注意到 和序列的内容无关,只和序列的长度有关。于是,。因此,我们可以得到以下公式:
公式2:
将 公式1,公式2 结合,可以得到 的递归表达式:
至此,我们从小到大计算 函数即可,因为 的值依赖于 。
代码实现
class Solution {
public int numTrees(int n) {
int[] G = new int[n + 1];
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
}
复杂度分析
-
时间复杂度 : ,其中 表示二叉搜索树的节点个数。 函数一共有 个值需要求解,每次求解需要 的时间复杂度,因此总时间复杂度为 。
-
空间复杂度 : 。我们需要 的空间存储 数组。
方法二:卡特兰数
卡特兰数简介
卡特兰数是组合数学中的一种著名数列,通常用如下通项式表示:
卡特兰数也是有递推式的(和 方法一 的 的递归表达式一样):
但在实际应用中,最常用的却是第一个通项式的变形:
卡特兰数推导
基本模型:有一个长度为 的 , 序列,其中 , 各 个,要求对于任意的整数,数列的前 个数中, 的个数不少于 。
- 我们把 , 操作扔到一个坐标系中。 看成向右上方走一步, 看成向右下角走一步,那么最后构造完后一定走到了,如下图:
那么总的路径数量就是在 步中选择 步为 ,方案数为 。
限制条件:对于任意前缀, 的个数不少于 。那么转化到坐标系中,也就是说走的路径不应该穿过 轴,即直线 ,也就是不接触 。
- 于是我们把与 的接触点的右边整条路径以 为对称轴翻折,于是终点变为了 ,如下图:
那么此时,从 到 的路径必定至少穿过一次 ,不满足条件,那么此时的路径数量即为总路径数中不符合题意的路径数,那么我们用总路径数减去不符合的路径数,就是最终的答案。
而此时的路径数量也很简单,由于反转后终点向下移了两位,也就意味着 步是 , 步是 ,总方案为 ,那么最终的答案就是 。 结果可以公式变形运算为:
,
卡特兰数的应用
- 一个栈(无穷大)的进栈序列为 ,有多少个不同的出栈序列?
- 有 个人排成一行进入剧场。入场费 元。其中只有 个人有一张 元钞票,另外 人只有 元钞票,剧院无其它钞票,问有多少中方法使得只要有 元的人买票,售票处就有 元的钞票找零?
- 一位大城市的律师在她住所以北 个街区和以东 个街区处工作。每天她走 个街区去上班。如果她从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
- 将一个凸多边形区域分成三角形区域的方法数?
- 在圆上选择 个点,将这些点成对连接起来使得所得到的 条线段不相交的方法数?
- 给定 个节点,能构成多少种形状不同的二叉树?
代码实现
class Solution {
public int numTrees(int n) {
// 在这里需要用 long 类型防止计算过程中的溢出
long C = 1;
for (int i = 0; i < n; ++i) {
C = C * 2 * (2 * i + 1) / (i + 2);
}
return (int) C;
}
}
复杂度分析
-
时间复杂度 : ,其中 表示二叉搜索树的节点个数。我们只需要循环遍历一次即可。
-
空间复杂度 : 。我们只需要常数空间存放若干变量。