不同的二叉搜索树 图解题解
这道题到底在问什么
- 输入
- n = 8
- 输出
- 1430
先想最直接的笨办法
核心一句话:枚举根,左子树 j 个节点、右子树 i-1-j 个,方案数相乘再把所有根加起来。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 96 不同的二叉搜索树用动态规划求解:dp[i] 表示 i 个节点能组成的二叉搜索树种数,枚举每个数当根,左子树方案数乘右子树方案数,再把所有根的结果加起来,即 dp[i] = Σ dp[j] × dp[i-1-j]。这串数正是卡特兰数,n=8 时答案 1430。时间 O(n²)、空间 O(n)。
这道题数的是什么,为什么只看形状
题目给整数 n,节点值固定是 1 到 n,问能组成多少种结构不同的二叉搜索树(BST,左子树全小于根、右子树全大于根的二叉树)。听起来要同时考虑「放哪些值」和「长什么形状」,其实只需要数形状。原因藏在 BST 的一条性质里:它的中序遍历必然是升序,也就是恒为 1 到 n。这意味着给定一种树的形状,把 1 到 n 按中序位置填进去的方式是唯一的——形状定了,每个节点放谁就定了。所以答案只跟节点个数有关,跟具体取哪些值无关。
为什么想到枚举根来分解问题
直接枚举所有树形是不现实的,树形数量增长极快。突破口是 BST 的结构约束:一旦选定某个数 k 当根,比 k 小的数必须全部进左子树、比 k 大的数必须全部进右子树,两边各是多少个节点就完全确定了。整棵树的问题被根一刀切成两个更小的同类问题。
更关键的是,由上一节的结论,子树的方案数只取决于节点个数。左边有 j 个节点,不管这 j 个具体是哪些值,能搭出的形状数都一样。于是「n 个节点的 BST 数」可以由「更少节点的 BST 数」推出来——这正是动态规划要的递推结构。
dp 怎么定义,为什么左右方案数是相乘
定义 dp[i] 为 i 个节点能组成的结构不同的 BST 个数。计算 dp[i] 时枚举根:让第 k 个数当根,左子树分到 j = k-1 个节点、右子树分到 i-1-j 个。左子树有 dp[j] 种搭法,右子树有 dp[i-1-j] 种,左右两边互不干扰、可以自由组合,按乘法原理这一种根贡献 dp[j] × dp[i-1-j] 种树。
不同的根产生的树根节点不同,绝不会重复,按加法原理把 i 种根的贡献全部加起来,就是 dp[i] = Σ dp[j] × dp[i-1-j],j 从 0 取到 i-1。这里最容易犯的错是把左右子树的方案数加起来——加法数的是「二选一」,而左右子树是同时存在、独立各选一种,必须相乘。
dp[0] 为什么必须等于 1 而不是 0
空树也算一种合法形态:根没有左孩子时,左子树就是空树,这种情况实实在在贡献着方案。dp[0] 在递推式里始终以乘数身份出现,比如最小的数当根时贡献是 dp[0] × dp[i-1],若 dp[0] 设成 0,所有单边为空的树会被整体乘没,dp[1] 都算不出来。把 dp[0] 定为 1,读作「空树恰有一种形态」,整个递推才立得住。
从 dp[0]=1、dp[1]=1 出发依次递推,得到 2、5、14、42、132、429、1430……n=8 时答案就是 1430。
复杂度多少,和卡特兰数是什么关系
时间复杂度 O(n²):外层 i 从 1 递推到 n,内层枚举 i 个根。空间 O(n),一维 dp 数组。
dp[i] = Σ dp[j] × dp[i-1-j] 正是卡特兰数的递推式,所以 dp[n] 就是第 n 个卡特兰数,有封闭公式 C(2n,n)/(n+1)。合法括号序列、出栈顺序等经典计数问题与本题同构,面试里能点出这一层,说明你看到的不只是一道题,而是一类「枚举分割点、左右相乘再求和」的计数结构。
▶ 动画逐步走查(共 48 步)——想跟着动画一帧帧对照就展开
- 3核心一句话:枚举根,左子树 j 个节点、右子树 i-1-j 个,方案数相乘再把所有根加起来。
- 4上行是节点数 n(固定参照),下行 dp 待填。先定基准:0 个节点是「空树」,只有 1 种,dp[0]=1。
- 5空树(一个节点都没有)也是一种合法形状,dp[0]=1。它是后面所有相乘的「乘 1」基底。
- 6算 dp[1]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 1。累加到这里共 1。
- 7把 1 个根全枚举完,方案数加起来 = 1,填进 dp[1]。
- 8算 dp[2]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 1。累加到这里共 1。
- 9算 dp[2]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 1。累加到这里共 2。
- 10把 2 个根全枚举完,方案数加起来 = 2,填进 dp[2]。
- 11算 dp[3]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 2。累加到这里共 2。
- 12算 dp[3]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 1。累加到这里共 3。
- 13算 dp[3]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 2。累加到这里共 5。
- 14把 3 个根全枚举完,方案数加起来 = 5,填进 dp[3]。
- 15算 dp[4]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 3 个有 dp[3]=5 种,左右独立相乘 = 5。累加到这里共 5。
- 16算 dp[4]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 2。累加到这里共 7。
- 17算 dp[4]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 2。累加到这里共 9。
- 18算 dp[4]:让第 4 个数当根 —— 它左边 3 个节点有 dp[3]=5 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 5。累加到这里共 14。
- 19把 4 个根全枚举完,方案数加起来 = 14,填进 dp[4]。
- 20算 dp[5]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 4 个有 dp[4]=14 种,左右独立相乘 = 14。累加到这里共 14。
- 21算 dp[5]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 3 个有 dp[3]=5 种,左右独立相乘 = 5。累加到这里共 19。
- 22算 dp[5]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 4。累加到这里共 23。
- 23算 dp[5]:让第 4 个数当根 —— 它左边 3 个节点有 dp[3]=5 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 5。累加到这里共 28。
- 24算 dp[5]:让第 5 个数当根 —— 它左边 4 个节点有 dp[4]=14 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 14。累加到这里共 42。
- 25把 5 个根全枚举完,方案数加起来 = 42,填进 dp[5]。
- 26算 dp[6]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 5 个有 dp[5]=42 种,左右独立相乘 = 42。累加到这里共 42。
- 27算 dp[6]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 4 个有 dp[4]=14 种,左右独立相乘 = 14。累加到这里共 56。
- 28算 dp[6]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 3 个有 dp[3]=5 种,左右独立相乘 = 10。累加到这里共 66。
- 29算 dp[6]:让第 4 个数当根 —— 它左边 3 个节点有 dp[3]=5 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 10。累加到这里共 76。
- 30算 dp[6]:让第 5 个数当根 —— 它左边 4 个节点有 dp[4]=14 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 14。累加到这里共 90。
- 31算 dp[6]:让第 6 个数当根 —— 它左边 5 个节点有 dp[5]=42 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 42。累加到这里共 132。
- 32把 6 个根全枚举完,方案数加起来 = 132,填进 dp[6]。
- 33算 dp[7]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 6 个有 dp[6]=132 种,左右独立相乘 = 132。累加到这里共 132。
- 34算 dp[7]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 5 个有 dp[5]=42 种,左右独立相乘 = 42。累加到这里共 174。
- 35算 dp[7]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 4 个有 dp[4]=14 种,左右独立相乘 = 28。累加到这里共 202。
- 36算 dp[7]:让第 4 个数当根 —— 它左边 3 个节点有 dp[3]=5 种、右边 3 个有 dp[3]=5 种,左右独立相乘 = 25。累加到这里共 227。
- 37算 dp[7]:让第 5 个数当根 —— 它左边 4 个节点有 dp[4]=14 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 28。累加到这里共 255。
- 38算 dp[7]:让第 6 个数当根 —— 它左边 5 个节点有 dp[5]=42 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 42。累加到这里共 297。
- 39算 dp[7]:让第 7 个数当根 —— 它左边 6 个节点有 dp[6]=132 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 132。累加到这里共 429。
- 40把 7 个根全枚举完,方案数加起来 = 429,填进 dp[7]。
- 41算 dp[8]:让第 1 个数当根 —— 它左边 0 个节点有 dp[0]=1 种、右边 7 个有 dp[7]=429 种,左右独立相乘 = 429。累加到这里共 429。
- 42算 dp[8]:让第 2 个数当根 —— 它左边 1 个节点有 dp[1]=1 种、右边 6 个有 dp[6]=132 种,左右独立相乘 = 132。累加到这里共 561。
- 43算 dp[8]:让第 3 个数当根 —— 它左边 2 个节点有 dp[2]=2 种、右边 5 个有 dp[5]=42 种,左右独立相乘 = 84。累加到这里共 645。
- 44算 dp[8]:让第 4 个数当根 —— 它左边 3 个节点有 dp[3]=5 种、右边 4 个有 dp[4]=14 种,左右独立相乘 = 70。累加到这里共 715。
- 45算 dp[8]:让第 5 个数当根 —— 它左边 4 个节点有 dp[4]=14 种、右边 3 个有 dp[3]=5 种,左右独立相乘 = 70。累加到这里共 785。
- 46算 dp[8]:让第 6 个数当根 —— 它左边 5 个节点有 dp[5]=42 种、右边 2 个有 dp[2]=2 种,左右独立相乘 = 84。累加到这里共 869。
- 47算 dp[8]:让第 7 个数当根 —— 它左边 6 个节点有 dp[6]=132 种、右边 1 个有 dp[1]=1 种,左右独立相乘 = 132。累加到这里共 1001。
- 48算 dp[8]:让第 8 个数当根 —— 它左边 7 个节点有 dp[7]=429 种、右边 0 个有 dp[0]=1 种,左右独立相乘 = 429。累加到这里共 1430。
- 49把 8 个根全枚举完,方案数加起来 = 1430,填进 dp[8]。
- 50最右 dp[8]=1430 就是 8 个节点能搭出的不同 BST 个数(这串 1,1,2,5,14,42,132,429,1430 就是卡特兰数)。
⚠️ 容易写错的地方
✗ 错:dp[0] 设成 0
✓ 对:dp[0]=1(空树 1 种)
它当乘数,设 0 会把单边子树方案全乘没
✗ 错:只数值不数形状
✓ 对:BST 由「形状」区分,值固定 1..n
中序遍历必是 1..n,决定形状的是结构
✗ 错:忘了左右独立相乘
✓ 对:左 dp[j] × 右 dp[i-1-j]
左右子树互不影响,是乘法不是加法
完整代码(Python / C++ / Java)
Python
def numTrees(n):
dp = [0] * (n + 1)
dp[0] = 1
for i in range(1, n + 1):
for j in range(i): # 左子树节点数
dp[i] += dp[j] * dp[i - 1 - j]
return dp[n]C++
int numTrees(int n){
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int i = 1; i <= n; ++i)
for(int j = 0; j < i; ++j)
dp[i] += dp[j] * dp[i - 1 - j];
return dp[n];
}Java
int numTrees(int n){
int[] dp = new int[n + 1];
dp[0] = 1;
for(int i = 1; i <= n; i++)
for(int j = 0; j < i; j++)
dp[i] += dp[j] * dp[i - 1 - j];
return dp[n];
}复杂度
时间
O(n²)
每个 i 枚举 i 个根
空间
O(n)
一维 dp 数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 不同的二叉搜索树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么这串数是卡特兰数?+
dp[i]=Σ dp[j]·dp[i-1-j] 正是卡特兰数 Cn 的递推式,所以 dp[n]=Cn=C(2n,n)/(n+1)。括号匹配、出栈序列等问题同构。
为什么节点值不影响个数?+
BST 的中序遍历恒为 1..n,给定一种「形状」就唯一确定每个节点放谁。所以只需数「形状」,与具体取值无关,只跟 n 有关。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 不同的二叉搜索树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。