LeetCode算法系列 96. 不同的二叉搜索树

602 阅读5分钟

白菜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

方法一:动态规划

思路与算法

给定一个有序序列 1n1 \cdots n,为了构建出一棵二叉搜索树,我们可以遍历每个数字 ii将该数字作为树根,将 1(i1)1 \cdots (i-1) 序列作为左子树,将 (i+1)n(i+1) \cdots n 序列作为右子树。接着我们可以按照同样的方式递归构建左子树和右子树。

在上述构建的过程中,由于根的值不同,因此我们能保证每棵二叉搜索树是唯一的

由此可见,原问题可以分解成规模较小的两个子问题,且子问题的解可以复用。因此,我们可以想到使用动态规划来求解本题。

题目要求是计算不同二叉搜索树的个数。为此,我们可以定义两个函数:

  1. G(n)G(n): 长度为 nn 的序列能构成的不同二叉搜索树的个数。
  2. F(i,n)F(i, n): 以 ii 为根、序列长度为 nn 的不同二叉搜索树个数 (1in)(1 \leq i \leq n)

不同的二叉搜索树的总数 G(n)G(n),是对遍历所有 ii (1in)(1 \le i \le n)F(i,n)F(i, n) 之和。

公式1G(n)=i=1nF(i,n)G(n) = \sum_{i=1}^{n} F(i, n)

于边界情况,当序列长度为 11(只有根)或为 00(空树)时,只有一种情况,即:

G(0)=1G(0) = 1, G(1)=1G(1) = 1

给定序列 1n1 \cdots n,我们选择数字 ii 作为根,则根为 ii 的所有二叉搜索树的集合是左子树集合和右子树集合的笛卡尔积,对于笛卡尔积中的每个元素,加上根节点之后形成完整的二叉搜索树。

举例说明

创建以 33 为根、长度为 77 的不同二叉搜索树,整个序列是 [1,2,3,4,5,6,7][1, 2, 3, 4, 5, 6, 7],我们需要从左子序列 [1,2][1, 2] 构建左子树,从右子序列 [4,5,6,7][4, 5, 6, 7] 构建右子树,然后将它们组合(即笛卡尔积)。

对于这个例子,不同二叉搜索树的个数为 F(3,7)F(3, 7)。我们将 [1,2][1,2] 构建不同左子树的数量表示为 G(2)G(2), 从 [4,5,6,7][4, 5, 6, 7] 构建不同右子树的数量表示为 G(4)G(4),注意到 G(n)G(n) 和序列的内容无关,只和序列的长度有关。于是,F(3,7)=G(2)G(4)F(3,7) = G(2) \cdot G(4)。因此,我们可以得到以下公式:

公式2F(i,n)=G(i1)G(ni)F(i, n) = G(i-1) \cdot G(n-i)

公式1,公式2 结合,可以得到 G(n)G(n) 的递归表达式:

G(n)=i=1nG(i1)G(ni)G(n) = \sum_{i=1}^{n}G(i-1) \cdot G(n-i)

至此,我们从小到大计算 GG 函数即可,因为 G(n)G(n) 的值依赖于 G(0)G(n1)G(0) \cdots G(n-1)

代码实现

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];
    }
}

复杂度分析

  • 时间复杂度 : O(n2)O(n^2),其中 nn 表示二叉搜索树的节点个数。G(n)G(n) 函数一共有 nn 个值需要求解,每次求解需要 O(n)O(n) 的时间复杂度,因此总时间复杂度为 O(n2)O(n^2)

  • 空间复杂度 : O(n)O(n)。我们需要 O(n)O(n) 的空间存储 GG 数组。

方法二:卡特兰数

卡特兰数简介

卡特兰数是组合数学中的一种著名数列,通常用如下通项式表示:

f(n)=C2nnn+1f(n)=\frac{C_{2n}^{n}}{n+1}

卡特兰数也是有递推式的(和 方法一 的 G(n)G(n) 的递归表达式一样):

f(n)=i=1nf(i1)×f(ni)f(n)=\sum_{i=1}^{n}f(i-1)\times f(n-i)

但在实际应用中,最常用的却是第一个通项式的变形:

f(n)=C2nnC2nn1f(n)=C_{2n}^{n}-C_{2n}^{n-1}

卡特兰数推导

基本模型:有一个长度为 2n2n00,11 序列,其中 11,00nn 个,要求对于任意的整数k[1,2n]k \in [1,2n],数列的前 kk 个数中,11 的个数不少于 00

  1. 我们把 00,11 操作扔到一个坐标系中。11 看成向右上方走一步,00 看成向右下角走一步,那么最后构造完后一定走到了(2n,0)(2n,0),如下图:

那么总的路径数量就是在 2n2n 步中选择 nn 步为 11,方案数为 C2nnC_{2n}^{n}

限制条件:对于任意前缀,11 的个数不少于 00。那么转化到坐标系中,也就是说走的路径不应该穿过 xx 轴,即直线 y=0y=0,也就是不接触 y=1y=-1

  1. 于是我们把与 y=1y=-1 的接触点的右边整条路径以 y=1y=-1 为对称轴翻折,于是终点变为了 (2n,2)(2n,-2),如下图:

那么此时,从 (0,0)(0,0)(2n,2)(2n,-2) 的路径必定至少穿过一次 y=1y=-1,不满足条件,那么此时的路径数量即为总路径数中不符合题意的路径数,那么我们用总路径数减去不符合的路径数,就是最终的答案。

而此时的路径数量也很简单,由于反转后终点向下移了两位,也就意味着 n1n-1 步是 11n+1n+1 步是 00,总方案为 C2nn1C_{2n}^{n-1} ,那么最终的答案就是 C2nnC2nn1C_{2n}^{n}-C_{2n}^{n-1}。 结果可以公式变形运算为:

C0=1C_0 = 1, Cn+1=2(2n+1)n+2CnC_{n+1} = \frac{2(2n+1)}{n+2}C_n

卡特兰数的应用

  1. 一个栈(无穷大)的进栈序列为 1,2,3,..n1,2,3,..n ,有多少个不同的出栈序列?
  2. 2n2n 个人排成一行进入剧场。入场费 55 元。其中只有 nn 个人有一张 55 元钞票,另外 nn 人只有 1010 元钞票,剧院无其它钞票,问有多少中方法使得只要有 1010 元的人买票,售票处就有 55 元的钞票找零?
  3. 一位大城市的律师在她住所以北 nn 个街区和以东 nn 个街区处工作。每天她走 2n2n 个街区去上班。如果她从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
  4. 将一个凸多边形区域分成三角形区域的方法数?
  5. 在圆上选择 2n2n 个点,将这些点成对连接起来使得所得到的 nn 条线段不相交的方法数?
  6. 给定 nn 个节点,能构成多少种形状不同的二叉树

代码实现

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;
    }
}

复杂度分析

  • 时间复杂度 : O(n)O(n),其中 nn 表示二叉搜索树的节点个数。我们只需要循环遍历一次即可。

  • 空间复杂度 : O(1)O(1)。我们只需要常数空间存放若干变量。