第 5 章 树与二叉树
408 数据结构第 5 章 树与二叉树复习讲义,涵盖核心概念、重点考点与典型题型。
Last updated on
先建立一个总框架:
树与二叉树
│
├─ 5.1 树
│ ├─ 基本术语
│ └─ 结点数、度、边数、高度关系
│
├─ 5.2 二叉树 ★★★
│ ├─ 二叉树性质
│ ├─ 满二叉树
│ ├─ 完全二叉树 ★★★
│ └─ 顺序/链式存储
│
├─ 5.3 二叉树遍历 ★★★★★
│ ├─ 先序 NLR
│ ├─ 中序 LNR
│ ├─ 后序 LRN
│ ├─ 层序
│ ├─ 根据遍历序列重建二叉树 ★★★★
│ └─ 线索二叉树 ★★★
│
├─ 5.4 树、森林 ★★★
│ ├─ 三种存储
│ ├─ 左孩子—右兄弟
│ └─ 树/森林 ↔ 二叉树
│
└─ 5.5 应用
├─ 哈夫曼树、哈夫曼编码 ★★★★
└─ 并查集 ★★★其中最值得投入时间的是:
二叉树性质 → 遍历 → 遍历算法题 → 遍历序列重构 → 哈夫曼。
资料也特别强调,遍历是二叉树各种算法操作的基础,历年曾直接结合遍历考算法题。
一、5.1 树的基本概念
1. 什么叫树
树本质上描述的是一种一对多的层次关系。
例如:
A
/ | \
B C D
/ \
E FA 是根。
A 的孩子是 B、C、D。
B 的孩子是 E、F。
从结构上看,最重要的性质是:
- 根结点没有双亲;
- 除根外,每个结点恰好一个双亲;
- 一个结点可以有任意多个孩子。
因此,一棵有 个结点的非空树一定有:
条边。
这个结论非常重要。
为什么?
因为除了根以外,其余每个结点都需要“一条从双亲过来的边”。
共有 个非根结点,所以就是 条边。
二、树中的各种“度”
设结点 A 有 3 个孩子:
A
/ | \
B C D那么 A 的度为 3。
结点的度
就是:
这个结点有几个孩子。
树的度
则是:
整棵树中最大的结点度数。
例如某棵树最大的结点有 4 个孩子,那么树的度就是 4。
三、树最重要的数量关系 ★★★
设所有结点的度之和为 。
每一个“孩子关系”对应一条边,因此:
所以:
这就是大量选择题的底层公式。
典型题型:分支结点的度全部为
题库中就有:
一棵树共有 个结点,所有分支结点的度都为 ,求叶结点数。
设:
- 分支结点数为 ;
- 叶结点数为 。
因为:
所有分支结点度数都是 ,所以总度数:
于是:
所以:
这种题不要专门背答案。
记住:
树的所有度之和 = 边数 = 结点数 − 1。
然后现场列方程即可。
四、 叉树的最大结点数
若树的度最大为 。
第 1 层最多:
第 2 层最多:
第 3 层最多:
所以第 层最多:
高度为 时,结点最多:
即:
最小高度
有 个结点,希望树尽可能矮。
怎么办?
当然是:
每一层尽可能塞满。
于是:
整理:
所以:
这是资料明确标注过考点的类型。
五、5.2 二叉树 ★★★★★
二叉树和“度为 2 的树”不能混为一谈。
二叉树规定:
每个结点最多有两个孩子,而且必须区分左孩子和右孩子。
比如:
A
/
B和
A
\
B是两棵不同的二叉树。
这件事非常重要。
六、满二叉树
高度为 的满二叉树,每一层都达到最大结点数:
A
/ \
B C
/ \ / \
D E F G结点总数:
叶结点全部位于最后一层:
七、完全二叉树 ★★★★★
完全二叉树是408最喜欢考的特殊二叉树之一。
最直观的理解:
前面每层全部放满,最后一层从左往右连续放置。
例如:
1
/ \
2 3
/ \ /
4 5 6是完全二叉树。
而:
1
/ \
2 3
\
5不是。
因为最后一层出现了“左边空、右边有”的洞。
八、完全二叉树编号公式 ★★★★★
按:
从上到下、从左到右
编号:
1
/ \
2 3
/ \ / \
4 5 6 7对编号 :
双亲
左孩子
右孩子
当然前提是这些编号没有超过 。
最后一个分支结点
完全二叉树有 个结点。
最后一个可能拥有孩子的结点是:
因此:
是分支结点,
而:
一定是叶结点。
九、完全二叉树高度 ★★★★
若有 个结点:
也可以写:
理解来源:
高度为 时:
取对数即可。
十、二叉树最核心公式 ★★★★★
设:
- :度为 0 的结点;
- :度为 1 的结点;
- :度为 2 的结点。
总数:
另一方面,边数:
而度为 1 的结点产生 1 条边,度为 2 的结点产生 2 条边:
两个式子相减:
得到:
这可能是整个树章节最值得直接背下来的公式之一。
资料也明确强调这是高频选择题性质。
例子
假如二叉树中:
那么叶结点一定:
根本不需要知道 。
十一、正则二叉树
正则二叉树规定:
每个结点度只能是 0 或 2。
所以:
又:
总数:
代入:
所以:
题库里就直接有这一类题。
十二、二叉树的存储 ★★★
1. 顺序存储
完全二叉树特别适合数组存储。
因为其编号关系非常规律:
数组下标:1 2 3 4 5 6 7
对应结点:A B C D E F G马上能找到父子。
资料也指出,完全二叉树顺序存储是高频应用;但普通二叉树如果采用这种方式,可能为了保存结构而插入大量空位置,单支树甚至可能浪费接近指数级空间。
所以:
完全二叉树/堆 → 数组特别合适。
普通二叉树:
链式存储更自然。
十三、二叉链表
基本结构:
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild;
struct BiTNode *rchild;
} BiTNode, *BiTree;逻辑结构:
data
/ \
lchild rchild十四、二叉链表中的空指针数 ★★★★
有 个结点。
每个结点有两个孩子指针,所以总指针域:
二叉树有:
条边。
也就是有 个非空指针。
所以空指针:
得到:
这个结论就是线索二叉树存在的基础。资料也特别指出它常用于选择题。
十五、5.3 二叉树遍历 ★★★★★
这是整个第 5 章最重要的内容。
必须形成这样的条件反射:
先序:根 左 右 NLR
中序:左 根 右 LNR
后序:左 右 根 LRN注意:
三种遍历,“左子树一定在右子树之前”。
区别只有:
根什么时候访问。
十六、用一棵树彻底搞懂三种遍历
例如:
A
/ \
B C
/ \ \
D E F先序
根 → 左 → 右:
A B D E C F中序
左 → 根 → 右:
D B E A C F后序
左 → 右 → 根:
D E B F C A十七、为什么递归代码几乎一模一样
先序:
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}中序:
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}后序:
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}真正不同的只有 visit(T) 的位置。
因此你最好不要背三个函数。
只背一个通用模板:
void Track(BiTree T) {
if (T != NULL) {
// 位置1:先序处理
Track(T->lchild);
// 位置2:中序处理
Track(T->rchild);
// 位置3:后序处理
}
}这正是资料最后归纳总结特别强调的算法思想。
十八、算法题真正怎么考 ★★★★★
408并不一定问:
“写一个先序遍历。”
更可能问:
利用二叉树遍历,完成某项统计或修改。
所以最关键的问题变成:
我应该在递归过程中返回什么?
十九、典型算法 1:统计叶结点数
叶结点定义:
T->lchild == NULL && T->rchild == NULL于是:
int LeafCount(BiTree T) {
if (T == NULL)
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1;
return LeafCount(T->lchild)
+ LeafCount(T->rchild);
}理解:
整棵树叶结点数 = 左子树叶数 + 右子树叶数。
这是典型的“分治 + 递归”。
二十、典型算法 2:求二叉树高度 ★★★★★
核心思想:
一棵树的高度 = 左右子树最大高度 + 1。
所以:
int Height(BiTree T) {
if (T == NULL)
return 0;
int left = Height(T->lchild);
int right = Height(T->rchild);
return (left > right ? left : right) + 1;
}公式:
题库也专门出现了这种代码的复杂度题:虽然树高可能只有对数级,但函数仍访问所有结点,所以时间复杂度是 ,而不是 。
二十一、典型算法 3:交换所有左右子树
void Swap(BiTree T) {
if (T == NULL)
return;
BiTree temp = T->lchild;
T->lchild = T->rchild;
T->rchild = temp;
Swap(T->lchild);
Swap(T->rchild);
}本质:
每个结点访问一次,并交换两条指针。
时间:
二十二、典型算法 4:删除整棵树
这个地方非常适合理解为什么需要后序。
如果你先删除根:
delete(root)那你连左右子树指针都可能丢掉。
所以应该:
先删左
先删右
最后删自己也就是后序遍历思想。
void Destroy(BiTree T) {
if (T != NULL) {
Destroy(T->lchild);
Destroy(T->rchild);
free(T);
}
}这就是:
“处理一个对象之前,需要先处理它依赖的子对象”
时典型使用后序。
二十三、层序遍历 ★★★★
层序遍历不是递归,而是:
一层一层从左到右。
使用队列。
根入队
while 队列不空:
出队 p
访问 p
左孩子存在 → 入队
右孩子存在 → 入队代码:
void LevelOrder(BiTree T) {
InitQueue(Q);
if (T != NULL)
EnQueue(Q, T);
while (!IsEmpty(Q)) {
BiTree p;
DeQueue(Q, p);
visit(p);
if (p->lchild != NULL)
EnQueue(Q, p->lchild);
if (p->rchild != NULL)
EnQueue(Q, p->rchild);
}
}为什么是队列
因为第二层结点:
B C要按照进入顺序:
B → C继续扩展。
这正是 FIFO。
题库中甚至直接给出一棵树,要求根据先序结果判断另一个序列是什么遍历,答案就是层序。
二十四、由遍历序列重建二叉树 ★★★★★
这是非常经典的综合题。
先记结论:
可以唯一确定
先序 + 中序
后序 + 中序
层序 + 中序通常不能唯一确定
先序 + 后序
先序 + 层序
后序 + 层序核心原因:
中序序列能够告诉你左子树和右子树的边界。
二十五、先序 + 中序重建 ★★★★★
例如:
先序:
A B D E C F中序:
D B E A C F先序第一个一定是根:
A去中序中找 A:
D B E | A | C F于是:
左子树:D B E
右子树:C F因为左子树 3 个结点,所以回先序:
A | B D E | C F左子树先序:
B D E右子树先序:
C F继续递归。
做题口诀
先序 + 中序
先序找根,中序切左右。
后序 + 中序
后序末尾找根,中序切左右。
整个算法就是不停递归。
二十六、为什么“先序 + 后序”不够
假设:
先序:
A B后序:
B A可能是:
A
/
B也可能是:
A
\
B两棵不同的树。
所以无法唯一确定。
二十七、线索二叉树 ★★★
这一部分刚学时容易觉得抽象,其实目的只有一句话:
把原本浪费的空指针拿来记录遍历序列中的前驱和后继。
前面已经知道:
个结点的二叉链表有:
个空指针。
既然空着,不如利用。
二十八、中序线索二叉树
普通结点:
lchild | data | rchild线索结点增加:
lchild | ltag | data | rtag | rchild规定:
左边
ltag = 0 → lchild 是左孩子
ltag = 1 → lchild 是中序前驱右边
rtag = 0 → rchild 是右孩子
rtag = 1 → rchild 是中序后继一定要记:
tag = 0 是真正的孩子 tag = 1 是线索
二十九、线索化的本质 ★★★★
线索化不是一种神秘算法。
本质就是:
遍历二叉树的同时记录上一个访问的结点。
例如中序:
D B E A C F当现在访问 E:
pre = B
p = E因此:
B 是 E 的中序前驱
E 是 B 的中序后继如果相关指针为空,就把它改为线索。
核心代码思想:
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
pre = p;三十、中序线索树怎么找后继 ★★★★
假设当前是 。
情况 1:有后继线索
即:
p->rtag == 1那么:
p->rchild直接就是后继。
情况 2:有真正的右子树
即:
p->rtag == 0根据中序:
左 根 右访问完 后,要进入它的右子树。
而右子树里首先访问谁?
最左下的结点。
因此:
后继 = 右子树最左结点这个规则必须会。
三十一、为什么后序线索树查后继比较麻烦
这是很好的理解题。
后序:
左 → 右 → 根当前结点访问结束后,有时要回到双亲。
但普通二叉链表:
结点 → 孩子并没有:
孩子 → 双亲所以可能无法直接回去。
因此资料强调:
后序线索二叉树若想高效求后继,常常还需要双亲信息。
这也是题库中“线索化后仍不能有效解决什么问题”的常见判断方向。
三十二、5.4 树的存储结构
这一块不难,重点理解“哪种查什么方便”。
双亲表示法
数组中每个结点记录:
data + parent例如:
index data parent
0 A -1
1 B 0
2 C 0
3 D 1优点:
找双亲非常快。
因为:
nodes[i].parent直接就是。
缺点:
找所有孩子麻烦。
得扫描整个数组。
资料也特别比较了这一点。
三十三、孩子表示法
反过来:
每个结点维护自己的孩子链表。
例如:
A → B → C → D
B → E → F优点:
找孩子方便。
缺点:
找双亲不方便。
所以可以把两种方式看成:
| 存储 | 找双亲 | 找孩子 |
|---|---|---|
| 双亲表示法 | 快 | 慢 |
| 孩子表示法 | 慢 | 快 |
三十四、孩子兄弟表示法 ★★★★
这是非常重要的结构,因为它直接通向:
树 ↔ 二叉树。
结点:
typedef struct CSNode {
ElemType data;
struct CSNode *firstchild;
struct CSNode *nextsibling;
} CSNode;含义:
左指针 = 第一个孩子
右指针 = 下一个兄弟记成一句:
左孩子,右兄弟。
资料正是用这种方式把一般树映射为二叉树。
三十五、树转换成二叉树 ★★★★
例如原树:
A
/ | \
B C D
/ \
E F按照:
左孩子,右兄弟
得到:
A
/
B
/ \
E C
\ \
F D注意这里:
- A 的左孩子 B = A 的第一个孩子;
- B 的右孩子 C = B 的下一个兄弟;
- C 的右孩子 D = C 的下一个兄弟;
- B 的左孩子 E;
- E 的右孩子 F。
三十六、树转二叉树的一个高频性质
因为原树的根没有兄弟,所以:
树转成的二叉树,其根一定没有右子树。
这个很容易出判断题。
三十七、森林转换成二叉树 ★★★★
假设森林:
T1 T2 T3三棵树的根:
A B C转换后:
A
\
B
\
C也就是说:
森林中不同树的根,也当作兄弟处理。
所以:
- 第一棵树根 → 二叉树根;
- 第二棵树根 → 第一棵根的右孩子;
- 第三棵树根 → 第二棵根的右孩子。
题库中就直接问:
三棵树结点数分别为 ,转换后根的右子树有多少结点?
答案显然:
因为第一棵树对应根和根的左子树,后两棵都进入右子树。
三十八、树、森林与二叉树的遍历对应 ★★★
这是一个很适合直接记忆的表。
| 原结构 | 遍历 | 对应二叉树 |
|---|---|---|
| 树 | 先根 | 先序 |
| 树 | 后根 | 中序 |
| 森林 | 先序 | 先序 |
| 森林 | 中序 | 中序 |
尤其记:
树的后根遍历 = 对应二叉树的中序遍历。
很多人会误认为对应后序,这里很容易丢分。
三十九、5.5 哈夫曼树 ★★★★★
先理解它究竟优化什么。
假设有四个字符:
A 出现 50 次
B 出现 25 次
C 出现 15 次
D 出现 10 次显然:
A 最常见,所以最好用最短编码。
D 最少见,可以用长一些。
哈夫曼树就是在做:
让权值大的叶子靠近根,权值小的叶子远离根。
从而让总体代价最小。
四十、WPL
设叶结点 :
- 权值 ;
- 到根的路径长度 。
带权路径长度:
整棵树:
哈夫曼树就是:
WPL 最小的二叉树。
四十一、哈夫曼树构造过程 ★★★★★
给权值:
2, 4, 5, 7每一步:
选当前最小两个。
第一步
最小:
2, 4合并:
现在:
5, 6, 7第二步
最小:
5, 6合并:
现在:
7, 11第三步
合并:
最终:
18
/ \
7 11
/ \
5 6
/ \
2 4四十二、WPL 怎么算
路径长度:
- 7 深度 1;
- 5 深度 2;
- 2 深度 3;
- 4 深度 3。
所以:
即:
四十三、哈夫曼树的重要性质 ★★★★
若有 个初始权值。
这些全部最终成为叶子。
每次:
两棵树 → 新建一个双亲最后需要合并:
次。
所以新建:
个分支结点。
总结点:
所以:
必为奇数。
同时不存在度为 1 的结点
因为每次新结点都同时连接两棵树。
所以:
度只有 0 和 2即:
二叉哈夫曼树是正则二叉树。
因此:
四十四、哈夫曼树并不一定唯一
如果出现相同权值:
2, 2, 3, 3选择顺序可能不同。
而且左右孩子也可以交换。
因此:
树形可能不同。
编码:
也可能不同。
高度:
甚至可能不同。
但是有一样东西不会变:
题库就专门考查“一组权值对应的哈夫曼树可能不唯一、高度也可能不唯一”。
四十五、哈夫曼编码 ★★★★
规定:
左边 = 0
右边 = 1例如:
root
/ \
A *
/ \
B C则:
A = 0
B = 10
C = 11四十六、为什么哈夫曼编码一定是前缀编码
前缀编码要求:
任何字符的编码,都不能是另一个字符编码的前缀。
因为字符只放在叶结点。
假设 A 编码对应一个叶子:
0那么这条路径到 A 就结束了。
A 下面不可能再有:
00
01对应其他字符。
所以天然没有前缀冲突。
四十七、前缀编码题怎么想
题库中有典型题:
已有字符编码
0和10,编码长度最多 4,还能编码多少字符?
因为 0 已经作为一个完整编码:
0xxx全部不能再用。
10 也已经占据一棵子树:
10xx也不能用。
剩余只剩:
11为了容纳最多字符,把它扩展到长度 4:
1100
1101
1110
1111因此最多还能有:
个字符。题库解析也是按这一思路处理。
四十八、 叉哈夫曼树的数量关系
题库还涉及一般 叉哈夫曼树。
若:
- 叶结点 ;
- 非叶结点 ;
- 每个非叶结点度都是 。
边数一方面:
另一方面:
所以:
整理:
因此:
二叉树 时自然退化为:
四十九、并查集 ★★★★
并查集名字虽然奇怪,本质非常简单:
管理若干互不相交的集合。
核心只解决两个问题:
Find
x 属于哪个集合?
Union
把两个集合合并。
五十、并查集为什么用树
例如:
集合1:{0,1,2}
集合2:{3,4}可以表示:
0 3
/ \ |
1 2 4每棵树代表一个集合。
根代表这个集合。
五十一、并查集数组表示
例如:
下标: 0 1 2 3 4
值: -3 0 0 -2 3解释:
S[1] = 0表示:
1 的双亲是 0。
S[2] = 0表示:
2 的双亲是 0。
而:
S[0] = -3负数表示:
0 是根。
绝对值:
表示这个集合有 3 个元素。
五十二、Find
int Find(int S[], int x) {
while (S[x] >= 0)
x = S[x];
return x;
}就是:
不停找爸爸
→ 找爸爸的爸爸
→ 找爸爸的爸爸的爸爸
→ 直到根例如:
5 → 3 → 1 → 0那么:
Find(5) = 0五十三、Union
如果:
Root1 = 0
Root2 = 3直接:
S[Root2] = Root1;即:
3 → 0两棵树就合成一个集合。
五十四、并查集为什么要优化 ★★★★
如果每次乱挂:
0
\
1
\
2
\
3
\
4Find(4) 要:
4→3→2→1→0变成:
很差。
所以有两个重要优化。
五十五、优化一:小树挂大树
例如:
集合 A:100 个结点
集合 B:2 个结点不要把 A 挂在 B 下面。
应该:
B → A即:
小树挂到大树根下面。
这样树不容易变深。
资料中给出的按大小合并后,树高可以控制在对数级附近。
五十六、优化二:路径压缩 ★★★★★
假设:
6 → 5 → 3 → 1 → 0第一次:
Find(6)已经知道最终根是 0。
那以后何必还绕路?
直接变成:
0
/ | | \
1 3 5 6即:
6 → 0
5 → 0
3 → 0这叫:
路径压缩。
路径压缩最经典的递归写法
int Find(int x) {
if (parent[x] < 0)
return x;
return parent[x] = Find(parent[x]);
}关键不是前半句。
真正精彩的是:
parent[x] = Find(parent[x]);它意味着:
- 找到最终根;
- 把自己的 parent 直接改成根。
查找和压缩同时完成。
五十七、并查集和 Kruskal ★★★★
题库直接考:
哪个算法需要并查集?
答案:
Kruskal 最小生成树算法。
为什么?
Kruskal 不断选择最小边:
u —— v但必须判断:
加入这条边会不会形成环?
判断方法:
Find(u) == Find(v)说明:
u 和 v 本来已经连通。
此时再加边一定形成环。
如果:
Find(u) != Find(v)说明属于不同连通分量:
可以加边
然后 Union(u,v)所以整个逻辑就是:
for 每条由小到大的边(u,v):
if Find(u) != Find(v):
选这条边
Union(u,v)这也是并查集在408中最重要的应用。
五十八、这一章的算法题,你真正应该掌握什么
资料最后列出了很多典型递归问题,包括统计不同度结点、求高度、宽度、删除叶子、计算层次、求最大值、交换左右孩子等。
不要把它们背成十道独立算法。
它们其实都是一个东西:
Solve(T) {
if (T == NULL)
...
// 根的处理
Solve(T->lchild);
// 根的处理
Solve(T->rchild);
// 根的处理
}你真正要判断的是:
第一问:空树返回什么?
例如高度:
0叶结点数:
0第二问:左右子树给我什么信息?
高度问题:
左右子树高度叶数问题:
左右子树叶数最大值:
左右子树最大值第三问:怎么合并左右答案?
高度:
叶数:
总结点:
这才是二叉树算法题的核心。
五十九、这一章最容易混淆的 10 个点
-
二叉树 ≠ 度为 2 的树。
-
完全二叉树最后一层必须从左向右连续。
-
二叉树:
和 没关系。
- 二叉链表的空指针数:
- 先序、中序、后序的左右顺序始终都是:
左在右前只是根的位置不同。
-
先序 + 后序通常不能唯一确定二叉树。
-
有中序 + 另一个序列通常才可以唯一划分左右子树。
-
树转换二叉树:
左孩子,右兄弟- 树的后根遍历对应二叉树的:
中序,不是后序。
- 哈夫曼树可能不唯一,但:
的最优值一致。
六十、按照408优先级复习
第一梯队:必须非常熟
1. 二叉树性质
尤其:
完全二叉树编号:
高度:
2. 四种遍历
先序 NLR
中序 LNR
后序 LRN
层序 Queue必须做到看到树就能迅速写序列。
3. 二叉树递归算法
至少熟练:
统计结点
统计叶子
统计度1/度2
求高度
交换左右子树
删除树
查找结点资料明确指出这里是统考算法题重点。
4. 根据序列构造树
必须熟练:
先序 + 中序
后序 + 中序
层序 + 中序最重要技巧:
找根 → 中序分左右 → 递归。
六十一、第二梯队:选择题高频
完全二叉树性质
二叉链表空指针
线索二叉树
树/森林与二叉树转换
哈夫曼树
哈夫曼编码
这些在你这里的题库中几乎都有单独对应题型。
六十二、第三梯队:理解为主
树的:
- 双亲表示法;
- 孩子表示法;
- 一般术语。
不需要花太多时间死背代码。
理解:
双亲表示 → 找爸爸快
孩子表示 → 找孩子快
孩子兄弟 → 方便转换二叉树基本足够。
最后给出一张“第5章考场公式表”
树
高度为 的 叉树最大结点数:
二叉树
第 层最大结点数:
高度为 最大结点数:
完全二叉树高度:
二叉链表空指针:
完全二叉树编号
遍历
先序:NLR
中序:LNR
后序:LRN
层序:队列哈夫曼
个叶结点的二叉哈夫曼树:
个总结点。
一般满 叉结构:
并查集
Find:找根
Union:合并根
优化1:小树挂大树
优化2:路径压缩
Kruskal:用并查集判环如果第 5 章只抓一条主线来学,就抓:
二叉树的递归结构 → 遍历 → 利用遍历处理问题 → 根据遍历重建结构。
一旦这条主线真正理解了,树这一章的大多数算法题就不再是“背代码”,而只是决定递归返回什么、在哪个位置处理根结点、左右子树答案怎么合并。