【嵌入式 C 语言实战】二叉树入门:创建与前 / 中 / 后序递归遍历详解

45 阅读9分钟

【嵌入式 C 语言实战】二叉树入门:创建与前 / 中 / 后序递归遍历详解

大家好,我是学嵌入式的小杨同学。在嵌入式开发中,除了单链表这类线性数据结构,树形结构也有着广泛的应用(比如配置项树、设备树、语法解析树等),而二叉树是树形结构中最基础、最核心的一种。今天就带大家从零开始,实现一棵简单的二叉树,并且掌握二叉树的三种核心遍历方式 —— 前序、中序、后序递归遍历,全程附可直接编译运行的 C 语言代码。

一、二叉树的核心概念(嵌入式开发视角)

1. 什么是二叉树?

二叉树是一种每个节点最多只有两个子节点的树形结构,这两个子节点分别被称为 “左子节点” 和 “右子节点”,对应的子树被称为 “左子树” 和 “右子树”。

本文实现的是一棵简单的完全二叉树(非平衡二叉树、非搜索二叉树),结构如下,这也是二叉树入门的经典示例:

plaintext

        1  (根节点)
      /   \
     2     3  (第二层节点)
    / \
   4   5  (第三层节点)

2. 二叉树的特点与嵌入式应用场景

  • 特点:非线性结构,具有层级关系,每个节点的左、右子树也是一棵二叉树(递归特性);

  • 嵌入式应用场景:

    1. 设备树解析:嵌入式系统中描述硬件设备的设备树,本质就是一种树形结构,二叉树是其解析的基础;
    2. 配置项管理:系统中的多级配置项(如系统→外设→参数),可通过二叉树存储和查询;
    3. 数据排序与查询:后续学习的二叉搜索树(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;
}
嵌入式注意点
  1. 嵌入式设备内存资源有限,malloc动态分配内存可能失败,必须添加判空逻辑,避免野指针导致系统崩溃;
  2. 若追求更高的稳定性,可采用静态内存分配(预先定义节点数组)替代malloc,避免内存碎片化问题。

2. 构建示例二叉树(main函数中手动构建)

本文采用手动构建的方式创建示例二叉树(适合入门理解),通过给根节点和子节点的leftright指针赋值,串联起整个二叉树结构。

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. 前序遍历:根节点 → 左子树 → 右子树
  2. 中序遍历:左子树 → 根节点 → 右子树
  3. 后序遍历:左子树 → 右子树 → 根节点

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 

运行结果与我们之前分析的一致,说明二叉树的构建和遍历代码均正确无误。

六、嵌入式开发中的二叉树优化建议

  1. 递归与非递归的选择:递归遍历虽然简洁,但递归深度过深可能导致栈溢出(嵌入式设备的栈空间通常较小,一般为几 KB 到几十 KB)。对于深度较大的二叉树,建议采用非递归遍历(借助栈或队列实现),避免栈溢出风险。
  2. 内存释放:本文示例未添加内存释放代码,嵌入式开发中,二叉树使用完毕后,必须递归释放所有节点的内存(避免内存泄漏),示例代码如下:

c

运行

// 递归释放二叉树所有节点内存
void FreeBinaryTree(Node *root)
{
	if(root!=NULL)
	{
		// 先释放左右子树
		FreeBinaryTree(root->left);
		FreeBinaryTree(root->right);
		// 再释放当前节点
		free(root);
		root=NULL;
	}
}

main函数末尾调用FreeBinaryTree(root),即可释放整个二叉树的内存。

  1. 二叉搜索树(BST)扩展:本文实现的是普通二叉树,后续可学习二叉搜索树(BST),其左子树节点值均小于根节点,右子树节点值均大于根节点,可实现高效的数据查询(时间复杂度 O (logn)),更适合嵌入式场景中的数据管理。

七、总结

  1. 二叉树的核心是节点的左右指针域,通过指针串联起层级结构,每个节点的左、右子树也具有二叉树的递归特性;
  2. 二叉树的三种递归遍历核心区别在于根节点的访问时机,前序(根先访)、中序(根中访)、后序(根后访);
  3. 递归遍历简洁易实现,但在嵌入式设备中需注意栈溢出风险,必要时采用非递归实现;
  4. 嵌入式开发中使用二叉树,需重点关注内存分配失败内存泄漏问题,确保系统长期稳定运行。

作为嵌入式开发者,掌握二叉树的基本实现和遍历,是深入学习复杂树形结构的基础。我是学嵌入式的小杨同学,关注我,后续将带大家解锁二叉树的非递归遍历、二叉搜索树的实现等进阶内容!