力扣hot100之96:不同的二叉搜索树
力扣hot100之96:不同的二叉搜索树

LeetCode 96:不同的二叉搜索树(Unique Binary Search Trees)题解

Unique Binary Search Trees 是 LeetCode Hot 100 里面一道非常经典的动态规划题。

这道题非常有代表性,因为它表面上是在问“由 1...n 组成的二叉搜索树有多少种不同结构”,但本质上考察的是:

  • 如何把一棵树拆成左右子树

  • 如何把“结构数量统计”写成递推

  • 如何从动态规划进一步过渡到数学公式

两种思路:

  1. 递归 + 记忆化搜索

  2. 卡特兰数公式

这两种写法都对,而且它们不是互相独立的。 更准确地说:

递归 / DP 是理解题目结构的过程,卡特兰数是把这个结构进一步抽象后的更简洁写法。

所以这篇文章我就按这个顺序来讲


题目介绍

给定一个整数 n,要求你统计:

1nn 个节点组成的二叉搜索树,一共有多少种不同的结构。

注意是“结构不同”的二叉搜索树数量,而不是遍历顺序,也不是随便二叉树。

二叉搜索树满足:

  • 左子树所有节点值都小于根节点

  • 右子树所有节点值都大于根节点

例如当 n = 3 时,节点值为:

1, 2, 3

它们一共可以组成 5 种不同的二叉搜索树,所以答案是:

5

最关键的观察

这道题最核心的一步,是去思考:

如果固定某个数字作为根节点,那么左右子树还能有多少种构造方法?

假设选数字 i 作为根节点,那么:

  • 左子树只能由 1 ... i-1 这些数字组成,一共 i-1 个节点

  • 右子树只能由 i+1 ... n 这些数字组成,一共 n-i 个节点

而且:

  • 左子树怎么构造,和右子树怎么构造,是彼此独立的

  • 所以如果左子树有 L 种构造方式,右子树有 R 种构造方式

  • 那么当前根节点对应的总数就是:

L × R

最后再把所有根节点的情况加起来,就是总答案。

这就是整道题最本质的递推来源。


解法一:递归 + 记忆化搜索

你图片里的写法,本质上是在做这件事:

定义:

numTrees(n) = 用 n 个节点能组成的不同二叉搜索树数量

那么递归关系就是:

numTrees(n) = Σ numTrees(i-1) * numTrees(n-i)   (i 从 1 到 n)

其中:

  • i 表示当前选哪个数当根

  • i-1 是左子树节点数

  • n-i 是右子树节点数

为什么要记忆化?

因为在递归过程中,会反复计算很多相同的子问题。

比如:

  • numTrees(2) 可能会被多次用到

  • numTrees(3) 也可能会被反复求

所以图片里的代码用了一个缓存数组 nums[],把算过的结果存起来。 下次再遇到同样的 n,就直接返回,不再重复递归。

这就是典型的 记忆化搜索


核心逻辑

可以概括成下面几步:

1. 边界情况

n = 0 时,返回 1

这里的 1 表示“空树也算一种结构”。

这个定义非常重要。因为如果某个根节点一边子树为空,我们仍然希望组合数公式成立。

2. 如果缓存里已经有结果,直接返回

避免重复计算。

3. 枚举每一个数字作为根节点

对于每个根节点 i

  • 左子树数量 = numTrees(i - 1)

  • 右子树数量 = numTrees(n - i)

然后把:

left × right

累加起来。

4. 把结果存进缓存

以后可以直接复用。


为什么这种写法是对的?

因为它完整体现了这道题的结构拆分:

  • 根节点一旦确定

  • 左子树和右子树的节点数量也随之确定

  • 左右子树的构造方案彼此独立,所以要相乘

  • 所有根节点情况都要考虑,所以最后求和

而缓存的加入,只是为了避免重复计算,并不改变递推本质。

所以图片里的解法,本质上其实就是:

递归版动态规划

它特别适合理解这道题为什么能递推。


进一步抽象:动态规划写法

如果把记忆化搜索改写成更经典的 DP 数组形式,就会得到:

设:

dp[k] = k 个节点能组成的不同 BST 数量

那么状态转移方程就是:

dp[n] = Σ dp[i-1] * dp[n-i]

初始条件:

dp[0] = 1
dp[1] = 1

例如:

dp[3] = dp[0]*dp[2] + dp[1]*dp[1] + dp[2]*dp[0]

如果:

dp[0] = 1
dp[1] = 1
dp[2] = 2

那么:

dp[3] = 1*2 + 1*1 + 2*1 = 5

这正好就是答案。

所以你图片里的递归写法,和标准 DP,其实本质完全一样。


解法二:卡特兰数

class Solution {
    public int numTrees(int n) {
        long res = 1;
        for (int i = 0; i < n; i++) res = res * 2 * (2 * i + 1) / (i + 2);
        return (int) res;
    }
}

这段代码没有显式写出 dp 数组,但它其实是在直接计算:

第 n 个卡特兰数

因为这道题的答案,恰好就是卡特兰数。

卡特兰数在这题里的来源,其实就是刚才那个递推:

dp[n] = Σ dp[i-1] * dp[n-i]

这个递推,正是卡特兰数最经典的定义型递推之一。

也就是说:

  • 你图片里的思路,帮我们理解了为什么答案满足这种递推

  • 卡特兰数公式,则是把这个递推进一步压缩成更简洁的数学实现


卡特兰数公式为什么能这样写?

卡特兰数常见递推形式之一是:

C0 = 1
Cn+1 = Cn * 2 * (2n + 1) / (n + 2)

而你的代码正是在按这个公式递推。

C0 = 1 开始,不断向后推到 Cn

long res = 1;
for (int i = 0; i < n; i++) {
    res = res * 2 * (2 * i + 1) / (i + 2);
}

最后得到的 res,就是答案。

这种写法的优点非常明显:

  • 不需要显式开 dp 数组

  • 不需要写双层循环

  • 时间复杂度降到 O(n)

  • 空间复杂度降到 O(1)

所以它是理解题目本质之后,一个非常漂亮的优化写法。


手动模拟一遍

我们用 n = 3 来看。

已知:

C0 = 1

第一次循环:i = 0

res = 1 * 2 * 1 / 2 = 1

得到:

C1 = 1

第二次循环:i = 1

res = 1 * 2 * 3 / 3 = 2

得到:

C2 = 2

第三次循环:i = 2

res = 2 * 2 * 5 / 4 = 5

得到:

C3 = 5

所以最终答案就是:

5

这正好和题目要求一致。


两种解法怎么理解它们的关系?

这题最值得掌握的,其实不是只会写某一种代码,而是理解这三层关系:

第一层:递归结构

枚举根节点,左右子树独立,相乘再求和。

第二层:动态规划 / 记忆化

把重复子问题缓存起来,避免反复计算。

第三层:卡特兰数

进一步发现这个递推其实就是卡特兰数,于是可以直接套更简洁的数学递推公式。

所以真正完整的理解顺序应该是:

先看懂递归结构,再接受 DP,最后认识到它其实就是卡特兰数。


复杂度分析

解法一:递归 + 记忆化 / DP

如果写成标准 DP:

  • 时间复杂度:

O(n^2)

因为每个 dp[i] 都要枚举一次根节点。

  • 空间复杂度:

O(n)

解法二:卡特兰数公式

你最终代码的复杂度更优:

  • 时间复杂度:

O(n)
  • 空间复杂度:

O(1)

所以从实现效率上来说,卡特兰数写法更强; 但从理解题目结构来说,递归 / 记忆化写法更适合讲清楚“为什么”。


总结

这道题的核心思想可以概括成一句话:

固定每个数字作为根节点时,左右子树的节点数量就确定了,而左右子树方案数彼此独立,因此总数满足“左右相乘、对所有根求和”的递推关系。

所以这题可以分成两层理解:

解法一:递归 + 记忆化搜索

核心递推:

numTrees(n) = Σ numTrees(i-1) * numTrees(n-i)

这是理解题目结构最自然的方式。

解法二:卡特兰数公式

最终实现:

class Solution {
    public int numTrees(int n) {
        long res = 1;
        for (int i = 0; i < n; i++) res = res * 2 * (2 * i + 1) / (i + 2);
        return (int) res;
    }
}

这是在看懂递推本质之后,更简洁、更优雅的实现。

这道题非常经典,因为它会让你真正体会到:

  • 树形结构如何拆成子问题

  • 动态规划如何从递归中长出来

  • 数学公式又如何进一步把 DP 压缩掉

把这道题真正吃透之后,再去看:

  • 括号生成

  • 不同的二叉搜索树 II

  • 卡特兰数相关题目

你会更容易意识到: 很多题表面不同,但它们背后统计的其实是同一种结构。

因为“不同的二叉搜索树”本身就是卡特兰数题里最经典的一道模板题。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇