Appearance
树与森林
2026 大纲 四(三)1 树的存储结构、(三)3 树和森林的遍历 · 本篇负责三种存储结构与树 / 森林各自的遍历(森林与二叉树的转换、遍历对应关系的推导见《转换》)。
一般树的麻烦:指针域开几个都不对
二叉树有一个隐藏的便利——每个结点最多两个孩子,所以可以用固定大小的结点(lchild、rchild)表示。一般树没有这个便利:结点的度可能是 1、2、3 甚至几十。
于是存储上立刻两难:
- 按最大度
开 个指针——大量结点的度远小于 ,空指针泛滥; - 按每个结点的实际度开——结点大小不统一,无法用数组,指针运算全失效。
下面三种存储方式,就是对这个两难的三种不同取舍。它们的分歧只有一条:存哪个方向的边。
c
// ① 双亲表示法:只存向上的一条边
typedef struct { ElemType data; int parent; } PTNode; // 根的 parent = -1
// ② 孩子表示法(孩子链表):每个结点挂一条孩子单链表
typedef struct ChildNode { int childIdx; struct ChildNode *next; } ChildNode;
typedef struct { ElemType data; ChildNode *firstChild; } CTBox;
// ③ 孩子兄弟表示法(又称二叉链表表示法):固定两个指针
typedef struct CSNode {
ElemType data;
struct CSNode *firstchild; // 第一个孩子
struct CSNode *nextsibling; // 右兄弟(同一双亲下的下一个孩子)
} CSNode, *CSTree;选型的判据只看主要操作是往上走还是往下走:查双亲、判连通 → 双亲表示法;展开子树 → 孩子表示法;要复用二叉树的算法 → 孩子兄弟表示法。
双亲表示法:只存向上的边
用一维数组存所有结点,每个结点带一个 parent 字段指向双亲在数组中的下标。
A 下标 | data | parent
/ | \ 0 | A | -1
B C D 1 | B | 0
/ \ | 2 | C | 0
E F G 3 | D | 0
4 | E | 1
5 | F | 1
6 | G | 3这种结构的取向非常明确:它利用"除根之外每个结点只有唯一双亲"这条性质,把每个结点的存储压缩成一个整数,代价是彻底放弃向下的可达性。 找双亲 parent 找根
并查集是它的典型应用——并查集的两个核心操作恰好是"沿双亲找根"和"把一个根挂到另一个根下",全是向上操作,一个 parent[] 数组就够。这不是巧合,而是先有需求、后选结构。
一个实现细节:根的
parent有两种约定,一是置,二是指向自己。后者的好处是找根的循环可以写成 while (parent[x] != x) x = parent[x];,不用额外判负。做题时看清题面给的约定。
孩子表示法:只存向下的边
最直接的想法是让每个结点带
空链域占全部指针域的比例约为
实际用的是孩子链表:把每个结点的孩子看成一个线性表,用单链表串起来;
下标 data 孩子链表
0 A → [1] → [2] → [3] → ∧
1 B → [4] → [5] → ∧
2 C → ∧
3 D → [6] → ∧链表结点总数恰为边数
常见折中——孩子双亲表示法:在
CTBox里再加一个parent字段,找双亲、找孩子 ,只多一个整数域。
孩子兄弟表示法:把"多个孩子"改写成一条链
每个结点只用两个指针——第一个孩子(firstchild)和右兄弟(nextsibling)。
一般树的困难在于"孩子有多少个不知道"。孩子兄弟法把这件事改写成一条链:一个结点的全部孩子被串成一条以 firstchild 为表头、以 nextsibling 为链的单链表;于是每个结点只需两个指针。
原一般树 孩子兄弟表示法(firstchild 向下、nextsibling 向右)
A A
/ | \ │
B C D B ──→ C ──→ D
/ \ | │ │
E F G E ──→ F G
竖线 │ 是 firstchild(指向第一个孩子)
箭头 ──→ 是 nextsibling(指向右兄弟)关键洞察:把 firstchild 当作二叉树的左指针、nextsibling 当作右指针,整个结构在内存里与一棵二叉链表一模一样。
孩子兄弟表示法把一般树映射成了一棵二叉树。 这就是"树 / 森林 ↔ 二叉树"转换成立的全部理由——它不是一个"技巧",而是同一份存储的两种读法。
由于每个结点 2 个指针域、非空的仍是
找第一个孩子 firstchild 再沿 nextsibling 走 parent 域。
这是应用最广的一种一般树表示法,原因就是它能把一般树的操作全部转成二叉树的操作。转换规则见 树与森林和二叉树的转换。
计数题的共同起点: 度之和
这一节的题目大多不问存储结构,而问计数。这类题的出发点是同一条恒等式:
证明是边数双计数:树中每条边恰好由一个结点"射出"(从双亲指向孩子),所以边数
把它套到"给定各度结点个数、求叶结点数"上就是一步的事:总度数就是边数,边数加 1 就是总结点数,再减去已知的各度结点数,剩下的就是叶子。
由这条恒等式还能顺出几个直接可用的结论:
森林的棵数
高度为
正则
- 若有
个非叶结点,则叶结点数 。推导:非叶结点共生出 个孩子,而除根之外每个结点恰是某个非叶结点的孩子,故 。 - 若高度为
,结点数最多 (满树),最少 。最少的形态是"每层只留 1 个非叶结点":第 1 层 1 个根,第 2 层到第 层各 个。注意正则树不允许某个非叶结点只有几个孩子,所以最少不是每层 1 个。
术语辨析:正则
叉树只要求每个非叶恰有 个孩子;满 叉树还额外要求所有叶子在同一层。满一定正则,反之不然。另外,"度为 的树"要求至少存在一个度恰为 的结点,而" 叉树"只要求每个结点至多 个孩子——求"最少结点数"时这个差别直接改变答案。
树的遍历与森林的遍历
| 遍历 | 规则 | 对应二叉树的遍历 |
|---|---|---|
| 树·先根遍历 | 先访问根,再依次先根遍历根的每棵子树 | 先序遍历 |
| 树·后根遍历 | 先依次后根遍历根的每棵子树,再访问根 | 中序遍历 |
| 树·层次遍历 | 按层从上到下、每层从左到右(用队列实现) | 无严格对应 |
| 森林·先序遍历 | ① 访问第一棵树的根 → ② 先序遍历第一棵树的子树森林 → ③ 先序遍历剩余树构成的森林 | 先序遍历 |
| 森林·中序遍历 | ① 中序遍历第一棵树的子树森林 → ② 访问第一棵树的根 → ③ 中序遍历剩余树构成的森林 | 中序遍历 |
森林那两条的三步结构不是随意写的:它精确对应转换后二叉树的"根 / 左子树 / 右子树"——第一棵树的根就是二叉树的根,第一棵树的子树森林转成左子树,剩余树的森林转成右子树。
所以题目要"转换成二叉树后的先序 / 中序序列"时,直接对原树 / 森林做先根 / 后根遍历即可,不必真的把转换图画出来。
为什么树没有"中根遍历":二叉树的中序之所以能定义,是因为"根"两侧的子树数量固定为 2——"在左子树之后、右子树之前"是一个无歧义的位置。一般树的孩子数不固定,"在第几个孩子之后访问根"完全没有依据:度为 3 的结点可以在第 1 或第 2 个孩子之后访问根,度为 5 的又有 4 个可选位置。不同结点之间无法统一,所以这个次序定义不出来。
为什么森林没有"后序遍历":森林的两种遍历是从"森林 ↔ 二叉树"的对应关系里继承过来的。若照搬"左、右、根"写成"先遍历子树森林、再遍历剩余森林、最后访问第一棵树的根",得到的次序里第一棵树的根会排在后面所有树之后——这与"森林中各树有先后次序"严重冲突,没有对应的应用语义,所以不定义。
注意术语:森林的中序遍历在部分材料里也叫"森林的后根遍历",因为它对每棵树内部执行的正是后根次序。两个名字指同一件事。
遍历序列的逐步求法(想核对序列怎么写出来就展开)
A
/ | \
B C D
/ \ |
E F G- 先根遍历:A B E F C D G 推导:访问 A → 先根遍历子树 B(访问 B → 子树 E → 子树 F)→ 先根遍历子树 C → 先根遍历子树 D(访问 D → 子树 G)
- 后根遍历:E F B C G D A 推导:后根遍历子树 B(E → F → B)→ 后根遍历子树 C(C)→ 后根遍历子树 D(G → D)→ 访问 A
- 层次遍历:A B C D E F G
森林的例子:
森林 F = { T1, T2, T3 }
T1 T2 T3
A E G
/ \ | / \
B C F H I
|
D- 先序遍历森林:访问 A → 先序遍历 A 的子树森林
(B → C → D)→ 先序遍历剩余森林 (E → F → G → H → I) 结果:A B C D E F G H I - 中序遍历森林:中序遍历 A 的子树森林(B → D → C)→ 访问 A → 中序遍历剩余森林(F → E → H → I → G) 结果:B D C A F E H I G
逐层核对第二条:
考点速记
三条会被反复调用的结论:
- 三种存储结构一句话:双亲表示法只存向上的边、孩子表示法只存向下的边、孩子兄弟表示法把"多个孩子"改写成"长子 + 兄弟链";选哪种只看主要操作往上走还是往下走。
- 孩子兄弟表示法等价于一棵二叉链表,
firstchild/nextsibling就是左右指针——这是树 / 森林 ↔ 二叉树转换成立的根据。 - 一般树没有中根遍历、森林没有后序遍历,原因都是"这个次序在定义上无法统一",不是遗漏。
这一节在真题里被考过的形式(下方「真题练习」逐题对应)。有一点值得先说:这一节的真题几乎不问三种存储结构本身,问的是计数——所以复习重心应该放在上面那条恒等式和由它导出的公式上。
- 给各度结点的个数,求叶结点数。 例如"度为 4 的树中有 20 个度 4、10 个度 3、1 个度 2、10 个度 1 的结点,求叶结点数"。做法固定两步:先算总度数(
边数), ;再用 边数 , ,得 。 - 给森林的边数与结点数,求树的棵数。 直接套
。 - 给
叉树的结点数,求最小高度。 逐层累加满 叉树的容量,找第一个 的高度。例如 244 个结点的三叉树:高度 5 只能装 121 个、高度 6 能装 364 个,所以至少 6。 - 正则
叉树的推导题(大题)。 两问: 个非叶结点对应多少叶子( )、高度 时结点数的最多与最少( 与 )。这是一道要求写推导过程的题,所以"孩子总数 非根结点总数"这句话本身要能写出来。
易错:把总度数直接当成总结点数。 总度数等于边数,还要加 1 才是结点数。
易错:正则
叉树求最少结点数时按"每层 1 个"算。 正则要求非叶结点必须恰有 个孩子,所以是"每层只留 1 个非叶",第 2 层起每层都有 个结点。
易错:混淆"度为
的树"与" 叉树"、"正则 叉树"与"满 叉树"。 前者要求存在度为 的结点,后者要求所有叶子同层。
易错:以为森林也有后序遍历、树也有中根遍历。 这两个次序定义不出来。
教材出处
- 树的三种存储结构总述、双亲表示法的结点形式与"求结点的双亲十分方便,也很容易求树的根,但求结点的孩子时需要遍历整个结构"、孩子表示法的两种结点格式,以及"在一棵有
个结点度为 的树中必有 个空链域":严蔚敏《数据结构(C 语言版)》(第 2 版)印刷 p133(5.6.1 节) - 孩子链表与带双亲的孩子链表;孩子兄弟法("又称二叉树表示法,或二叉链表表示法")的结点形式、
CSNode类型定义、"若要访问结点的第 个孩子,则只要先从 firstchild域找到第 1 个孩子结点,然后沿着孩子结点的nextsibling域连续走步",以及"这种存储结构的优点是它和二叉树的二叉链表表示完全一样,便于将一般的树结构转换为二叉树进行处理":印刷 p134 - 树的先根遍历与后根遍历的定义,以及教材示例树的先根序列
RADEBCFGHK与后根序列DEABGHKFCR;"按照森林和树相互递归的定义,可以推出森林的两种遍历方法:先序遍历和中序遍历":印刷 p135(5.6.3 节) - 森林的先序遍历与中序遍历的三步规则,教材示例森林的先序序列
ABCDEFGHIJ与中序序列BCDAFEHJIG,以及"当以二叉链表做树的存储结构时,树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历的算法实现":印刷 p136
相关知识
树与二叉树基本概念(二叉树侧,边数双计数在那篇也用了一次)|树与森林 ↔ 二叉树的转换(转换规则与遍历对应的推导)|并查集(双亲表示法的典型应用)|哈夫曼树|前序遍历|中序遍历|层序遍历(一般树的层次遍历规则与之相同)