(LeetCodeHot100)96. 不同的二叉搜索树——unique-binary-search-trees
(LeetCodeHot100)96. 不同的二叉搜索树——unique-binary-search-trees
zhangzhang96. 不同的二叉搜索树——unique-binary-search-trees
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
1 | 输入:n = 3 |
示例 2:
1 | 输入:n = 1 |
提示:
1 <= n <= 19
第一想法
二叉搜索树就是左小右大
用回溯法,先选一个,然后递归,回溯
但是如果是先选2的话,后面的顺序是13和31创建的二叉搜索树是一样的:
1
2
32
/ \
1 3
官方答案
方法一:动态规划
思路
给定一个有序序列 1⋯n,为了构建出一棵二叉搜索树,我们可以遍历每个数字 i,将该数字作为树根,将 1⋯(i−1) 序列作为左子树,将 (i+1)⋯n 序列作为右子树。接着我们可以按照同样的方式递归构建左子树和右子树。
在上述构建的过程中,由于根的值不同,因此我们能保证每棵二叉搜索树是唯一的。
由此可见,原问题可以分解成规模较小的两个子问题,且子问题的解可以复用。因此,我们可以想到使用动态规划来求解本题。
算法
题目要求是计算不同二叉搜索树的个数。为此,我们可以定义两个函数:
- G(n): 长度为 n 的序列能构成的不同二叉搜索树的个数。
- F(i,n): 以 i 为根、序列长度为 n 的不同二叉搜索树个数 (1≤i≤n)。
可见,G(n) 是我们求解需要的函数。
稍后我们将看到,G(n) 可以从 F(i,n) 得到,而 F(i,n) 又会递归地依赖于 G(n)。
首先,根据上一节中的思路,不同的二叉搜索树的总数 G(n),是对遍历所有 i (1≤i≤n) 的 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 的所有二叉搜索树的集合是左子树集合和右子树集合的笛卡尔积,对于笛卡尔积中的每个元素,加上根节点之后形成完整的二叉搜索树,如下图所示:
举例而言,创建以 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 | class Solution { |
复杂度分析
- 时间复杂度 : 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 的:
初始化:
G[0]=1(空树,基础 case),G[1]=1(1 个节点,1 种树);计算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 种树);
- 枚举根节点
计算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 种树);
- 枚举
计算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 个节点的情况」:用小问题的答案,拼出大问题的答案!




