一、树、森林与二叉树的核心关系
在数据结构中,树、森林、二叉树三者并非孤立存在,而是可以通过左孩子 - 右兄弟表示法相互转换,且遍历规则存在严格对应关系,这是理解整个树结构的核心基础。
1.1 树与二叉树的遍历对应
| 树(普通树) | 森林 | 二叉树 |
|---|---|---|
| 先根遍历 | 前序遍历 | 前序遍历 |
| 后根遍历 | 中序遍历 | 中序遍历 |
核心结论:
- 树的先根遍历 = 对应二叉树的前序遍历
- 树的后根遍历 = 对应二叉树的中序遍历
- 森林的前序遍历 = 对应二叉树的前序遍历
- 森林的中序遍历 = 对应二叉树的中序遍历
1.2 基础遍历示例
(1)普通树的遍历
左图为普通树,右图为其转换后的二叉树:

- 先根遍历:
ABEFCDG(先访问根,再依次遍历子树) - 后根遍历:
EFBCGDA(先依次遍历子树,再访问根) - 对应二叉树的前序遍历:
ABEFCDG,中序遍历:EFBCGDA,完全符合上述对应关系。
(2)森林的遍历
下图为包含 3 棵树的森林:

- 森林前序遍历:
ABCDEFGHJI(依次对每棵树做先根遍历) - 森林中序遍历:
BCDAFEJHIG(依次对每棵树做后根遍历)

- 对应二叉树的前序 / 中序遍历与森林完全一致,验证了转换的正确性。
二、考研真题深度解析(树 / 森林 / 二叉树核心考点)
2.1 经典选择题逐题拆解
【2009】森林转二叉树的结点关系
题目:将森林转换为对应的二叉树,若在二叉树中,结点 u 是结点 v 父结点的父结点,则在原来的森林中,u 和 v 可能具有的关系是______。
Ⅰ. 父子关系
Ⅱ. 兄弟关系
Ⅲ.u 的父结点与 v 的父结点是兄弟关系
A. 只有 Ⅱ B. Ⅰ 和 Ⅱ C. Ⅰ 和 Ⅲ D. Ⅰ、Ⅱ 和 Ⅲ
解析:方法1:举个例子

方法2:
- 二叉树的左孩子 对应原树的第一个孩子 ,右孩子 对应原树的兄弟。
- 情况 1(父子关系):u 是 v 的祖父(二叉树中 u→父→v),对应原树中 u 是 v 的祖父的兄弟,v 的父是 u 的孩子,因此 u 是 v 的祖先(父子关系成立)。
- 情况 2(兄弟关系):u 的右孩子是 v 的父,v 的父的左孩子是 v,对应原树中 u 和 v 的父是兄弟,v 是 v 父的孩子,因此 u 和 v 是叔侄(兄弟关系的延伸,成立)。
- 情况 3 不成立:若 u 的父与 v 的父是兄弟,二叉树中 u 的父的右孩子是 v 的父,无法满足 u 是 v 父的父的条件。
答案:B
【2011】树转二叉树的无右孩子结点数
题目:已知一棵有 2011 个结点的树,其叶结点个数为 116,该树对应的二叉树中无右孩子的结点个数是______。
A. 115 B. 116 C. 1895 D. 1896
解析:方法1:特殊法

方法2:
核心公式:树中无右孩子的结点数 = 总结点数 - 非叶结点数 + 1
- 总结点数
n = 2011,叶结点数n0 = 116,非叶结点数n1 = 2011 - 116 = 1895 - 无右孩子结点数 =
2011 - 1895 + 1 = 1896 - 原理:树中每个非叶结点的最后一个孩子、根结点,在转换为二叉树后均无右孩子,总数为
(n - n0) + 1。
答案:D
【2014】森林转二叉树的叶结点对应关系
题目:将森林 F 转换为对应的二叉树 T,F 中叶结点的个数等于______。
A. T 中叶结点的个数
B. T 中度为 1 的结点个数
C. T 中左孩子指针为空的结点个数
D. T 中右孩子指针为空的结点个数
解析:

根据转换规则:
- 森林中的叶结点:没有孩子,因此在二叉树中左孩子指针为空(右孩子可能为兄弟,不为空)。
- 二叉树中左孩子为空的结点,对应原森林中无孩子的结点(即叶结点)。
答案:C
【2016】森林的树的个数计算
题目:若森林 F 有 15 条边、25 个结点,则 F 包含树的个数是______。
A. 8 B. 9 C. 10 D. 11
解析:

核心公式:n 个结点的树有 n-1 条边,因此森林中树的个数 = 总结点数 - 总边数
- 树的个数 =
25 - 15 = 10
答案:C
【2019】树转二叉树的遍历对应
题目:若将一棵树 T 转化为对应的二叉树 BT,则下列对 BT 的遍历中,其遍历序列与 T 的后根遍历序列相同的是______。
A. 先序遍历 B. 中序遍历 C. 后序遍历 D. 按层遍历
解析:直接对应核心遍历关系表:树的后根遍历 = 对应二叉树的中序遍历。
答案:B
【2021】由二叉树遍历还原森林的树数
题目:某森林 F 对应的二叉树为 T,若 T 的先序遍历序列是 a,b,d,c,e,g,f,中序遍历序列是 b,d,a,e,g,c,f,则 F 中树的棵数是______。
A. 1 B. 2 C. 3 D. 4
解析:

步骤 1:由二叉树的前序 + 中序遍历还原二叉树:
- 前序:
a(根) → b,d → c,e,g,f - 中序:
b,d → a → e,g,c,f→ 根 a 的左子树为b,d,右子树为c,e,g,f - 右子树前序:
c(根) → e,g → f,中序:e,g → c → f→ 根 c 的左子树为e,g,右子树为f - 左子树
e,g:前序e(根)→g,中序e→g→ e 的右孩子为 g
步骤 2:将二叉树转换为森林:
- 二叉树的根
a是第一棵树的根; a的右孩子c是第二棵树的根;c的右孩子f是第三棵树的根;- 因此森林共 3 棵树。
答案:C
2.2 正则 k 叉树专项(2016 真题)
题目:如果一棵非空 k(k≥2)叉树 T 中每个非叶结点都有 k 个孩子,则称 T 为正则 k 叉树。请回答下列问题,并给出推导过程。
(1)若 T 有 m 个非叶结点,则 T 中的叶结点有多少个?
(2)若 T 的高度为 h(单结点的树 h=1),则 T 的结点数最多为多少个?最少为多少个?
(1)叶结点数推导

- 总边数 = 总结点数 - 1(树的基本性质)
- 非叶结点数为
m,每个非叶结点有k个孩子,因此总边数 =k * m - 总结点数
n = 叶结点数n0 + 非叶结点数m - 代入边数公式:
k * m = (n0 + m) - 1 - 解得:
n0 = k*m - m + 1 = m*(k-1) + 1
(2)结点数的最值推导

- 最多结点数 :满 k 叉树,每一层都填满结点第 1 层:1 个,第 2 层:k 个,第 3 层:k² 个,...,第 h 层:k^(h-1) 个总结点数 =
1 + k + k² + ... + k^(h-1) = (k^h - 1) / (k - 1)

- 最少结点数 :每一层仅一个非叶结点,其余为叶结点第 1 层:1 个,第 2~h 层:每层 k 个结点总结点数 =
1 + k*(h-1) = k*h - k + 1
三、核心算法实现:层序遍历、树深度、带权路径长度 WPL
3.1 二叉树层序遍历求深度(完整可运行代码)
层序遍历(广度优先遍历)是求二叉树深度的经典方法,核心思路是按层遍历,每遍历完一层深度 + 1。

cpp
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
// 二叉树结点定义
typedef char TreeType;
typedef struct TreeNode
{
TreeType data;
struct TreeNode *lchild;
struct TreeNode *rchild;
}TreeNode;
typedef TreeNode* BiTree;
// 队列定义(用于层序遍历)
typedef TreeNode* ElemType;
typedef struct
{
ElemType *data;
int front;
int rear;
}Queue;
// 全局变量:先序遍历创建二叉树的字符串(#表示空结点)
char str[] = "ABDH#K###E##CFI###G#J##";
int idx = 0;
// 1. 先序遍历创建二叉树
void createTree(BiTree *T)
{
TreeType ch = str[idx++];
if (ch == '#') // 空结点
{
*T = NULL;
}
else
{
// 分配结点空间
*T = (BiTree)malloc(sizeof(TreeNode));
(*T)->data = ch;
// 递归创建左、右子树
createTree(&(*T)->lchild);
createTree(&(*T)->rchild);
}
}
// 2. 初始化队列
Queue* initQueue()
{
Queue *q = (Queue*)malloc(sizeof(Queue));
q->data = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE);
q->front = 0;
q->rear = 0;
return q;
}
// 3. 判断队列是否为空
int isEmpty(Queue *Q)
{
return Q->front == Q->rear;
}
// 4. 入队
int equeue(Queue *Q, ElemType e)
{
// 队列满判断
if ((Q->rear + 1) % MAXSIZE == Q->front)
{
printf("队列已满,入队失败\n");
return 0;
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXSIZE;
return 1;
}
// 5. 出队
int dequeue(Queue *Q, ElemType *e)
{
if (isEmpty(Q))
{
printf("队列为空,出队失败\n");
return 0;
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return 1;
}
// 6. 获取队列当前元素个数
int queueSize(Queue *Q)
{
if (isEmpty(Q))
return 0;
return (Q->rear - Q->front + MAXSIZE) % MAXSIZE;
}
// 7. 层序遍历求二叉树深度
int maxDepth(TreeNode* root)
{
if (root == NULL) return 0; // 空树深度为0
int depth = 0;
Queue *q = initQueue();
equeue(q, root); // 根结点入队
while(!isEmpty(q))
{
int count = queueSize(q); // 当前层的结点数
while(count > 0)
{
TreeNode* curr;
dequeue(q, &curr); // 出队当前结点
// 左孩子入队
if (curr->lchild != NULL)
equeue(q, curr->lchild);
// 右孩子入队
if (curr->rchild != NULL)
equeue(q, curr->rchild);
count--;
}
depth++; // 一层遍历完成,深度+1
}
return depth;
}
int main(int argc, char const *argv[])
{
BiTree T;
createTree(&T); // 创建二叉树
printf("二叉树深度为:%d\n", maxDepth(T)); // 输出深度
return 0;
}
代码说明:
- 采用先序遍历 +# 占位的方式创建二叉树,符合数据结构教材的标准实现。
- 队列采用循环队列实现,避免假溢出,时间复杂度 O (n),空间复杂度 O (n)(最坏情况为完全二叉树,队列存储 n/2 个结点)。
- 运行结果:示例二叉树深度为
5。
3.2 二叉树带权路径长度 WPL(2014 真题代码实现)

带权路径长度(WPL)是哈夫曼树的核心概念,定义为:所有叶结点的权值 × 该结点到根的路径长度 之和。
cpp
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
typedef int ElemType;
// 二叉树结点定义(带权值)
typedef struct TreeNode
{
ElemType weight;
struct TreeNode *left;
struct TreeNode *right;
}TreeNode;
typedef TreeNode* BiTree;
// 全局变量:先序遍历创建二叉树的权值数组(-1表示空结点)
int idx = 0;
int weight[] = {100, 42, 15, -1, -1, 27, -1, -1, 58, 28, 13, 5, -1, -1, 8, -1, -1, 15, -1, -1, 30, -1, -1};
// 1. 先序遍历创建带权二叉树
void createTree(BiTree *T)
{
ElemType ch = weight[idx++];
if (ch == -1) // 空结点
{
*T = NULL;
}
else
{
*T = (BiTree)malloc(sizeof(TreeNode));
(*T)->weight = ch;
createTree(&(*T)->left);
createTree(&(*T)->right);
}
}
// 2. 层序遍历计算WPL
int wpl(BiTree T)
{
if (T == NULL) return 0;
// 数组模拟队列(简化实现)
BiTree queue[MAXSIZE];
int front = 0;
int rear = 0;
int wpl = 0;
int depth = 0; // 当前层的深度(根结点深度为0)
queue[rear++] = T; // 根结点入队
while(rear != front)
{
int count = rear - front; // 当前层结点数
while(count > 0)
{
BiTree curr = queue[front++]; // 出队
// 叶结点:累加权值×深度
if (curr->left == NULL && curr->right == NULL)
{
wpl += depth * curr->weight;
}
// 非叶结点:孩子入队
if (curr->left != NULL)
queue[rear++] = curr->left;
if (curr->right != NULL)
queue[rear++] = curr->right;
count--;
}
depth += 1; // 深度+1
}
return wpl;
}
int main(int argc, char const *argv[])
{
BiTree T;
createTree(&T);
int w = wpl(T);
printf("二叉树带权路径长度WPL为:%d\n", w);
return 0;
}
代码说明:
- 采用层序遍历实现,时间复杂度 O (n),空间复杂度 O (n),符合考研算法题的评分标准。
- 示例二叉树的 WPL 计算:叶结点 D (15, 深度 3)、A (27, 深度 3)、F (5, 深度 5)、B (8, 深度 5)、C (15, 深度 4)、E (30, 深度 3)
- WPL =
15×3 + 27×3 + 5×5 + 8×5 + 15×4 + 30×3 = 325 - 代码运行结果:
325,验证正确性。
四、核心考点总结与避坑指南
4.1 必背核心公式
- 树的基本性质:n 个结点的树有 n-1 条边;森林的树数 = 总结点数 - 总边数。
- 正则 k 叉树 :叶结点数
n0 = m*(k-1)+1;满 k 叉树结点数(k^h -1)/(k-1)。 - 树转二叉树:无右孩子结点数 = 总结点数 - 非叶结点数 + 1。
- 遍历对应关系:树先根 = 二叉树前序,树后根 = 二叉树中序。
4.2 常见易错点
- ❌ 混淆树、森林、二叉树的遍历对应关系,错误认为树的后根 = 二叉树的后序。
- ❌ 正则 k 叉树的结点数计算错误,忽略 "边数 = 总结点数 - 1" 的核心公式。
- ❌ 层序遍历求深度时,忘记按层计数,直接累加结点数导致深度错误。
- ❌ 森林转二叉树时,错误认为二叉树的右孩子对应原树的孩子,实际对应兄弟。