树是一种重要的非线性数据结构,广泛应用于数据存储、检索和排序等场景。相较于线性结构(如数组、链表),树结构可以实现 O(logn) 级别的高效查找,同时支持动态存储。二叉树作为树结构的核心分支,以其简洁的结构和高效的操作特性,成为数据结构学习的重点。
一、树与二叉树的核心概念
在深入代码之前,我们先梳理树与二叉树的基础定义和特性,为后续的代码实现奠定理论基础。
1.1 树的基本定义
树是 n(n≥0) 个结点的有限集合,满足以下条件:
- 当 n=0 时,称为空树;
- 当 n>1 时,有且仅有一个根结点,其余结点可分为 m 个互不相交的有限子集,每个子集都是一棵独立的子树。
树的关键术语:
- 结点的度:结点拥有的子树个数;
- 叶结点:度为 0 的结点(无子女结点);
- 分支结点:度不为 0 的结点;
- 树的度:树中所有结点的度的最大值;
- 树的深度(高度):从根结点开始计数,根为第 1 层,根的子结点为第 2 层,以此类推。
1.2 二叉树的定义与特点
二叉树是一种特殊的树结构,其每个结点最多只有两棵子树,分别称为左子树 和右子树。
二叉树的核心特点:
- 每个结点的度最大为 2;
- 左子树和右子树是有序的,次序不能颠倒;
- 即使只有一棵子树,也必须区分是左子树还是右子树。
1.3 特殊的二叉树
- 斜树:所有结点都只有左子树的称为左斜树,所有结点都只有右子树的称为右斜树(斜树的结构等价于链表,查找效率退化为 O(n));
- 满二叉树:所有分支结点都存在左右子树,且所有叶结点都在同一层;
- 完全二叉树:对一棵有 n 个结点的二叉树按层序编号,若编号 i(1≤i≤n) 的结点与同样深度的满二叉树中编号 i 的结点位置完全相同,则为完全二叉树(完全二叉树是效率很高的数据结构,堆就是基于完全二叉树实现的)。
1.4 二叉树的重要特性
- 第 i 层上最多有 2i−1 个结点(i≥1);
- 深度为 k 的二叉树至多有 2k−1 个结点(k≥1);
- 任意二叉树中,叶结点数 n0 与度为 2 的结点数 n2 满足:n0=n2+1;
- 有 n 个结点的完全二叉树深度为 ⌊log2n⌋+1。
1.5 二叉树的前序、中序、后序遍历概念
二叉树的深度优先遍历 核心是围绕根结点、左子树、右子树的访问顺序展开,根据根结点的访问时机不同,分为前序、中序、后序三种遍历方式,这三种遍历是二叉树操作的基础。
| 遍历方式 | 核心顺序 | 访问逻辑 | 核心特点 |
|---|---|---|---|
| 前序遍历 | 根 → 左 → 右 | 先访问当前根结点,再递归遍历左子树,最后递归遍历右子树 | 根结点最先被访问,可用于快速复制二叉树结构 |
| 中序遍历 | 左 → 根 → 右 | 先递归遍历左子树,再访问当前根结点,最后递归遍历右子树 | 对于二叉搜索树,中序遍历结果是有序序列 |
| 后序遍历 | 左 → 右 → 根 | 先递归遍历左子树,再递归遍历右子树,最后访问当前根结点 | 根结点最后被访问,可用于安全销毁二叉树(先释放子结点再释放根) |
二、二叉树的存储结构
二叉树的存储结构分为顺序存储 和链式存储两种,两种方式各有优劣:
| 存储方式 | 优点 | 缺点 |
|---|---|---|
| 顺序存储 | 访问结点速度快,无需指针 | 空间利用率低,适合完全二叉树,不适合斜树 |
| 链式存储 | 空间利用率高,动态分配内存,灵活性强 | 需额外存储指针,访问结点需遍历指针 |
本文采用链式存储实现,通过指针连接结点,灵活性更高。
2.1 链式存储的结点结构定义
链式存储的二叉树结点包含三部分:数据域、左子树指针、右子树指针。同时,为了支持非递归遍历,我们还需要定义链式栈结构。
将以下代码放在 tree.h 头文件中:
#ifndef _TREE_H_
#define _TREE_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 二叉树结点结构
typedef struct treenode
{
// 结点数据,本文以字符为例
char data;
// 左子树指针
struct treenode *left;
// 右子树指针
struct treenode *right;
} TREENODE;
// 栈结点结构(用于非递归遍历)
typedef struct stacknode
{
TREENODE *treeNode;
struct stacknode *next;
} StackNode;
// 栈结构(链式栈)
typedef struct
{
StackNode *top;
int size;
} LinkStack;
// 栈操作函数声明
LinkStack *CreateLinkStack();
int PushStack(LinkStack *stack, TREENODE *node);
TREENODE *PopStack(LinkStack *stack);
int IsEmptyStack(LinkStack *stack);
void DestroyLinkStack(LinkStack *stack);
// 二叉树操作函数声明
void CreateRoot(TREENODE **root, char *data, int *inx);
void PreOrderTraverse(TREENODE *T);
void InOrderTraverse(TREENODE *T);
void PostOrderTraverse(TREENODE *T);
void PreOrderTraverseNonRecursive(TREENODE *T);
void InOrderTraverseNonRecursive(TREENODE *T);
void PostOrderTraverseNonRecursive(TREENODE *T);
void LevelOrderTraverse(TREENODE *root);
void DestroyTree(TREENODE *root);
#endif
三、二叉树的构建:基于前序遍历的递归创建
构建二叉树的常用方法是前序遍历序列 + 空结点标记 ,本文使用 # 表示空结点。例如序列 abd##e##c#fh### 对应的二叉树结构如下:
a
/ \
b c
/ \ \
d e f
/
h
3.1 核心思路
递归创建二叉树的逻辑严格遵循前序遍历的根 - 左 - 右顺序:
- 读取序列中的一个字符;
- 若字符为
#,则当前结点为空; - 若字符不为
#,则分配内存创建结点,存入数据,再递归创建左子树和右子树。
3.2 构建代码实现
优化点:将前序序列和索引作为参数传入,避免使用全局变量,提高代码可复用性。
#include "tree.h"
#include "linkque.h"
// 递归创建二叉树
void CreateRoot(TREENODE **root, char *data, int *inx)
{
char c = data[(*inx)++];
// 空结点,直接返回
if ('#' == c)
{
*root = NULL;
return;
}
else
{
// 分配结点内存
*root = (TREENODE *)malloc(sizeof(TREENODE));
if (NULL == *root)
{
printf("CreateRoot malloc error\n");
return;
}
// 存入当前结点数据
(*root)->data = c;
// 递归创建左子树
CreateRoot(&(*root)->left, data, inx);
// 递归创建右子树
CreateRoot(&(*root)->right, data, inx);
}
}
四、二叉树的遍历算法
二叉树的遍历是指按某种规则访问树中所有结点,且每个结点仅被访问一次。常见的遍历方式分为深度优先遍历 (前序、中序、后序)和广度优先遍历(层序)两类,本文同时实现递归和非递归版本。
4.1 链式栈实现(非递归遍历依赖)
非递归遍历需要借助栈来模拟递归调用栈,以下是链式栈的完整实现:
#include "tree.h"
// 创建空栈
LinkStack *CreateLinkStack()
{
LinkStack *stack = (LinkStack *)malloc(sizeof(LinkStack));
if (NULL == stack)
{
printf("CreateLinkStack malloc error\n");
return NULL;
}
stack->top = NULL;
stack->size = 0;
return stack;
}
// 入栈操作
int PushStack(LinkStack *stack, TREENODE *node)
{
if (NULL == stack || NULL == node)
{
return 1;
}
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (NULL == newNode)
{
printf("PushStack malloc error\n");
return 1;
}
newNode->treeNode = node;
newNode->next = stack->top;
stack->top = newNode;
stack->size++;
return 0;
}
// 出栈操作
TREENODE *PopStack(LinkStack *stack)
{
if (NULL == stack || IsEmptyStack(stack))
{
return NULL;
}
StackNode *tmp = stack->top;
TREENODE *treeNode = tmp->treeNode;
stack->top = tmp->next;
free(tmp);
stack->size--;
return treeNode;
}
// 判断栈是否为空
int IsEmptyStack(LinkStack *stack)
{
return (stack == NULL || stack->size == 0);
}
// 销毁栈
void DestroyLinkStack(LinkStack *stack)
{
if (NULL == stack)
{
return;
}
while (!IsEmptyStack(stack))
{
PopStack(stack);
}
free(stack);
}
4.2 深度优先遍历:递归实现
深度优先遍历的核心是递归思想,通过不断深入子树完成遍历,三种遍历方式的区别在于访问根结点的时机,完全契合前面定义的遍历概念。
4.2.1 前序遍历(根 - 左 - 右)
遍历顺序:先访问根结点 → 递归遍历左子树 → 递归遍历右子树
void PreOrderTraverse(TREENODE *T)
{
if (NULL == T)
{
return;
}
// 访问根结点
printf("%c ", T->data);
// 遍历左子树
PreOrderTraverse(T->left);
// 遍历右子树
PreOrderTraverse(T->right);
}
对于示例序列构建的二叉树,前序遍历结果:a b d e c f h
4.2.2 中序遍历(左 - 根 - 右)
遍历顺序:递归遍历左子树 → 访问根结点 → 递归遍历右子树
void InOrderTraverse(TREENODE *T)
{
if (NULL == T)
{
return;
}
// 遍历左子树
InOrderTraverse(T->left);
// 访问根结点
printf("%c ", T->data);
// 遍历右子树
InOrderTraverse(T->right);
}
示例二叉树的中序遍历结果:d b e a c h f
4.2.3 后序遍历(左 - 右 - 根)
遍历顺序:递归遍历左子树 → 递归遍历右子树 → 访问根结点
void PostOrderTraverse(TREENODE *T)
{
if (NULL == T)
{
return;
}
// 遍历左子树
PostOrderTraverse(T->left);
// 遍历右子树
PostOrderTraverse(T->right);
// 访问根结点
printf("%c ", T->data);
}
示例二叉树的后序遍历结果:d e b h f c a
4.3 深度优先遍历:非递归实现
递归遍历虽简洁,但递归深度过大时可能导致栈溢出,非递归遍历通过手动模拟栈调用,更贴近底层实现,稳定性更高。其核心逻辑依然遵循前序、中序、后序的访问顺序规则。
4.3.1 非递归前序遍历
核心思路:
-
根结点入栈,循环弹出栈顶结点并访问(符合 "根优先" 的前序规则);
-
先将右子结点入栈(栈先进后出,保证左子树先遍历),再将左子结点入栈;
-
重复直到栈为空。
void PreOrderTraverseNonRecursive(TREENODE *T)
{
if (NULL == T)
{
return;
}
LinkStack *stack = CreateLinkStack();
PushStack(stack, T);while (!IsEmptyStack(stack)) { TREENODE *node = PopStack(stack); // 访问当前结点 printf("%c ", node->data); // 右子结点先入栈(后遍历) if (node->right != NULL) { PushStack(stack, node->right); } // 左子结点后入栈(先遍历) if (node->left != NULL) { PushStack(stack, node->left); } } DestroyLinkStack(stack);}
4.3.2 非递归中序遍历
核心思路:
-
从根结点开始,将所有左子结点入栈,直到左子结点为空(符合 "先左" 的中序规则);
-
弹出栈顶结点并访问,然后处理其右子树;
-
重复直到栈为空且当前结点为 NULL。
void InOrderTraverseNonRecursive(TREENODE *T)
{
if (NULL == T)
{
return;
}
LinkStack *stack = CreateLinkStack();
TREENODE *cur = T;while (cur != NULL || !IsEmptyStack(stack)) { // 左子结点全部入栈 while (cur != NULL) { PushStack(stack, cur); cur = cur->left; } // 弹出栈顶结点并访问 cur = PopStack(stack); printf("%c ", cur->data); // 处理右子树 cur = cur->right; } DestroyLinkStack(stack);}
4.3.3 非递归后序遍历
核心思路(双栈法):
-
栈 1 存储待处理结点,栈 2 存储访问顺序;
-
根结点入栈 1,弹出后入栈 2,再将左、右子结点依次入栈 1;
-
栈 1 为空时,依次弹出栈 2 结点并访问(栈 2 顺序为根 - 右 - 左,弹出后为左 - 右 - 根,符合后序规则)。
void PostOrderTraverseNonRecursive(TREENODE *T)
{
if (NULL == T)
{
return;
}
LinkStack *stack1 = CreateLinkStack();
LinkStack *stack2 = CreateLinkStack();
PushStack(stack1, T);while (!IsEmptyStack(stack1)) { TREENODE *node = PopStack(stack1); PushStack(stack2, node); // 左子结点先入栈1 if (node->left != NULL) { PushStack(stack1, node->left); } // 右子结点后入栈1 if (node->right != NULL) { PushStack(stack1, node->right); } } // 遍历栈2,输出后序结果 while (!IsEmptyStack(stack2)) { TREENODE *node = PopStack(stack2); printf("%c ", node->data); } DestroyLinkStack(stack1); DestroyLinkStack(stack2);}
4.4 广度优先遍历:层序遍历(队列实现)
层序遍历的顺序是从上到下、从左到右访问每一层的结点,其实现依赖队列的 "先进先出" 特性,与深度优先遍历的前序、中序、后序规则完全不同。
4.4.1 链式队列的实现
层序遍历依赖队列结构,我们需要先实现一个链式队列(对应 linkque.h 和 linkque.c)。
队列头文件 linkque.h
#ifndef _LINKQUE_H_
#define _LINKQUE_H_
#include "tree.h"
// 队列数据类型为二叉树结点
typedef TREENODE DATATYPE;
// 队列结点结构
typedef struct quenode
{
DATATYPE data;
struct quenode *next;
} LinkQueNode;
// 队列结构(带头尾指针)
typedef struct
{
LinkQueNode *head;
LinkQueNode *tail;
// 队列长度
int clen;
} LinkQue;
// 函数声明
LinkQue * CreateLinkQue();
int EnterLinkQue(LinkQue *lq,DATATYPE* newnode);
int QuitLinkQue(LinkQue*lq);
DATATYPE* GetHeadLinkQue(LinkQue *lq);
int GetSizeLinkQue(LinkQue *lq);
int IsEmptyLinkQue(LinkQue *lq);
int DestroyLinkQue(LinkQue * lq);
#endif
队列实现文件 linkque.c
#include "linkque.h"
// 创建空队列
LinkQue *CreateLinkQue()
{
LinkQue *lq = (LinkQue *)malloc(sizeof(LinkQue));
if (NULL == lq)
{
printf("CreateLinkQue malloc error\n");
return NULL;
}
lq->head = NULL;
lq->tail = NULL;
lq->clen = 0;
return lq;
}
// 入队操作
int EnterLinkQue(LinkQue *lq, DATATYPE *newdata)
{
LinkQueNode *newnode = (LinkQueNode *)malloc(sizeof(LinkQueNode));
if (NULL == newnode)
{
printf("EnterLinkQue malloc error\n");
return 1;
}
memcpy(&newnode->data, newdata, sizeof(DATATYPE));
newnode->next = NULL;
if (IsEmptyLinkQue(lq))
{
// 空队列,头尾指针指向新结点
lq->head = newnode;
lq->tail = newnode;
}
else
{
// 新结点加入队尾
lq->tail->next = newnode;
lq->tail = newnode;
}
lq->clen++;
return 0;
}
// 获取队列长度
int GetSizeLinkQue(LinkQue *lq)
{
return lq->clen;
}
// 判断队列是否为空
int IsEmptyLinkQue(LinkQue *lq)
{
return 0 == lq->clen;
}
// 销毁队列
int DestroyLinkQue(LinkQue *lq)
{
int size = GetSizeLinkQue(lq);
int i = 0 ;
for(i=0;i<size;i++)
{
QuitLinkQue(lq);
}
free(lq);
return 0;
}
// 获取队首元素
DATATYPE *GetHeadLinkQue(LinkQue *lq)
{
if(IsEmptyLinkQue(lq))
{
return NULL;
}
return &lq->head->data;
}
// 出队操作
int QuitLinkQue(LinkQue *lq)
{
if(IsEmptyLinkQue(lq))
{
return 1;
}
LinkQueNode* tmp = lq->head;
lq->head = lq->head->next;
// 队列为空时,尾指针置空
if(NULL == lq->head)
{
lq->tail = NULL;
}
free(tmp);
lq->clen--;
return 0;
}
4.4.2 层序遍历代码实现
void LevelOrderTraverse(TREENODE *root)
{
if (NULL == root)
{
return;
}
// 创建空队列
LinkQue *lq = CreateLinkQue();
// 根结点入队
EnterLinkQue(lq, root);
while (!IsEmptyLinkQue(lq))
{
// 获取队首结点
DATATYPE *tmp = GetHeadLinkQue(lq);
// 访问队首结点
printf("%c ", tmp->data);
// 左子结点入队
if (NULL != tmp->left)
{
EnterLinkQue(lq, tmp->left);
}
// 右子结点入队
if (NULL != tmp->right)
{
EnterLinkQue(lq, tmp->right);
}
// 队首结点出队
QuitLinkQue(lq);
}
// 销毁队列
DestroyLinkQue(lq);
}
示例二叉树的层序遍历结果:a b c d e f h
五、二叉树的销毁:防止内存泄漏
链式存储的二叉树在使用完毕后,需要手动释放所有结点的内存,避免内存泄漏。销毁逻辑采用后序遍历的思想:先销毁左子树 → 再销毁右子树 → 最后释放根结点,这正是后序遍历 "根最后访问" 特性的典型应用。
void DestroyTree(TREENODE *root)
{
if (NULL == root)
{
return;
}
// 销毁左子树
DestroyTree(root->left);
// 销毁右子树
DestroyTree(root->right);
// 释放当前结点
free(root);
root = NULL; // 避免野指针
}
六、完整测试代码与运行结果
6.1 main 函数测试
#include "tree.h"
#include "linkque.h"
int main(int argc, char **argv)
{
TREENODE *root = NULL;
// 前序遍历序列,#表示空结点
char data[] = "abd##e##c#fh###";
int inx = 0;
// 创建二叉树
CreateRoot(&root, data, &inx);
printf("===== 递归遍历 =====\n");
printf("前序遍历:");
PreOrderTraverse(root);
printf("\n中序遍历:");
InOrderTraverse(root);
printf("\n后序遍历:");
PostOrderTraverse(root);
printf("\n===== 非递归遍历 =====\n");
printf("前序遍历:");
PreOrderTraverseNonRecursive(root);
printf("\n中序遍历:");
InOrderTraverseNonRecursive(root);
printf("\n后序遍历:");
PostOrderTraverseNonRecursive(root);
printf("\n===== 层序遍历 =====\n");
printf("层序遍历:");
LevelOrderTraverse(root);
printf("\n");
// 销毁二叉树
DestroyTree(root);
root = NULL;
return 0;
}
6.2 编译与运行
将 tree.h、tree.c、linkque.h、linkque.c 和 main.c 放在同一目录下,使用以下命令编译:
gcc -o binary_tree main.c tree.c linkque.c
./binary_tree
运行结果:
===== 递归遍历 =====
前序遍历:a b d e c f h
中序遍历:d b e a c h f
后序遍历:d e b h f c a
===== 非递归遍历 =====
前序遍历:a b d e c f h
中序遍历:d b e a c h f
后序遍历:d e b h f c a
===== 层序遍历 =====
层序遍历:a b c d e f h
七、递归 vs 非递归遍历:优缺点分析
| 遍历方式 | 递归实现 | 非递归实现 |
|---|---|---|
| 代码复杂度 | 简洁易懂,行数少 | 代码量稍多,需手动管理栈 |
| 性能 | 存在函数调用栈开销,深度大时易栈溢出 | 手动管理栈,开销更小,无栈溢出风险 |
| 适用场景 | 小规模二叉树,代码可读性优先 | 大规模二叉树,性能优先 |
八、总结
1、二叉树的深度优先遍历分为前序(根左右)、中序(左根右)、后序(左右根)三种,其区别在于根结点的访问时机;
2、二叉树的链式存储通过结点的左右指针实现动态扩展,相较于顺序存储更灵活;
3、递归遍历的核心是 "分治思想",非递归遍历通过栈模拟递归调用栈,避免栈溢出风险;
4、层序遍历依赖队列的 "先进先出" 特性,实现按层访问结点;
5、二叉树的销毁逻辑基于后序遍历,先释放子结点再释放根结点,可有效避免内存泄漏;
6、二叉树是许多高级数据结构的基础,掌握其遍历算法是学习后续内容的关键。