C实现二叉树及相关操作(非递归)

前言

本文使用C语言利用非递归方法生成一棵二叉树以及实现遍历等操作,包括前序、中序、后序及层序遍历。

在实现的过程中需要使用到堆栈和队列,整体代码实现较为繁杂,若将其全部放入一个C程序文件,则会变得较为冗长,不利于程序代码的可读性,故我借助了 CLion 以大程序文件的形式进行二叉树及相关操作的实现,文章结尾处有我的代码文件。

思路

首先需要实现一个二叉树,后面的操作都是在这棵二叉树上做的。
我们采用层序生成二叉树,层序创建所用的节点输入序列是按照树的从左至右,从上到下的顺序形成的,空节点用一个特定的标志 nullNode 表示(以字符存储为例,可以定义空节点的值为 ‘-’ )。在构造二叉树的过程中,需要一个队列暂时存储各节点地址,具体过程如下:

  1. 输入第一个数据
  • 若为 nullNode,表示此树为空,直接将根指针置为 NULL 并返回,树构造完成。
  • 若不为 nullNode,则动态分配一块内存存储该值,并将其左、右子树值置为空,最后将该值放入队列中。
  1. 取出队列中的元素,建立其左、右子树
  • 执行出队操作,得到当前元素;
  • 先输入一个数据,若为 nullNode,则进行下一步;反之,为当前元素的左子树分配空间存储所输入的值,将左子树的左子树和右子树置为 NULL,最后将生成的节点入队;
  • 接着输入另一个数据,若为 nullNode,则进行回到上一步;反之,为当前元素的右子树分配空间存储所输入的值,将右子树的左子树和右子树置为 NULL。最后将生成节点入队。
  1. 重复第 2 步操作,直到队列为空,构造完成
    为了实现上述操作,我们必须先实现一个队列,代码如下:
  1. 创建二叉树
    首先为 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

思路如下图解:

  • 将第一个元素入队,作为根结点

1

  • 依次遍历数组,为出队元素的根结点添加左右孩子,并将左右孩子依次入队

2

  • 当遇到 ‘-’ 时,表示空节点,无左孩子

3

  • 添加右孩子,数组结束,退出循环,最后返回根结点

4

具体代码如下:

// 创建二叉树的函数
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;
}
  1. 二叉树的销毁
    这个函数首先检查传入的 binTree 指针是否为 NULL。如果不为 NULL,它递归地调用自身来销毁 binTree 的左子树和右子树。在左右子树都被销毁之后,使用 free 函数释放当前节点的内存。这个过程一直递归进行,直到所有的节点都被释放,从而完全销毁了二叉树。
// 销毁二叉树的函数
void destroyBinaryTree(BinTree *binTree) {
    if (binTree != NULL) {
        destroyBinaryTree(binTree->left);
        destroyBinaryTree(binTree->right);
        free(binTree);
    }
}
  1. 二叉树的遍历
    首先需要实现一个堆栈:

这个头文件定义了一个栈的数据结构 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)二叉树的思路是使用栈来模拟递归过程。先序遍历的顺序是先访问根节点,然后遍历左子树,最后遍历右子树。步骤如下:

  1. 初始化栈:创建一个空栈。

  2. 访问根节点:将根节点压入栈中。

  3. 循环遍历:当栈不为空时,执行以下操作:

    • 弹出栈顶元素,这是当前要访问的节点。
    • 访问该节点(例如,打印节点的值)。
    • 如果该节点有右子节点,将右子节点压入栈中(右子节点会后于左子节点被处理,因为栈是后进先出的)。
    • 如果该节点有左子节点,将左子节点压入栈中(左子节点会先于右子节点被处理)。
  4. 处理子节点:对于每个节点,重复步骤 3,直到所有子节点都被访问。

  5. 结束遍历:当栈为空时,遍历结束。

在这个过程中,栈帮助我们保持了递归过程中的调用顺序。每次从栈中弹出节点时,我们首先访问它,然后将其右子节点(如果有的话)压入栈中,这样右子节点会在左子节点之后被访问。左子节点也压入栈中,但由于栈的后进先出特性,它会在右子节点之后被处理。这样,我们就可以在不使用递归的情况下实现先序遍历。

以上图的树为例,使用栈来实现先序遍历的图解如下:

  • Print 表示先序遍历的结果,并初始化栈

5

  • a 开始遍历左子树,将元素打印并入栈

6

  • 当遍历到最左边的结点时,跳出循环,将栈顶元素弹出,做 temp = temp->right,开始右子树的入栈和输出

7

  • temp->right = NULL 时,不进行入栈操作,将栈顶元素弹出,进行该元素的右子树遍历

8

  • 重复操作,直到栈空为止

9

  • 遍历操作结束

10

具体代码实现如下:

// 前序遍历二叉树的函数
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 中,然后重复上述操作。具体可见下面图解:

  • 初始情况,均为空栈

11

  • 将根结点 a 压入 Stack

12

  • 而后将 a 弹出压入 Stack_Tmp 中,将 a 的左后孩子结点压入 Stack

13

  • 重复上面操作,检查 c 的左右孩子是否存在,将 c 弹出后再压入 Stack_Tmp 中,将 c 的左右孩子压入 Stack 中…

14

  • 最后将 Stack_Tmp 中的元素依次弹出就是后序遍历的输出

15

具体代码如下:

// 后序遍历二叉树的函数
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);
}
  1. 获取二叉树的高度

较为简单,不做解释

// 计算二叉树高度的函数
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;
}

代码文件

如果需要代码文件,见本文顶处

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二叉树的中序遍历可以用栈来实现非递归程序,具体实现步骤如下: 1. 创建一个栈,并将根节点入栈。 2. 当栈不为空时,执行以下操作: a. 取出栈顶元素,如果该元素有左子树,则将左子树入栈。 b. 如果该元素没有左子树或者左子树已经遍历完了,则输出该元素的值,并将右子树入栈。 3. 当所有节点都被遍历完了,中序遍历结束。 下面是该算法的C语言实现代码: ```c #include <stdio.h> #include <stdlib.h> // 二叉树的结构体定义 typedef struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; } TreeNode; // 栈的结构体定义 typedef struct StackNode { struct TreeNode* node; struct StackNode* next; } StackNode; // 创建一个空栈 StackNode* createStack() { return NULL; } // 判断栈是否为空 int isEmpty(StackNode* top) { return top == NULL; } // 元素入栈 void push(StackNode** top, TreeNode* node) { StackNode* new_node = (StackNode*)malloc(sizeof(StackNode)); new_node->node = node; new_node->next = *top; *top = new_node; } // 元素出栈 TreeNode* pop(StackNode** top) { if (isEmpty(*top)) { return NULL; } TreeNode* res = (*top)->node; StackNode* temp = *top; *top = (*top)->next; free(temp); return res; } // 中序遍历二叉树非递归实现 void inorderTraversal(TreeNode* root) { if (root == NULL) { return; } StackNode* stack = createStack(); while (root != NULL || !isEmpty(stack)) { while (root != NULL) { push(&stack, root); root = root->left; } root = pop(&stack); printf("%d ", root->val); root = root->right; } } // 主函数 int main() { // 创建二叉树 TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode)); root->val = 1; root->left = (TreeNode*)malloc(sizeof(TreeNode)); root->left->val = 2; root->left->left = NULL; root->left->right = NULL; root->right = (TreeNode*)malloc(sizeof(TreeNode)); root->right->val = 3; root->right->left = (TreeNode*)malloc(sizeof(TreeNode)); root->right->left->val = 4; root->right->left->left = NULL; root->right->left->right = NULL; root->right->right = (TreeNode*)malloc(sizeof(TreeNode)); root->right->right->val = 5; root->right->right->left = NULL; root->right->right->right = NULL; // 中序遍历二叉树 inorderTraversal(root); return 0; } ``` 这段代码中,我们定义了一个`StackNode`结构体表示栈中的节点,该结构体包含一个指向二叉树节点的指针`node`和一个指向下一个节点的指针`next`。我们还定义了一个`createStack()`函数用来创建一个空栈,一个`isEmpty()`函数判断栈是否为空,一个`push()`函数将元素入栈,一个`pop()`函数将元素出栈。最后,我们定义了一个`inorderTraversal()`函数用来实现中序遍历二叉树非递归程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值