【数据结构】「树」专题:树、森林与二叉树遍历之间的关系+408真题

一、树、森林与二叉树的核心关系

在数据结构中,树、森林、二叉树三者并非孤立存在,而是可以通过左孩子 - 右兄弟表示法相互转换,且遍历规则存在严格对应关系,这是理解整个树结构的核心基础。

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 必背核心公式

  1. 树的基本性质:n 个结点的树有 n-1 条边;森林的树数 = 总结点数 - 总边数。
  2. 正则 k 叉树 :叶结点数n0 = m*(k-1)+1;满 k 叉树结点数(k^h -1)/(k-1)
  3. 树转二叉树:无右孩子结点数 = 总结点数 - 非叶结点数 + 1。
  4. 遍历对应关系:树先根 = 二叉树前序,树后根 = 二叉树中序。

4.2 常见易错点

  • ❌ 混淆树、森林、二叉树的遍历对应关系,错误认为树的后根 = 二叉树的后序。
  • ❌ 正则 k 叉树的结点数计算错误,忽略 "边数 = 总结点数 - 1" 的核心公式。
  • ❌ 层序遍历求深度时,忘记按层计数,直接累加结点数导致深度错误。
  • ❌ 森林转二叉树时,错误认为二叉树的右孩子对应原树的孩子,实际对应兄弟。
相关推荐
Fcy6482 小时前
算法基础详解(4)双指针算法
开发语言·算法·双指针
zk_ken2 小时前
优化图像拼接算法思路
算法
xwz小王子2 小时前
Nature Communications从结构到功能:基于Kresling折纸的多模态微型机器人设计
人工智能·算法·机器人
luj_17682 小时前
从R语言想起的,。。。
服务器·c语言·开发语言·经验分享·算法
计算机安禾2 小时前
【数据结构与算法】第29篇:红黑树原理与C语言模拟
c语言·开发语言·数据结构·c++·算法·visual studio
生信研究猿2 小时前
94. 二叉树的中序遍历 (二叉树遍历整理)
数据结构·算法
挂科边缘2 小时前
image-restoration-sde复现,图像修复,使用均值回复随机微分方程进行图像修复,ICML 2023
算法·均值算法·ir-sde·扩散模块图像修复
2301_822703202 小时前
开源鸿蒙跨平台Flutter开发:血氧饱和度数据降噪:基于滑动窗口的滤波算法优化-利用动态列队 (Queue) 与时间窗口平滑光电容积脉搏波 (PPG)
算法·flutter·华为·开源·harmonyos
Vin0sen2 小时前
算法-线段树与树状数组
算法