
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划、程序人生
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- 二叉树实战进阶全攻略:从层序遍历到OJ题深度解析✨
-
- 文章摘要
- 一、知识回顾:二叉树的核心解题思想
- 二、实战一:二叉树节点计数(递归分治的经典应用)🔢
-
- [1. 求二叉树的节点总数](#1. 求二叉树的节点总数)
- [2. 求二叉树的叶子节点数](#2. 求二叉树的叶子节点数)
- 二、实战二:二叉树层序遍历(队列实现,广度优先遍历)📊
-
- [1. 层序遍历的核心逻辑](#1. 层序遍历的核心逻辑)
- [2. 层序遍历的代码实现(结合之前实现的队列结构)](#2. 层序遍历的代码实现(结合之前实现的队列结构))
- [3. 层序遍历的经典练习](#3. 层序遍历的经典练习)
- 三、实战三:二叉树经典OJ题深度解析(LeetCode高频考点)🔥
-
- [1. OJ题1:二叉树的前序遍历(LeetCode 144)](#1. OJ题1:二叉树的前序遍历(LeetCode 144))
- [2. OJ题2:二叉树的最大深度(LeetCode 104)](#2. OJ题2:二叉树的最大深度(LeetCode 104))
- [3. OJ题3:平衡二叉树(LeetCode 110)](#3. OJ题3:平衡二叉树(LeetCode 110))
- 四、实战四:二叉树的销毁与构建(工程必备技能)🛠️
-
- [1. 二叉树的销毁(后序遍历,必须掌握)](#1. 二叉树的销毁(后序遍历,必须掌握))
- [2. 基于先序字符串构建二叉树(面试高频题)](#2. 基于先序字符串构建二叉树(面试高频题))
- 五、拓展提升:哈夫曼树与哈夫曼编码(贪心算法的应用)📡
-
- [1. 哈夫曼树的构建思想](#1. 哈夫曼树的构建思想)
- [2. 哈夫曼编码(前缀编码,无损压缩)](#2. 哈夫曼编码(前缀编码,无损压缩))
- [3. 带权路径长度计算](#3. 带权路径长度计算)
- 六、写在最后
二叉树实战进阶全攻略:从层序遍历到OJ题深度解析✨
你好!欢迎来到数据结构系列二叉树篇的第二篇实战进阶内容~
在上一篇中,我们夯实了树与二叉树的基础概念,掌握了前序、中序、后序三种深度优先遍历的核心逻辑。今天,我们将聚焦二叉树的实战核心技能,从节点计数、层序遍历等基础操作,到经典OJ题解、二叉树的销毁与构建,再到哈夫曼树的拓展知识,全方位提升你对二叉树的灵活运用能力。这些内容既是笔试高频考点,也是工程开发中处理树结构的必备技能🌳
准备好了吗?让我们一起将理论转化为实战能力,攻克二叉树的进阶关卡!🚀
文章摘要
本文为数据结构系列二叉树篇第二篇实战进阶内容,聚焦二叉树核心实战技能。详细讲解二叉树节点总数、叶子节点数的递归分治求解方法,深入剖析层序遍历的队列实现逻辑与核心思想。结合前序遍历数组存储、最大深度计算、平衡二叉树判断等经典OJ题,拆解解题思路并提供高效代码实现。补充二叉树的后序销毁方法、基于先序字符串的构建逻辑,以及哈夫曼树的贪心构建思想与编码规则,全方位提升二叉树实战能力。
阅读时长 :约30分钟
阅读建议:
- 初学者:先掌握层序遍历的队列实现,再练习节点计数的递归写法
- 刷题备考者:重点记忆平衡二叉树、最大深度的解题模板,理解递归分治思想
- 面试冲刺者:熟练掌握基于先序字符串构建二叉树的逻辑,能独立手写代码
- 查漏补缺者:聚焦哈夫曼树的构建思想与编码规则,理解贪心算法的应用
一、知识回顾:二叉树的核心解题思想
在进入实战前,我们先回顾二叉树的两大核心解题思想,这是解决所有二叉树问题的关键:
- 递归分治思想:将整棵树的问题,拆分为根节点、左子树、右子树的子问题,递归求解子问题后合并结果(适用于深度优先遍历、节点计数等)。
- 辅助结构思想:利用栈、队列等线性结构,实现二叉树的非递归遍历或广度优先遍历(适用于层序遍历、非递归前序遍历等)。
核心原则:深度优先问题用递归分治,广度优先问题用队列辅助!
二、实战一:二叉树节点计数(递归分治的经典应用)🔢
节点计数是二叉树最基础的实战操作,重点考察对递归分治思想的理解。
1. 求二叉树的节点总数
方法1:指针传参统计(不推荐)
通过传入整型指针修改外部变量,统计节点总数。缺点是依赖外部变量,违背高内聚的代码设计思想。
c
void TreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return;
(*psize)++; // 节点非空,计数+1
TreeSize(root->left, psize); // 递归遍历左子树
TreeSize(root->right, psize); // 递归遍历右子树
}
方法2:分治递归(推荐,面试首选)
核心思想:整棵树的节点数 = 根节点(1个) + 左子树节点数 + 右子树节点数。空树的节点数为0,递归求解左右子树的节点数即可。
c
int TreeSize(BTNode* root)
{
// 三目运算符简化代码:空树返回0,否则递归计算左右子树节点数之和+1
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
2. 求二叉树的叶子节点数
叶子节点定义 :度为0的节点,即left和right指针均为NULL的节点。
核心思想:整棵树的叶子节点数 = 左子树叶子节点数 + 右子树叶子节点数。若当前节点是叶子节点,直接返回1。
c
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0; // 空树无叶子节点
// 当前节点是叶子节点,返回1
if (root->left == NULL && root->right == NULL)
return 1;
// 递归计算左右子树叶子节点数之和
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
💡 核心技巧 :二叉树的递归问题,本质是将大问题拆分为左右子树的小问题,明确递归终止条件和子问题合并方式,就能快速写出代码!
二、实战二:二叉树层序遍历(队列实现,广度优先遍历)📊
层序遍历是二叉树的广度优先遍历(BFS),核心思想是一层一层遍历,上一层节点带下一层节点,利用队列的先进先出(FIFO)特性实现。这是二叉树实战中的高频考点,必须熟练掌握。
1. 层序遍历的核心逻辑
- 初始化队列,若根节点非空,则将根节点入队。
- 循环判断队列是否为空:
- 取出队头节点,访问该节点(打印、存储等操作)。
- 若队头节点有左孩子,将左孩子入队;若有右孩子,将右孩子入队。
- 队头节点出队。
- 队列空时,遍历结束,销毁队列避免内存泄漏。
2. 层序遍历的代码实现(结合之前实现的队列结构)
c
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q); // 初始化队列
// 根节点非空则入队,作为遍历的起点
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q); // 取出队头节点(当前层的节点)
QueuePop(&q); // 队头节点出队
printf("%c ", front->data); // 访问当前节点
// 左孩子非空则入队,为下一层遍历做准备
if (front->left)
QueuePush(&q, front->left);
// 右孩子非空则入队,注意左孩子先入队,保证层序遍历的顺序
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q); // 销毁队列,避免内存泄漏
}
3. 层序遍历的经典练习
题目 :某完全二叉树按层序输出的序列为ABCDEFGH,求该树的前序序列。
解题思路 :根据层序序列构建完全二叉树,完全二叉树的节点编号(根为1)满足:左孩子编号为2*i,右孩子编号为2*i+1。构建完成后,进行前序遍历。
答案 :前序序列为ABDHECFG
三、实战三:二叉树经典OJ题深度解析(LeetCode高频考点)🔥
接下来,我们结合三道LeetCode经典题目,讲解二叉树的实战解题思路。这些题目覆盖了前序遍历的数组存储、最大深度计算、平衡二叉树判断,是笔试和面试的高频考点。
1. OJ题1:二叉树的前序遍历(LeetCode 144)
题目要求 :给定一个二叉树的根节点root,返回它的前序遍历节点值数组。数组需手动malloc分配空间,调用者负责free。
解题思路:
- 先递归计算二叉树的节点总数,确定数组的大小。
- 再递归进行前序遍历,将节点值存入数组中(使用指针传递数组下标,避免全局变量)。
- 返回数组,并通过
returnSize参数返回数组大小。
代码实现:
c
// 辅助函数:递归计算二叉树的节点总数
int TreeSize(struct TreeNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
// 辅助函数:前序遍历,将节点值存入数组
void preOrder(struct TreeNode* root, int* a, int* pi)
{
if (root == NULL)
return;
a[*pi] = root->val; // 根节点值存入数组
(*pi)++; // 数组下标后移,注意括号不能省略
preOrder(root->left, a, pi); // 递归遍历左子树
preOrder(root->right, a, pi); // 递归遍历右子树
}
// 主函数:前序遍历并返回数组
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
*returnSize = TreeSize(root); // 获取数组大小
int* a = (int*)malloc(*returnSize * sizeof(int)); // 分配数组空间
int i = 0;
preOrder(root, a, &i); // 前序遍历存入数组
return a; // 返回数组
}
2. OJ题2:二叉树的最大深度(LeetCode 104)
题目要求:给定一个二叉树,求它的最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点数。
解题思路(递归分治) :整棵树的最大深度 = max(左子树最大深度, 右子树最大深度) + 1。空树的最大深度为0,递归求解左右子树的最大深度即可。
代码实现:
c
int maxDepth(struct TreeNode* root)
{
if (root == NULL)
return 0; // 空树深度为0
// 递归计算左右子树的深度
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
// 返回左右子树深度的较大值 + 1(当前节点)
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
3. OJ题3:平衡二叉树(LeetCode 110)
题目要求:给定一个二叉树,判断它是否是高度平衡的二叉树。高度平衡二叉树的定义:每个节点的左右两个子树的高度差的绝对值不超过1。
解题思路(递归分治):
- 空树是平衡二叉树。
- 非空树的平衡条件:
- 当前节点的左右子树高度差的绝对值 ≤ 1。
- 左子树是平衡二叉树。
- 右子树是平衡二叉树。
- 利用之前实现的
maxDepth函数计算子树高度。
代码实现:
c
// 辅助函数:求二叉树的最大深度
int maxDepth(struct TreeNode* root)
{
if (root == NULL)
return 0;
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
// 主函数:判断是否是平衡二叉树
bool isBalanced(struct TreeNode* root)
{
if (root == NULL)
return true; // 空树是平衡二叉树
// 计算当前节点左右子树的高度
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
// 平衡条件:当前节点平衡,且左右子树都平衡
return abs(leftDepth - rightDepth) < 2
&& isBalanced(root->left)
&& isBalanced(root->right);
}
四、实战四:二叉树的销毁与构建(工程必备技能)🛠️
1. 二叉树的销毁(后序遍历,必须掌握)
核心思想 :二叉树的销毁必须采用后序遍历 的顺序,先销毁左子树和右子树,再销毁根节点。若先销毁根节点,会导致无法访问左右子树,造成内存泄漏。
注意事项 :需使用二级指针 ,因为要修改根节点的指向,将其置为NULL,避免野指针。
代码实现:
c
void DestroyTree(struct TreeNode** root)
{
// 边界条件:指针为空或根节点为空
if (root == NULL || *root == NULL)
return;
// 递归销毁左子树
DestroyTree(&(*root)->left);
// 递归销毁右子树
DestroyTree(&(*root)->right);
// 销毁根节点
free(*root);
*root = NULL; // 置空,避免野指针
}
2. 基于先序字符串构建二叉树(面试高频题)
需求 :读入用户输入的先序遍历字符串(#代表空节点),构建一棵二叉树,然后进行中序遍历并输出结果。
核心思想 :递归构建二叉树。遇到#则返回空节点,否则构建根节点,再递归构建左子树和右子树(符合先序遍历的顺序:根→左→右)。
代码实现:
c
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char val;
} TNode;
// 根据先序字符串创建二叉树,pi是数组下标指针,用于记录当前遍历的位置
TNode* CreateTree(char* a, int* pi)
{
if (a[*pi] == '#') // #代表空节点
{
(*pi)++;
return NULL;
}
// 构建当前根节点
TNode* root = (TNode*)malloc(sizeof(TNode));
if (root == NULL)
{
perror("malloc fail");
exit(EXIT_FAILURE);
}
root->val = a[*pi];
(*pi)++;
// 递归创建左子树
root->left = CreateTree(a, pi);
// 递归创建右子树
root->right = CreateTree(a, pi);
return root;
}
// 中序遍历
void InOrder(TNode* root)
{
if (root == NULL)
return;
InOrder(root->left);
printf("%c ", root->val);
InOrder(root->right);
}
int main()
{
char str[100];
scanf("%s", str);
int i = 0;
TNode* root = CreateTree(str, &i);
InOrder(root);
// 注意:实际工程中需要销毁二叉树,避免内存泄漏
DestroyTree(&root);
return 0;
}
五、拓展提升:哈夫曼树与哈夫曼编码(贪心算法的应用)📡
哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。它在数据压缩、通信编码等领域有广泛的应用,核心思想是贪心算法。
1. 哈夫曼树的构建思想
给定n个带权值的叶子节点,构建哈夫曼树的步骤如下:
- 将所有节点按权值从小到大排序。
- 取出权值最小的两个节点,构建一个新的父节点,父节点的权值为两个子节点权值之和。
- 将新父节点重新加入节点序列中,保持序列的有序性。
- 重复步骤2-3,直到序列中只剩下一个节点,该节点即为哈夫曼树的根节点。
2. 哈夫曼编码(前缀编码,无损压缩)
哈夫曼编码是基于哈夫曼树的一种编码方式,核心规则:
- 左分支标记为
0,右分支标记为1。 - 每个叶子节点的编码为从根节点到该节点的路径上的标记序列。
- 权值越大的节点,编码越短(带权路径长度越短),从而实现数据压缩。
示例:给定权值为7(a)、5(b)、2©、4(d)的叶子节点,构建的哈夫曼编码如下:
- a(权值7):编码为
0(路径最短) - b(权值5):编码为
10 - c(权值2):编码为
110 - d(权值4):编码为
111
3. 带权路径长度计算
带权路径长度(WPL)是指叶子节点的权值乘以其到根节点的路径长度(边数)。哈夫曼树的总带权路径长度是所有叶子节点的带权路径长度之和,且为最小值。
示例计算 :上述哈夫曼树的总带权路径长度 = 7×1 + 5×2 + 2×3 + 4×3 = 7 + 10 + 6 + 12 = 35。
六、写在最后
恭喜你!二叉树篇的实战进阶内容至此圆满结束🎉~
从二叉树的节点计数、层序遍历,到经典OJ题解、二叉树的销毁与构建,再到哈夫曼树的拓展知识,你已经掌握了二叉树的核心实战技能,完成了从理论到应用的跨越:
- 熟练掌握了二叉树的递归分治思想,能解决节点计数、最大深度等问题。
- 掌握了层序遍历的队列实现,理解广度优先遍历的核心逻辑。
- 能独立解决二叉树的经典OJ题,具备一定的算法解题能力。
- 了解了二叉树的销毁与构建方法,掌握工程开发中的必备技能。
- 初步认识了哈夫曼树与哈夫曼编码,理解贪心算法的应用。
二叉树是数据结构的"分水岭",也是算法面试的"高频考点"。建议你多敲几遍代码,多做几道OJ题,真正理解递归分治的核心思想------这不仅是解决二叉树问题的关键,更是解决所有复杂算法问题的基础。
下一个系列,我们将进入搜索二叉树与平衡树的世界,学习更高级的树结构,提升数据结构的应用能力~😜
点赞+收藏+关注,跟着系列内容一步步吃透数据结构!你的支持是我创作的最大动力~👍