【嵌入式 C 语言实战】二叉树入门:创建与前 / 中 / 后序递归遍历详解
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,除了单链表这类线性数据结构,树形结构也有着广泛的应用(比如配置项树、设备树、语法解析树等),而二叉树是树形结构中最基础、最核心的一种。今天就带大家从零开始,实现一棵简单的二叉树,并且掌握二叉树的三种核心遍历方式 —— 前序、中序、后序递归遍历,全程附可直接编译运行的 C 语言代码。
一、二叉树的核心概念(嵌入式开发视角)
1. 什么是二叉树?
二叉树是一种每个节点最多只有两个子节点的树形结构,这两个子节点分别被称为 “左子节点” 和 “右子节点”,对应的子树被称为 “左子树” 和 “右子树”。
本文实现的是一棵简单的完全二叉树(非平衡二叉树、非搜索二叉树),结构如下,这也是二叉树入门的经典示例:
plaintext
1 (根节点)
/ \
2 3 (第二层节点)
/ \
4 5 (第三层节点)
2. 二叉树的特点与嵌入式应用场景
-
特点:非线性结构,具有层级关系,每个节点的左、右子树也是一棵二叉树(递归特性);
-
嵌入式应用场景:
- 设备树解析:嵌入式系统中描述硬件设备的设备树,本质就是一种树形结构,二叉树是其解析的基础;
- 配置项管理:系统中的多级配置项(如系统→外设→参数),可通过二叉树存储和查询;
- 数据排序与查询:后续学习的二叉搜索树(BST),可实现高效的数据插入、查询和排序,适合资源受限的嵌入式设备。
3. 二叉树节点的结构体设计
二叉树的每个节点包含三个核心部分:
- 数据域:存储实际业务数据(如设备 ID、配置值等);
- 左指针域:指向左子节点的内存地址;
- 右指针域:指向右子节点的内存地址。
这是实现二叉树的基础,与单链表的节点结构体设计类似,只是多了一个子节点指针。
二、前期准备:头文件引入与节点结构体定义
首先引入必要的头文件,并定义二叉树的节点结构体,这是所有操作的前提:
c
运行
#include<stdio.h>
#include<stdlib.h>
// 二叉树节点结构体定义
typedef struct Node
{
int data; // 数据域:存储int类型数据(嵌入式可替换为设备结构体等)
struct Node *left; // 左指针域:指向左子节点
struct Node *right; // 右指针域:指向右子节点
}Node;
三、二叉树核心实现:节点创建与二叉树构建
1. 单个节点创建(create函数)
创建二叉树的第一步是创建单个节点,为节点分配堆内存,并初始化其数据域和指针域。
c
运行
// 创建单个二叉树节点,返回节点指针
Node *create(int data)
{
// 为节点分配堆内存(嵌入式需注意内存分配失败处理)
Node *node=(Node*)malloc(sizeof(Node));
if(node==NULL)
{
printf("malloc error:节点创建失败!\n");
exit(0); // 内存分配失败,程序退出(嵌入式可改为返回NULL并做容错处理)
}
// 初始化节点数据域
node->data=data;
// 初始化左右指针域为NULL,标识当前节点无左右子节点
node->left=node->right=NULL;
return node;
}
嵌入式注意点
- 嵌入式设备内存资源有限,
malloc动态分配内存可能失败,必须添加判空逻辑,避免野指针导致系统崩溃; - 若追求更高的稳定性,可采用静态内存分配(预先定义节点数组)替代
malloc,避免内存碎片化问题。
2. 构建示例二叉树(main函数中手动构建)
本文采用手动构建的方式创建示例二叉树(适合入门理解),通过给根节点和子节点的left、right指针赋值,串联起整个二叉树结构。
c
运行
int main()
{
// 步骤1:创建根节点(数据为1)
Node *root =create(1);
// 步骤2:为根节点创建左右子节点(数据分别为2、3)
root->left =create(2); // 根节点左子节点为2
root->right =create(3); // 根节点右子节点为3
// 步骤3:为节点2创建左右子节点(数据分别为4、5)
root->left->left =create(4); // 节点2的左子节点为4
root->left->right =create(5); // 节点2的右子节点为5
// 至此,示例二叉树构建完成,后续将进行遍历操作
// ... 遍历代码后续补充
return 0;
}
构建完成后的二叉树结构与前文一致,层级清晰,便于后续验证遍历结果。
四、二叉树的三种递归遍历(核心重点)
二叉树的遍历是指 “按照一定的顺序,访问二叉树中所有节点,且每个节点仅被访问一次”。由于二叉树具有递归特性(每个子树也是一棵二叉树),因此递归遍历是最简洁、最易理解的实现方式。
根据根节点被访问的顺序不同,二叉树的递归遍历分为三种:
- 前序遍历:根节点 → 左子树 → 右子树
- 中序遍历:左子树 → 根节点 → 右子树
- 后序遍历:左子树 → 右子树 → 根节点
1. 前序遍历(Preorder函数)
遍历规则
先访问当前根节点,再递归遍历左子树,最后递归遍历右子树(根→左→右)。
代码实现
c
运行
// 二叉树前序遍历(递归实现)
void Preorder(Node *root)
{
// 递归终止条件:当前节点为NULL(无节点可访问)
if(root!=NULL)
{
// 步骤1:访问当前根节点(打印节点数据)
printf("%d ",root->data);
// 步骤2:递归遍历左子树
Preorder(root->left);
// 步骤3:递归遍历右子树
Preorder(root->right);
}
}
遍历结果分析(针对示例二叉树)
按照 “根→左→右” 的规则,遍历顺序为:1 → 2 → 4 → 5 → 3,最终打印结果:1 2 4 5 3。
2. 中序遍历(Inorder函数)
遍历规则
先递归遍历左子树,再访问当前根节点,最后递归遍历右子树(左→根→右)。
代码实现
c
运行
// 二叉树中序遍历(递归实现)
void Inorder(Node *root)
{
// 递归终止条件:当前节点为NULL(无节点可访问)
if(root!=NULL)
{
// 步骤1:递归遍历左子树
Inorder(root->left);
// 步骤2:访问当前根节点(打印节点数据)
printf("%d ",root->data);
// 步骤3:递归遍历右子树
Inorder(root->right);
}
}
遍历结果分析(针对示例二叉树)
按照 “左→根→右” 的规则,遍历顺序为:4 → 2 → 5 → 1 → 3,最终打印结果:4 2 5 1 3。
3. 后序遍历(Postorder函数)
遍历规则
先递归遍历左子树,再递归遍历右子树,最后访问当前根节点(左→右→根)。
代码实现
c
运行
// 二叉树后序遍历(递归实现)
void Postorder(Node *root)
{
// 递归终止条件:当前节点为NULL(无节点可访问)
if(root!=NULL)
{
// 步骤1:递归遍历左子树
Postorder(root->left);
// 步骤2:递归遍历右子树
Postorder(root->right);
// 步骤3:访问当前根节点(打印节点数据)
printf("%d ",root->data);
}
}
遍历结果分析(针对示例二叉树)
按照 “左→右→根” 的规则,遍历顺序为:4 → 5 → 2 → 3 → 1,最终打印结果:4 5 2 3 1。
4. 完善main函数:调用遍历函数并打印结果
在main函数中添加遍历函数的调用,打印三种遍历的结果,验证代码正确性:
c
运行
int main()
{
// 构建示例二叉树(代码同前)
Node *root =create(1);
root->left =create(2);
root->right =create(3);
root->left->left =create(4);
root->left->right =create(5);
// 调用三种遍历函数,打印结果
printf("前序遍历结果:\n");
Preorder(root);
printf("\n\n");
printf("中序遍历结果:\n");
Inorder(root);
printf("\n\n");
printf("后序遍历结果:\n");
Postorder(root);
printf("\n\n");
return 0;
}
五、编译运行与结果验证
1. 编译命令(Linux/macOS)
将上述代码保存为binary_tree.c,在终端中执行以下编译命令:
bash
运行
gcc binary_tree.c -o binary_tree
./binary_tree
2. 预期运行结果
plaintext
前序遍历结果:
1 2 4 5 3
中序遍历结果:
4 2 5 1 3
后序遍历结果:
4 5 2 3 1
运行结果与我们之前分析的一致,说明二叉树的构建和遍历代码均正确无误。
六、嵌入式开发中的二叉树优化建议
- 递归与非递归的选择:递归遍历虽然简洁,但递归深度过深可能导致栈溢出(嵌入式设备的栈空间通常较小,一般为几 KB 到几十 KB)。对于深度较大的二叉树,建议采用非递归遍历(借助栈或队列实现),避免栈溢出风险。
- 内存释放:本文示例未添加内存释放代码,嵌入式开发中,二叉树使用完毕后,必须递归释放所有节点的内存(避免内存泄漏),示例代码如下:
c
运行
// 递归释放二叉树所有节点内存
void FreeBinaryTree(Node *root)
{
if(root!=NULL)
{
// 先释放左右子树
FreeBinaryTree(root->left);
FreeBinaryTree(root->right);
// 再释放当前节点
free(root);
root=NULL;
}
}
在main函数末尾调用FreeBinaryTree(root),即可释放整个二叉树的内存。
- 二叉搜索树(BST)扩展:本文实现的是普通二叉树,后续可学习二叉搜索树(BST),其左子树节点值均小于根节点,右子树节点值均大于根节点,可实现高效的数据查询(时间复杂度 O (logn)),更适合嵌入式场景中的数据管理。
七、总结
- 二叉树的核心是节点的左右指针域,通过指针串联起层级结构,每个节点的左、右子树也具有二叉树的递归特性;
- 二叉树的三种递归遍历核心区别在于根节点的访问时机,前序(根先访)、中序(根中访)、后序(根后访);
- 递归遍历简洁易实现,但在嵌入式设备中需注意栈溢出风险,必要时采用非递归实现;
- 嵌入式开发中使用二叉树,需重点关注内存分配失败和内存泄漏问题,确保系统长期稳定运行。
作为嵌入式开发者,掌握二叉树的基本实现和遍历,是深入学习复杂树形结构的基础。我是学嵌入式的小杨同学,关注我,后续将带大家解锁二叉树的非递归遍历、二叉搜索树的实现等进阶内容!