前言
本文使用C语言利用非递归方法生成一棵二叉树以及实现遍历等操作,包括前序、中序、后序及层序遍历。
在实现的过程中需要使用到堆栈和队列,整体代码实现较为繁杂,若将其全部放入一个C程序文件,则会变得较为冗长,不利于程序代码的可读性,故我借助了 CLion 以大程序文件的形式进行二叉树及相关操作的实现,文章结尾处有我的代码文件。
思路
首先需要实现一个二叉树,后面的操作都是在这棵二叉树上做的。
我们采用层序生成二叉树,层序创建所用的节点输入序列是按照树的从左至右,从上到下的顺序形成的,空节点用一个特定的标志 nullNode 表示(以字符存储为例,可以定义空节点的值为 ‘-’ )。在构造二叉树的过程中,需要一个队列暂时存储各节点地址,具体过程如下:
- 输入第一个数据
- 若为 nullNode,表示此树为空,直接将根指针置为 NULL 并返回,树构造完成。
- 若不为 nullNode,则动态分配一块内存存储该值,并将其左、右子树值置为空,最后将该值放入队列中。
- 取出队列中的元素,建立其左、右子树
- 执行出队操作,得到当前元素;
- 先输入一个数据,若为 nullNode,则进行下一步;反之,为当前元素的左子树分配空间存储所输入的值,将左子树的左子树和右子树置为 NULL,最后将生成的节点入队;
- 接着输入另一个数据,若为 nullNode,则进行回到上一步;反之,为当前元素的右子树分配空间存储所输入的值,将右子树的左子树和右子树置为 NULL。最后将生成节点入队。
- 重复第 2 步操作,直到队列为空,构造完成
为了实现上述操作,我们必须先实现一个队列,代码如下:
- 创建二叉树:
首先为 Queue.h 头文件
// 条件编译指令
#ifndef CLIONPROJECT_QUEUE_H
#define CLIONPROJECT_QUEUE_H
// 包含二叉树的头文件,其中定义了BinTree结构
#include "BinaryTree.h"
// 定义宏QUEUE_EMPTY,用于表示队列为空的情况
#define QUEUE_EMPTY NULL
// 声明BinTree类型
typedef struct BinTree BinTree;
// 定义队列结构体Queue
typedef struct Queue {
BinTree **queue; // 队列数组,存储指向BinTree的指针
int front, rear; // 队列的前端和后端索引
int queSize; // 队列的大小
} Queue;
// 创建一个队列
Queue *CreateQueue(void);
// 检查队列是否已满
bool isFull(Queue *que);
// 向队列中添加一个元素
bool AddQueue(Queue *que, BinTree *val);
// 检查队列是否为空
bool QueueIsEmpty(Queue *que);
// 从队列中删除一个元素
BinTree *DeleteQueue(Queue *que);
// 销毁队列
void DestroyQueue(Queue *que);
#endif //CLIONPROJECT_QUEUE_H
Queue.c 文件如下:
#include "Queue.h"
Queue *CreateQueue() {
// 分配内存给队列结构体
Queue *que = (Queue *) malloc(sizeof(Queue));
// 分配内存给队列数组,用于存储指向BinTree的指针
que->queue = (BinTree **) malloc(sizeof(BinTree *) * MAXSIZE);
// 初始化队列数组中的所有指针为NULL
for (int i = 0; i < MAXSIZE; i++) {
que->queue[i] = NULL;
}
// 初始化队列的大小、前端和后端索引
que->queSize = MAXSIZE;
que->front = 0;
que->rear = 0;
return que;
}
bool isFull(Queue *que) {
// 如果后端索引的下一个位置是前端索引,则队列为满
return (que->rear + 1) % que->queSize == que->front;
}
bool AddQueue(Queue *que, BinTree *val) {
// 如果队列已满,无法添加新元素,返回false
if (isFull(que)) {
return false;
}
// 将后端索引移动到下一个位置,并更新队列数组中相应的位置
que->rear = (que->rear + 1) % que->queSize;
que->queue[que->rear] = val;
return true;
}
bool QueueIsEmpty(Queue *que) {
// 如果前端和后端索引相同,则队列为空
return que->rear == que->front;
}
BinTree *DeleteQueue(Queue *que) {
// 如果队列为空,返回NULL指针
if (QueueIsEmpty(que)) {
return QUEUE_EMPTY;
}
// 将前端索引移动到下一个位置,并返回队列数组中被删除的元素
que->front = (que->front + 1) % que->queSize;
// 返回被删除的元素
return que->queue[que->front];
}
void DestroyQueue(Queue *que) {
// 释放队列数组的内存
free(que->queue);
// 释放队列结构体的内存
free(que);
}
声明二叉树
二叉树的头文件如下:
#ifndef CLIONPROJECT_BINARYTREE_H
#define CLIONPROJECT_BINARYTREE_H
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include "Queue.h"
#include "Stack.h"
// 定义二叉树的最大节点数
#define MAXSIZE 100
// 定义元素类型为字符
#define eleType char
// 定义一个宏,用于表示不存在的元素值
#define NOTEXIST '-'
// 定义二叉树节点的结构体
typedef struct BinTree {
eleType val; // 节点存储的元素值
struct BinTree *left; // 指向左子树的指针
struct BinTree *right; // 指向右子树的指针
} BinTree;
// 创建二叉树
BinTree *createBinaryTree(eleType arr[], int arrSize);
// 销毁二叉树
void destroyBinaryTree(BinTree *binTree);
// 前序遍历二叉树
void preOrderTraversal(BinTree *binTree);
// 中序遍历二叉树
void inOrderTraversal(BinTree *binTree);
// 后序遍历二叉树
void postOrderTraversal(BinTree *binTree);
// 层序遍历二叉树
void levelOrderTraversal(BinTree *binTree);
// 计算二叉树的高度
int getHeight(BinTree *binTree);
#endif //CLIONPROJECT_BINARYTREE_H
思路如下图解:
- 将第一个元素入队,作为根结点
- 依次遍历数组,为出队元素的根结点添加左右孩子,并将左右孩子依次入队
- 当遇到 ‘-’ 时,表示空节点,无左孩子
- 添加右孩子,数组结束,退出循环,最后返回根结点
具体代码如下:
// 创建二叉树的函数
BinTree *createBinaryTree(eleType arr[], int arrSize ) {
// 如果数组的第一个元素表示不存在的节点,则返回空指针
if ( arr[0] == NOTEXIST ) {
return NULL;
}
// 为二叉树的根节点分配内存
BinTree *binTree = ( BinTree * ) malloc(sizeof(BinTree));
// 创建一个队列用于层序遍历
Queue *que = CreateQueue();
// 设置根节点的值
binTree->val = arr[0];
// 初始化根节点的左右子节点为NULL
binTree->left = binTree->right = NULL;
// 将根节点添加到队列中
AddQueue(que, binTree);
// 打印根节点的值
printf("%d : %c\n", 0, binTree->val);
// 临时变量用于存储队列中取出的节点
BinTree *temp = NULL;
// 从数组的第二个元素开始遍历
for (int idx = 1; idx < arrSize; idx++) {
// 从队列中取出一个节点
temp = DeleteQueue(que);
// 如果当前元素表示不存在的节点,则不创建左子节点
if ( arr[idx] == NOTEXIST ) {
temp->left = NULL;
}
// 否则,为当前节点创建左子节点,并将其添加到队列中
else {
temp->left = ( BinTree * ) malloc(sizeof(BinTree));
temp->left->val = arr[idx];
temp->left->left = temp->left->right = NULL;
// 打印新创建的左子节点的值
printf("l: %d : %c\n", idx, temp->left->val);
// 将新创建的左子节点添加到队列中
AddQueue(que, temp->left);
}
// 如果下一个元素也表示不存在的节点,则跳过
if ( arr[idx + 1] == NOTEXIST ) {
temp->right = NULL;
// 由于跳过了一个元素,所以索引需要增加1
idx++;
}
// 如果下一个元素表示存在的节点,则为其创建右子节点,并添加到队列中
else {
temp->right = ( BinTree * ) malloc(sizeof(BinTree));
temp->right->val = arr[++idx];
temp->right->left = temp->right->right = NULL;
// 打印新创建的右子节点的值
printf("r: %d : %c\n", idx, temp->right->val);
// 将新创建的右子节点添加到队列中
AddQueue(que, temp->right);
}
}
// 遍历完成后,销毁队列
DestroyQueue(que);
// 返回创建的二叉树的根节点
return binTree;
}
- 二叉树的销毁
这个函数首先检查传入的 binTree 指针是否为 NULL。如果不为 NULL,它递归地调用自身来销毁 binTree 的左子树和右子树。在左右子树都被销毁之后,使用 free 函数释放当前节点的内存。这个过程一直递归进行,直到所有的节点都被释放,从而完全销毁了二叉树。
// 销毁二叉树的函数
void destroyBinaryTree(BinTree *binTree) {
if (binTree != NULL) {
destroyBinaryTree(binTree->left);
destroyBinaryTree(binTree->right);
free(binTree);
}
}
- 二叉树的遍历
首先需要实现一个堆栈:
这个头文件定义了一个栈的数据结构 Stack,其中包含一个指向 BinTree 节点的指针数组 data,一个整数 top用于追踪栈顶的位置,以及一个整数 capacity 表示栈的最大容量。栈使用数组来存储元素,top 索引指向栈中的最后一个元素。
- createStack 函数用于创建一个栈实例;
- isEmpty 函数用于检查栈是否为空;
- Push 函数用于向栈中添加元素;
- Pop 函数用于移除并返回栈顶元素;
- destroyStack 函数用于销毁栈并释放所有节点的内存。
#ifndef CLIONPROJECT_STACK_H
#define CLIONPROJECT_STACK_H
// 包含二叉树的头文件,以便在栈中使用BinTree类型的数据
#include "BinaryTree.h"
#define MAXSIZE 100
// 定义STACK_EMPTY为NULL,用于表示栈为空的情况
#define STACK_EMPTY NULL
typedef struct BinTree BinTree;
// 定义栈的结构体
typedef struct Stack {
BinTree **data; // 存储指向BinTree节点的指针数组
int top; // 栈顶索引,指向栈中最后一个元素的位置
int capacity; // 栈的总容量
} Stack;
// 创建一个栈
Stack *createStack(int size);
// 检查栈是否为空
bool isEmpty(Stack *stk);
// 向栈中添加一个元素
bool Push(Stack *stk, BinTree *val);
// 从栈中移除并返回栈顶元素
BinTree *Pop(Stack *stk);
// 销毁栈并释放内存
void destroyStack(Stack *stk);
#endif //CLIONPROJECT_STACK_H
#include "Stack.h"
// 创建一个栈的函数
Stack *createStack(int size) {
// 为栈结构体分配内存
Stack *obj = (Stack *)malloc(sizeof(Stack));
// 为栈的数据数组分配内存,大小为size
obj->data = (BinTree **)malloc(sizeof(BinTree *) * size);
// 初始化栈中的所有元素为NULL
for (int i = 0; i < size; i++) {
obj->data[i] = NULL;
}
// 初始化栈顶索引为-1,表示栈为空
obj->top = -1;
// 设置栈的容量
obj->capacity = size;
return obj;
}
// 检查栈是否为空的函数
bool isEmpty(Stack *stk) {
// 如果栈顶索引为-1,则栈为空
return stk->top == -1;
}
// 向栈中添加元素的函数
bool Push(Stack *stk, BinTree *val) {
// 如果栈已满(栈顶索引加1等于容量),则无法添加新元素
if (stk->top + 1 == stk->capacity) {
return false;
}
// 将新元素添加到栈顶,并更新栈顶索引
stk->data[++stk->top] = val;
// 添加成功,返回true
return true;
}
// 从栈中移除并返回栈顶元素的函数
BinTree *Pop(Stack *stk) {
// 如果栈为空,则返回STACK_EMPTY(假设为NULL)
if (isEmpty(stk)) {
return STACK_EMPTY;
}
// 保存栈顶元素,然后更新栈顶索引
BinTree *temp = stk->data[stk->top--];
// 返回被移除的栈顶元素
return temp;
}
// 销毁栈并释放内存的函数
void destroyStack(Stack *stk) {
// 释放栈的数据数组
free(stk->data);
// 释放栈结构体本身
free(stk);
}
- 先序遍历
非递归先序遍历(PreorderTraversal)二叉树的思路是使用栈来模拟递归过程。先序遍历的顺序是先访问根节点,然后遍历左子树,最后遍历右子树。步骤如下:
初始化栈:创建一个空栈。
访问根节点:将根节点压入栈中。
循环遍历:当栈不为空时,执行以下操作:
- 弹出栈顶元素,这是当前要访问的节点。
- 访问该节点(例如,打印节点的值)。
- 如果该节点有右子节点,将右子节点压入栈中(右子节点会后于左子节点被处理,因为栈是后进先出的)。
- 如果该节点有左子节点,将左子节点压入栈中(左子节点会先于右子节点被处理)。
处理子节点:对于每个节点,重复步骤 3,直到所有子节点都被访问。
结束遍历:当栈为空时,遍历结束。
在这个过程中,栈帮助我们保持了递归过程中的调用顺序。每次从栈中弹出节点时,我们首先访问它,然后将其右子节点(如果有的话)压入栈中,这样右子节点会在左子节点之后被访问。左子节点也压入栈中,但由于栈的后进先出特性,它会在右子节点之后被处理。这样,我们就可以在不使用递归的情况下实现先序遍历。
以上图的树为例,使用栈来实现先序遍历的图解如下:
- Print 表示先序遍历的结果,并初始化栈
- 从 a 开始遍历左子树,将元素打印并入栈
- 当遍历到最左边的结点时,跳出循环,将栈顶元素弹出,做 temp = temp->right,开始右子树的入栈和输出
- 当 temp->right = NULL 时,不进行入栈操作,将栈顶元素弹出,进行该元素的右子树遍历
- 重复操作,直到栈空为止
- 遍历操作结束
具体代码实现如下:
// 前序遍历二叉树的函数
void preOrderTraversal(BinTree *binTree) {
Stack *stk = createStack(MAXSIZE);
// 初始化临时变量temp,指向二叉树的根节点
BinTree *temp = binTree;
// 当temp不为空或者栈不为空时,继续遍历
while (temp != NULL || !isEmpty(stk)) {
// 当temp不为空时,进入内层循环
while (temp != NULL) {
// 将当前节点temp压入栈中
Push(stk, temp);
// 访问当前节点,打印其值
printf("%c ", temp->val);
// 将temp指针指向其左子节点,为下一次循环访问左子树做准备
temp = temp->left;
}
// 如果当前节点为空,且栈不为空,则从栈中弹出一个节点继续遍历
temp = Pop(stk);
// 将temp指针指向其右子节点,以便访问右子树
temp = temp->right;
}
// 遍历结束后,打印换行符
printf("\n");
// 销毁栈,释放内存
destroyStack(stk);
}
- 中序遍历
思路与先序遍历相似,只是节点值的访问时机改变了,代码如下:
// 中序遍历二叉树的函数
void inOrderTraversal(BinTree *binTree) {
Stack *stk = createStack(MAXSIZE);
// 初始化临时变量temp,指向二叉树的根节点
BinTree *temp = binTree;
// 当temp不为空或者栈不为空时,继续遍历
while (temp != NULL || !isEmpty(stk)) {
// 当temp不为空时,进入内层循环
while (temp != NULL) {
// 将当前节点temp压入栈中
Push(stk, temp);
// 将temp指针指向其左子节点,为访问左子树做准备
temp = temp->left;
}
// 如果当前节点为空,且栈不为空,则从栈中弹出一个节点
temp = Pop(stk);
// 访问弹出的节点,打印其值
printf("%c ", temp->val);
// 将temp指针指向其右子节点,为访问右子树做准备
temp = temp->right;
}
// 遍历结束后,打印换行符
printf("\n");
// 销毁栈,释放内存
destroyStack(stk);
}
- 后序遍历
后序遍历不同于前两种遍历方式,不论是先序遍历还是中序遍历,中间节点都是只访问一次,而对于后序遍历需要两次访问,在这种情况下,可以使用两个堆栈来实现,可参见这篇文章 五分钟C语言数据结构 之 二叉树后序遍历(非递归很重要)使用一个堆栈 Stack 来存储当前结点,可以把它看作中间节点,首先将其弹出并放入 Stack_Tmp 中,之后检查该节点是否有左右节点,如果有则依次压入 Stack 中,然后重复上述操作。具体可见下面图解:
- 初始情况,均为空栈
- 将根结点 a 压入 Stack 中
- 而后将 a 弹出压入 Stack_Tmp 中,将 a 的左后孩子结点压入 Stack 中
- 重复上面操作,检查 c 的左右孩子是否存在,将 c 弹出后再压入 Stack_Tmp 中,将 c 的左右孩子压入 Stack 中…
- 最后将 Stack_Tmp 中的元素依次弹出就是后序遍历的输出
具体代码如下:
// 后序遍历二叉树的函数
void postOrderTraversal(BinTree *binTree) {
// 创建两个栈,stk用于暂存节点,stk_Bin用于输出
Stack *stk = createStack(MAXSIZE);
Stack *stk_Bin = createStack(MAXSIZE);
// 初始化临时变量temp,用于在遍历过程中保存节点
BinTree *temp = NULL;
// 将根节点压入stk
Push(stk, binTree);
// 当stk不为空时,执行循环
while (!isEmpty(stk)) {
// 弹出stk栈顶的节点,保存在temp中
temp = Pop(stk);
// 将temp压入stk_Bin,用于后续输出
Push(stk_Bin, temp);
// 如果temp有左子节点,将其压入stk
if (temp->left != NULL) {
Push(stk, temp->left);
}
// 如果temp有右子节点,将其压入stk
if (temp->right != NULL) {
Push(stk, temp->right);
}
}
// 当stk_Bin不为空时,循环弹出节点并打印
while (!isEmpty(stk_Bin)) {
printf("%c ", Pop(stk_Bin)->val);
}
// 打印换行符,表示遍历结束
printf("\n");
// 销毁两个栈,释放内存
destroyStack(stk);
destroyStack(stk_Bin);
}
- 层序遍历
思路与创建二叉树时层序生成的过程相似
// 层序遍历二叉树的函数
void levelOrderTraversal(BinTree *binTree) {
// 如果二叉树为空,则直接返回
if (binTree == NULL) {
return;
}
// 创建一个队列用于层序遍历
Queue *que = CreateQueue();
// 将根节点添加到队列中
AddQueue(que, binTree);
// 当队列不为空时,执行循环
while (!QueueIsEmpty(que)) {
// 从队列中删除并返回队列头部的节点
BinTree *temp = DeleteQueue(que);
// 访问并打印当前节点的值
printf("%c ", temp->val);
// 如果当前节点有左子节点,将其添加到队列中
if (temp->left != NULL) {
AddQueue(que, temp->left);
}
// 如果当前节点有右子节点,将其添加到队列中
if (temp->right != NULL) {
AddQueue(que, temp->right);
}
}
// 遍历结束后,打印换行符
printf("\n");
// 销毁队列,释放内存
DestroyQueue(que);
}
- 获取二叉树的高度
较为简单,不做解释
// 计算二叉树高度的函数
int getHeight(BinTree *binTree) {
// 如果二叉树为空,返回高度0
if (binTree == NULL) {
return 0;
}
// 计算左子树的高度
int lMax = getHeight(binTree->left);
// 计算右子树的高度
int rMax = getHeight(binTree->right);
// 返回左子树和右子树中较大的高度值加1(加1是因为要包括当前节点)
return (lMax > rMax ? lMax : rMax) + 1;
}
代码文件
如果需要代码文件,见本文顶处