(LeetCodeHot100)96. 不同的二叉搜索树——unique-binary-search-trees

96. 不同的二叉搜索树——unique-binary-search-trees

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

img

1
2
输入:n = 3
输出:5

示例 2:

1
2
输入:n = 1
输出:1

提示:

  • 1 <= n <= 19

第一想法

  • 二叉搜索树就是左小右大

  • 用回溯法,先选一个,然后递归,回溯

  • 但是如果是先选2的话,后面的顺序是13和31创建的二叉搜索树是一样的:

    1
    2
    3
      2
    / \
    1 3

官方答案

方法一:动态规划

思路

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

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

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

算法

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

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

可见,G(n) 是我们求解需要的函数。

稍后我们将看到,G(n) 可以从 F(i,n) 得到,而 F(i,n) 又会递归地依赖于 G(n)。

首先,根据上一节中的思路,不同的二叉搜索树的总数 G(n),是对遍历所有 i (1≤in) 的 F(i,n) 之和。换言之:

$G(n) = \sum_{i=1}^{n} F(i,n)$

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

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

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

fig1

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

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

1
F(i,n)=G(i−1)⋅G(n−i)

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

$G(n) = \sum_{i=1}^{n} G(i-1) \cdot G(n-i)$

至此,我们从小到大计算 G 函数即可,因为 G(n) 的值依赖于 G(0)⋯G(n−1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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),其中 n 表示二叉搜索树的节点个数。G(n) 函数一共有 n 个值需要求解,每次求解需要 O(n) 的时间复杂度,因此总时间复杂度为 O(n2)。
  • 空间复杂度 : O(n)。我们需要 O(n) 的空间存储 G 数组。

清晰解释

  • 假设 n 个节点存在二叉排序树的个数是 G (n),令 f(i) 为以 i 为根的二叉搜索树的个数,则
1
G(n)=f(1)+f(2)+f(3)+f(4)+...+f(n)
  • i 为根节点时,其左子树节点个数为 i-1 个,右子树节点为 n-i,则
1
f(i)=G(i−1)∗G(n−i)

难点就在于

f(i)=G(i−1)∗G(n−i)

这个地方。

首先,这个公式翻译一下就是以i为根节点的二叉搜索树的数量等于以i-1的总数的二叉搜索树的数量乘以以n-i的二叉搜索树的数量。

什么意思呢?

意思就是

假设有n=100,i=50,那么就有以50这个数字为根节点的二叉搜索树的数量等于以49为总数的二叉搜索树的数量乘以以50为总数的二叉搜索树的数量。注意,此时50和49和50,三个数字代表的是不同的意义,第一个50是根节点,第二个49是总数,第三个50还是总数。

想象一下,50这个数字的左边,很明显仅能够从1到49这49个数字进行挑选,这很好理解,因为二叉搜索树的左子树的所有值都必须小于根节点,它们继续在50的左子树下面排列组合,得出的最终各种排列的总数就是G(49)。重点在后面,在50这个数字的根节点的右边,很显然只能是51到100,这50个数字进行排列组合,此时很多人不理解,为什么51到100的排列组合的总数等于G(50),G(50)从字面意义上看,也就是从0到50的排列组合的总数。

其实道理很简单,将51到100这些数字排列组合的总数,是等于从0到50的排列组合的总数的,因为将51替换为1,将52替换为2,以此类推下去,将100替换为50,就很容易理解了,因为本质上就是50个从大到小的不同的数进行二叉树的排列组合,不论是1001到1050,还是从51到100,所排列组合的二叉树数量是一样的。

所以,这也就解释了这个公式为什么是f(50) = G(49) * G(50),因为在50这个根节点的左边,有1到49这些数字在不断进行排列组合得出总的排列组合数,在50的右边,有51到100这些数字同样的在不断的进行排列组合得出总的排列组合数,自然而然的,50作为根节点时,所能得到的不同组合,就是G(49) * G(50)了。

第一步:为什么需要「计算 i 个节点的情况」?

因为这是「动态规划的核心思想:从小到大递推」—— 要算出 N 个节点的二叉搜索树总数(G[N]),必须先知道「比 N 小的所有节点数(1、2、…、N-1)对应的总数」,因为:

  • 当总节点数是 N 时,选任意 root 作为根,左子树的节点数是 root-1(比如 root=50 时,左子树 49 个节点),右子树的节点数是 N-root(右子树 50 个节点);
  • 而左子树 49 个节点的总数是 G[49],右子树 50 个节点的总数是 G[50] —— 这些都是「比 N 小的节点数对应的结果」,必须提前算好!

代码里的外层循环 for (int i = 2; i <= n; ++i),本质就是「依次计算 2 个节点、3 个节点、…、直到 n 个节点的总数」—— 把这些中间结果存到 G 数组里,后面计算更大的 i 时直接复用,不用重复计算。

第二步:用具体例子帮你打通(N=4,总节点数 4)

我们跟着代码走一遍,看「i 个节点」的计算是怎么服务于最终 N=4 的:

  1. 初始化:G[0]=1(空树,基础 case),G[1]=1(1 个节点,1 种树);

  2. 计算i=2

    (2 个节点的总数):

    • 枚举根节点 j=1:左 0 个(G[0]),右 1 个(G[1])→ 贡献 1*1=1;
    • 枚举根节点 j=2:左 1 个(G[1]),右 0 个(G[0])→ 贡献 1*1=1;
    • G[2] = 1+1=2(2 个节点有 2 种树);
  3. 计算i=3

    (3 个节点的总数):

    • 枚举 j=1:左 0→G [0],右 2→G [2] → 1*2=2;
    • 枚举 j=2:左 1→G [1],右 1→G [1] →1*1=1;
    • 枚举 j=3:左 2→G [2],右 0→G [0] →2*1=2;
    • G[3] = 2+1+2=5(3 个节点有 5 种树);
  4. 计算i=4

    (最终要的 4 个节点总数):

    • 枚举 j=1:左 0→G [0],右 3→G [3] →1*5=5;
    • 枚举 j=2:左 1→G [1],右 2→G [2] →1*2=2;
    • 枚举 j=3:左 2→G [2],右 1→G [1] →2*1=2;
    • 枚举 j=4:左 3→G [3],右 0→G [0] →5*1=5;
    • G[4] =5+2+2+5=14(4 个节点有 14 种树);

你看:计算 i=4 时,完全依赖之前算好的 G[0]、G[1]、G[2]、G[3] —— 这就是为什么需要「先算 i 个节点的情况」:用小问题的答案,拼出大问题的答案