一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情。
二叉树后序遍历
📚 后序遍历(Postorder Traversal):访问根节点的操作发生在遍历其左右子树之后。
即,首先遍历左子树,然后遍历右子树,最后访问根结点。
/* 二叉树后序遍历 */
void PostOrder(BTNode* root) {
/* 首先判断根是否为空,为空就返回 */
if (root == NULL) {
printf("/ ");
return;
}
/* 走到这里说明不为空,根据后序遍历,先遍历左子树(利用递归) */
PostOrder(root->left);
/* 然后遍历右子树(利用递归) */
PostOrder(root->right);
/* 最后访问根节点 */
printf("%c ", root->data);
}
🔑 解读:
① 首先判断根是否为空,如果根为空,则返回。
② 如果跟不为空,这说明有数据。由于是后序遍历(Postorder),后序遍历是先遍历左子树,然后遍历右子树,最后访问根节点。
层序遍历
📚 层序遍历(Level Traversal):设二叉树的根节点所在的层数为1的情况下,从二叉树的根节点出发,首先访问第1层的树根节点,然后再从左到右访问第2层上的节点。接着是第3层的节点……以此类推,自上而下、从左向右地逐层访问树的节点。
❓ 该如何实现层序遍历呢?
💡 我们可以利用队列的性质来实现!
我们之前再讲过队列,这里你可以选择自己实现一个队列。如果不想实现就直接 CV 即可,因为我们这里重点要学的是层序遍历!
💬 Queue.h:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int QueueDataType;
typedef struct QueueNode {
struct QueueNode* next;
QueueDataType data;
} QueueNode;
typedef struct Queue {
QueueNode* pHead;
QueueNode* pTail;
} Queue;
void QueueInit(Queue* pQ); //队列初始化
void QueueDestroy(Queue* pQ); //销毁队列
bool QueueIfEmpty(Queue* pQ); //判断队列是否为空
void QueuePush(Queue* pQ, QueueDataType x); //入队
void QueuePop(Queue* pQ); //出队
QueueDataType QueueFront(Queue* pQ); //返回队头数据
QueueDataType QueueBack(Queue* pQ); //返回队尾数据
int QueueSize(Queue* pQ); //求队列大小
💬 Queue.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
/* 队列初始化 */
void QueueInit(Queue* pQ) {
assert(pQ);
pQ->pHead = pQ->pTail = NULL;
}
/* 销毁队列 */
void QueueDestroy(Queue* pQ) {
assert(pQ);
QueueNode* cur = pQ->pHead;
while (cur != NULL) {
QueueNode* cur_next = cur->next;
free(cur);
cur = cur_next;
}
pQ->pHead = pQ->pTail = NULL;
}
/* 判断队列是否为空 */
bool QueueIfEmpty(Queue* pQ) {
assert(pQ);
return pQ->pHead == NULL;
}
/* 入队 */
QueueNode* CreateNewNode(QueueDataType x) {
QueueNode* new_node = (QueueNode*)malloc(sizeof(QueueNode));
if (new_node == NULL) {
printf("malloc failed!\n");
exit(-1);
}
new_node->data = x;
new_node->next = NULL;
return new_node;
}
void QueuePush(Queue* pQ, QueueDataType x) {
assert(pQ);
QueueNode* new_node = CreateNewNode(x);
if (pQ->pHead == NULL) {
pQ->pHead = pQ->pTail = new_node;
}
else {
pQ->pTail->next = new_node;
pQ->pTail = new_node;
}
}
/* 出队 */
void QueuePop(Queue* pQ) {
assert(pQ);
assert(!QueueIfEmpty(pQ));
QueueNode* save_next = pQ->pHead->next;
free(pQ->pHead);
pQ->pHead = save_next;
if (pQ->pHead == NULL) {
pQ->pTail = NULL;
}
}
/* 返回队头数据 */
QueueDataType QueueFront(Queue* pQ) {
assert(pQ);
assert(!QueueIfEmpty(pQ));
return pQ->pHead->data;
}
/* 返回队尾数据 */
QueueDataType QueueBack(Queue* pQ) {
assert(pQ);
assert(!QueueIfEmpty(pQ));
return pQ->pTail->data;
}
/* 求队列大小 */
int QueueSize(Queue* pQ) {
assert(pQ);
int count = 0;
QueueNode* cur = pQ->pHead;
while (cur != NULL) {
count++;
cur = cur->next;
}
return count;
}
这里为了方便讲解 #include "展开" 的特点,我们把树的定义放到 test.c 中,并且在 test.c 里完成层序遍历。
💬 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
typedef char BTDataType;
typedef struct BinaryTreeNode {
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
} BTNode;
//#include "Queue.h" 解决方案?
/* 创建新节点 */
BTNode* BuyNode(BTDataType x) {
BTNode* new_node = (BTNode*)malloc(sizeof(BTNode));
if (new_node == NULL) {
printf("malloc failed!\n");
exit(-1);
}
new_node->data = x;
new_node->left = new_node->right = NULL;
return new_node;
}
/* 手动创建二叉树 */
BTNode* CreateBinaryTree() {
BTNode* nodeA = BuyNode('A');
BTNode* nodeB = BuyNode('B');
BTNode* nodeC = BuyNode('C');
BTNode* nodeD = BuyNode('D');
BTNode* nodeE = BuyNode('E');
BTNode* nodeF = BuyNode('F');
nodeA->left = nodeB;
nodeA->right = nodeC;
nodeB->left = nodeD;
nodeC->left = nodeE;
nodeC->right = nodeF;
return nodeA;
}
由于是我们的数据类型是 BTNode,我们需要修改一下 Queue.h 的 QueueDataType,我们之前一直强调的 typedef 的好处,这里就显现出来了。我们只需要把 int 改成 BTNode 就可以了,而不需要改很多地方。
💬 Queue.h:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef BTNode* QueueDataType;
typedef struct QueueNode {
struct QueueNode* next;
QueueDataType data;
} QueueNode;
typedef struct Queue {
QueueNode* pHead;
QueueNode* pTail;
} Queue;
void QueueInit(Queue* pQ); //队列初始化
void QueueDestroy(Queue* pQ); //销毁队列
bool QueueIfEmpty(Queue* pQ); //判断队列是否为空
void QueuePush(Queue* pQ, QueueDataType x); //入队
void QueuePop(Queue* pQ); //出队
QueueDataType QueueFront(Queue* pQ); //返回队头数据
QueueDataType QueueBack(Queue* pQ); //返回队尾数据
int QueueSize(Queue* pQ); //求队列大小
这时我们运行一下代码会出现一个问题,我们发现它报错了:
它说,缺少 " { " ,这明显是胡说八道的,咱编译器也没有那么只能,毕竟是也不是VS2077。
❓ 这里产生问题的原因是什么呢?
💡 编译器原则:编译器认识 int,因为 int 是一个内置类型。但是 BTNode* 编译器并不认识,就需要 "往上面" 去找这个类型。这里显然往上找,是找不到它的定义的,所以编译器会报错。
如果你要用这个类型,你就需要先定义这个类型。test.c文件中 #include "Queue.h" ,相当于把这里的代码拷贝过去了。这时,由于BTNode*会在上面展开,导致找不到 BTNode* 。
❓ 我把 #include 移到 定义类型的代码 的后面,可以解决问题吗?
可以!遗憾的是只能解决这里 typedef BTNode* 的问题,还有 Queue.c 里的问题……
那我们该怎么做,能彻底解决呢?
🔑 解决方案:前置声明。 这样就不会带来问题了,满足了先声明后使用。
💬 Queue.h (修改后):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
// 前置声明
struct BinaryTreeNode;
typedef struct BinaryTreeNode* QueueDataType;
typedef struct QueueNode {
struct QueueNode* next;
QueueDataType data;
} QueueNode;
typedef struct Queue {
QueueNode* pHead;
QueueNode* pTail;
} Queue;
void QueueInit(Queue* pQ); //队列初始化
void QueueDestroy(Queue* pQ); //销毁队列
bool QueueIfEmpty(Queue* pQ); //判断队列是否为空
void QueuePush(Queue* pQ, QueueDataType x); //入队
void QueuePop(Queue* pQ); //出队
QueueDataType QueueFront(Queue* pQ); //返回队头数据
QueueDataType QueueBack(Queue* pQ); //返回队尾数据
int QueueSize(Queue* pQ); //求队列大小
似乎有些扯远了,这块在《维生素C语言》中没有详细的说,所以这里还是需要说一下的。解决了这个报错的问题后,我们就可以正式开始写层序遍历了。
💡 思路如下:
① 让根节点先入队。
② 记录当前队头后打印,并让队头出队,然后检测,如过孩子不为空就把孩子带进去。
(上一层节点出队时带入下一层节点入队)
③ 只要队列不为空就说明还没完。如果队列为空,说明下面最后一层没有节点,遍历结束。
📌 注意事项:使用完队列后别忘了要销毁!
💬 代码实现:
void BinaryTreeLevelOrder(BTNode* root) {
if (root == NULL) { // 判断根是否为空
return;
}
Queue pQ; // 建立队列
QueueInit(&pQ); // 初始化队列
QueuePush(&pQ, root); // 先让根进入队列
while (!QueueIfEmpty(&pQ)) { // 只要队列内还有元素,就进入循环
BTNode* front = QueueFront(&pQ); // 记录当前对头数据
printf("%c ", front->data); // 打印队头数据
QueuePop(&pQ); // 当队头出队
if (front->left != NULL) { // 如果队头元素的左子树不为空
QueuePush(&pQ, front->left); // 让左子树进入队列
}
if (front->right != NULL) { // 如果对头元素的右子树不为空
QueuePush(&pQ, front->right); // 让右子树进入队列
}
}
QueueDestroy(&pQ); // 销毁队列
}
🔑 解读:
① 首先判断根是否为空,如果为空就没有必要往下走了。
② 建立和初始化队列后,首先让根节点进入队列。只要队列内还有元素存在(说明还没遍历完)就进入循环。每次循环进入后都记录一下当前队头,这里使用 QueueFront 取队头数据即可。之后打印对头的数据。
③ 打印完后让队头出队,随后判断它的左子树和右子树,如果不为空就允许它们进队。我们先判断 left,再判断 right,这样就可以做到一层一层从左往右走的效果了。
④ 最后使用完队列后,别忘了销毁队列!
参考资料:
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
百度百科[EB/OL]. []. baike.baidu.com/.
📌 笔者:王亦优
📃 更新: 2022.1.12
❌ 勘误: 无
📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
本篇完。