
◆ 博主名称: 小此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️个人专栏:《C语言》_小此方的博客-CSDN博客
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰。
前面我们了解到:满二叉树和完全二叉树由于其物理连续性可以使用数组来存储,然而,二叉树不只有这些特殊情况。对于一般的二叉树,我们只能使用链式结构进行存储。这样的二叉树:我们称之为——链式二叉树。

链式二叉树有三叉链和二叉链两种常见形式,但是二叉链式更加常见——以下我们的讨论主要集中在二叉链。
学习二叉树结构,最简单的一步就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的结点进行相应的操作,并且每个结点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%c", root->_data);
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreePrevOrder(root->_left);
printf("%c", root->_data);
BinaryTreePrevOrder(root->_right);
}// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
printf("%c", root->_data);
}很多人看到这几行代码后一脸懵。递归确实不好理解。
下面主要用递归展开图分析前序递归遍历,帮助你更好理解递归遍历。(中序与后序图解类似)

如图详细解释了前序遍历递归调用的过程。
1,整个过程从根节点开始,遵循“根 → 左 → 右”的顺序访问每个节点。
2,图中左侧为二叉树结构,右侧则展示了相应的递归函数调用栈。
3,红色箭头指示了进入子函数调用的方向,而绿色箭头则表示函数执行完毕后的返回路径。
4,最终,按照前序遍历规则,节点被依次访问并打印出:1, 2, 3, 4, 5, 6。
很多人在学习这三大遍历的时候都会犯的错误:只记住遍历结果的“数值序列”,却忽略了递归过程中对 NULL节点的处理。
前序遍历结果:1 2 3 4 5 6 中序遍历结果:3 2 1 5 4 6 后序遍历结果:3 2 5 6 4 1
表面上是正确的,但是可能没有真正理解前序中序后序遍历

但如果深入理解递归过程,我们会发现:每次访问一个节点时,程序都会尝试进入其左右子树。
如图所示:当递归执行到节点 3 时,函数仍会继续递归调用其左子树(空指针),然后立刻返回;同理,右子树也如此。
因此,在完整追踪所有调用路径的前提下,真正的遍历轨迹应包含 NULL 的访问行为。即使子树为空,也会触发一次“递归调用”并立即返回。因此我们得到这三者遍历的实际结果应该是:
前序遍历结果:1,2,3,N,N,N,4,5,N,N,6,N,N 中序遍历结果:N,3,N,2,N,1,N,5,N,4,N,6,N 后序遍历结果:N,N,3,N,2,N,N5,N,N,6,4,1

如图所示,在前序序列 1,2,3,N,N,N,4,5,N,N,6,N,N 中,第一个数字 1 即为根节点。后续的子序列可以按此原则递归划分。

在中序序列 N,3,N,2,N,1,N,5,N,4,N,6,N 中,根节点 1 出现的位置将序列分为两部分:
N,3,N,2,N → 对应左子树N,5,N,4,N,6,N → 对应右子树结合前序/后序中根的信息,即可递归地构建出完整的树形结构。
BTNode* BinaryTreeCreate(BTDataType* a,int* pi)
{
if (*(a + *pi) == '#')
{
(*pi)++;
return NULL;
}
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->_data = *(a + *pi);
(*pi)++;
root->_left=BinaryTreeCreate(a, pi);
root->_right=BinaryTreeCreate(a, pi);
return root;
}步骤 | 当前字符 | 操作 |
|---|---|---|
1 | 'A' | 创建根节点 A,继续处理左子树 |
2 | 'B' | 创建 B,其左子树为 # → 返回 NULL;右子树为 D |
3 | 'D' | 创建 D,左右均为 # → 成为叶子节点 |
4 | 'E' | 创建 E,左子树为 #,右子树为 H |
5 | 'H' | 创建 H,左右均为 # → 叶子节点 |
6 | 'C' | 创建 C,左子树为 F,右子树为 G |
7 | 'F' | 创建 F,左右均为 # → 叶子节点 |
8 | 'G' | 创建 G,左右均为 # → 叶子节点 |
其核心在于:
“先创建当前节点,再递归构建其左右子树。”
这是一种典型的自顶向下递归构造策略,广泛应用于树的序列化与反序列化场景。
层序遍历是一种迭代遍历而不是递归遍历。

1,设二叉树的根结点所在层数为1。
2,从所在二叉树的根结点出发,首先访问第一层的树根结点。
3,然后从左到右访问第2层 上的结点,接着是第三层的结点。
4,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%c ", front->_data);
QueuePop(&q);
if (front->_left) {
QueuePush(&q, front->_left);
}
if (front->_right) {
QueuePush(&q, front->_right);
}
}
QueueDestroy(&q);
}与递归遍历不同,层序遍历使用队列做为中介缓冲,利用父亲结点带动子节点入队的特性实现。


1 入队。1 出队后,其左右子节点 2 和 4 入队。2 出队后,其左子节点 3 入队;4 出队后,其左右子节点 5 和 6 入队。<操作流程> 初始状态:队列为空 步骤1:将根节点 1 入队 → 队列: [1] ↓ 步骤2:取出 1,打印并将其子节点 2, 4 入队 → 队列: [2, 4] ↓ 步骤3:取出 2,打印并将其子节点 3 入队 → 队列: [4, 3] ↓ 步骤4:取出 4,打印并将其子节点 5, 6 入队 → 队列: [3, 5, 6] ↓ 步骤5:取出 3,无子节点 → 队列: [5, 6] ↓ ...
int binarytreesize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return binarytreesize(root->_left) + binarytreesize(root->_right) + 1;
}代码简洁明了,但其背后蕴含着深刻的递归思维——“分而治之”与“层层上报”。
首先我来举一个常见的例子:

省部级领导需要了解本省的干部总人数,于是向下派发任务:
1 + 1 + 1 = 3(两个下属 + 自己)。3 + 3 + 1 = 7。7 + 7 + 1 = 15。最终结果:整个组织共有 15 名干部。
组织角色 | 代码对应 | 功能 |
|---|---|---|
科长(叶子节点) | root == NULL | 返回 0;无子节点,不参与计数 |
处长、厅长、部长 | 递归调用 | 向下分发任务,向上汇总结果 |
递归本质:
趁热打铁,我们来看一下叶子节点个数的计算方式
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if ( root->_left == NULL&&root->_right==NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}与“总结点个数”的递归思路类似,但有一个关键区别:
省部级领导现在只想知道有多少位“科长”——也就是最基层的干部(叶子节点)。
1。1 和 1。1 + 1 = 2。2,所以总和为 2 + 2 = 4。4,最终结果为 4 + 4 = 8。最终结果:共有 8 位科长,即 8 个叶子节点。
相比前两个问题,本题的递归逻辑更加复杂,需要引入一个额外参数 k 来追踪当前所在的层级。我们通过“层层递减”的方式,精准定位目标层的节点数量。
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (k == 1)
{
return 1;
}
else
{
k--;
}
return BinaryTreeLevelKSize(root->_left, k)+BinaryTreeLevelKSize(root->_right, k);
}k 表示当前目标层数。k 就减 1,直到 k == 1 时,表示当前节点正好位于目标层。1,表示该节点是目标层的一个有效节点。注意:
k < 1,说明已经超出树的深度,应返回 0。k >= 1。省部级领导现在想知道有多少位“处长”——也就是权力体系中的第三层干部(k = 3)
部长(第1层)
k = 3k--,变为 k = 2厅长(第2层)
k = 2 层的节点k--,变为 k = 1处长(第3层)
k = 1 层的节点k == 1,说明到达目标层,节点返回 1步骤 | 说明 |
|---|---|
if (k == 1) | 判断是否到达目标层,若是,则返回 1 |
k-- | 向下一层递归时,层数减 1 |
return left + right | 汇总左右子树中第 k 层的节点总数 |
关键技巧:
k 是“倒计时器”:每深入一层,就减 1。k == 1 时,当前节点才被计入结果。这个函数展示了递归中状态传递的强大能力:
“我不关心自己是不是目标层,我只负责把‘还有几层’的信息传下去,直到有人找到答案。”
在实际应用中,我们常常需要在二叉树中查找某个特定值的节点。
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->_data == x)
{
return root;
}
BTNode* ret1 = BinaryTreeFind(root->_left, x);
if (ret1)
{
return ret1;
}
return BinaryTreeFind(root->_right, x);
}步骤 | 说明 |
|---|---|
if (root == NULL) | 边界条件:空树无节点可查,返回 NULL |
if (root->_data == x) | 当前节点匹配目标值 → 返回当前节点指针 |
ret1 = BinaryTreeFind(root->_left, x) | 递归搜索左子树 |
if (ret1 != NULL) | 若左子树找到,则直接返回结果(短路) |
return BinaryTreeFind(root->_right, x) | 左边没找到,再去右边找 |
这应该是二叉树中最难的部分

同样采用对列作为缓冲器。如果当我们出队列出到空时:对于完全二叉树而言,空结点后面应该全部都是空结点。如果有非空结点则为非完全二叉树。
● 前n-1层全部都h是满结点。
● 第n层可能不是满结点,并且结点从左向右排列。
NULL 节点时,后续所有出队的节点都必须是 NULL。NULL 之后又出现了非空节点,则说明存在“空缺”,不是完全二叉树。NULL)。NULL 时,表示某一层出现了空位。bool BinaryTreeComplete(BTNode* root)
{
Queue p;
QueueInit(&p);
if (root)
{
QueuePush(&p, root);
}
while(!QueueEmpty(&p))
{
BTNode*ret=QueueFront(&p);
QueuePop(&p);
if (ret == NULL)
{
break;
}
}
while (!QueueEmpty)
{
BTNode* ret = QueueFront(&p);
QueuePop(&p);
if (ret == NULL)
{
QueueDestroy(&p);
return false;
}
}
QueueDestroy(&p);
return true;
}1. 初始化队列,将根节点入队 2. 循环取出节点,直到遇到第一个 NULL 3. 继续循环,检查剩余节点是否全为 NULL 4. 若有非空 → 返回 false;否则返回 true
二叉树的销毁是数据结构中一个经典且基础的操作,几乎在每一次算法面试中都会被考察。
void BinaryTreeDestory(BTNode* root)
{
if ((root)==NULL)
{
return;
}
BinaryTreeDestory((root)->_left);
BinaryTreeDestory((root)->_right);
free(root);
}错误做法:先释放根节点,再释放子树 —— 此时子树指针已失效,会导致段错误(Segmentation Fault)。
正确做法:必须先销毁左右子树,再销毁根节点。
这正是后序遍历(Left → Right → Root)的天然应用场景。
A,那么 B 和 C 的父指针就变成了野指针。B 或 C 时,程序会崩溃。语句 | 功能 |
|---|---|
if (root == NULL) return; | 边界条件:空树无需处理 |
BinaryTreeDestroy(root->_left); | 递归销毁左子树 |
BinaryTreeDestroy(root->_right); | 递归销毁右子树 |
free(root); | 释放当前节点内存 |
好了,本期文章就到这里,如果对你有帮助,还请一键三联支持,我是此方,我们下期再见
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=9cmybdoqp8r