如果不使用递归对二叉树进行深度遍历遍历,那么需要一个数据结构来记录我们之前走过的节点,通过对遍历过程的预演可以得到我们需要用栈来记录,因为走过的节点是先进先出.
所以我们需要构建一个链式栈和一棵二叉树.本文重点在于遍历的过程,栈和树的构建可以参考以前的文章.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//树的节点结构
typedef struct BTNode {
char data;
struct BTNode* l;
struct BTNode* r;
}BTNode,*BTree;
//链式栈
//栈的节点结构
typedef struct StackNode {
BTNode* data;
struct StackNode* next;
}sstack;
//初始化链式栈
sstack* InitStack() {
sstack* s = (sstack*)malloc(sizeof(sstack));
if (s == NULL) {
printf("内存申请失败\n");
return s;
}
s->next = NULL;
return s;
}
//入栈
void Push(sstack* s, BTNode* k) {
sstack* p = (sstack*)malloc(sizeof(sstack));
if (p == NULL) {
printf("内存申请失败\n");
return ;
}
p->data = k;
p->next = s->next;
s->next = p;
return ;
}
//判空
int Empty(sstack* s) {
if (s->next == NULL) {
return 1;
}
return 0;
}
//出栈
BTNode* Pop(sstack* s) {
if (Empty(s)) {
printf("栈空无法出栈\n");
return NULL;
}
sstack* p = s->next;
BTNode* k = p->data;
s->next = p->next;
free(p);
p = NULL;
return k;
}
//得到栈顶元素但不出栈
BTNode* Get(sstack* s) {
if (Empty(s)) {
printf("栈空无法获取\n");
return NULL;
}
BTNode* k = s->next->data;
return k;
}
//建树
BTree InitTree(char root) {
BTNode* rt = (BTNode*)malloc(sizeof(BTNode));
if (rt == NULL) {
printf("内存申请失败\n");
return rt;
}
rt->data = root;
rt->l = NULL;
rt->r = NULL;
return rt;
}
//查找函数
BTNode* Find(BTree root,char x) {
if (root->data == x) {
return root;
}
if (root->l != NULL) {
BTNode* ans = Find(root->l, x);
if (ans != NULL && ans->data == x) {
return ans;
}
}
if (root->r != NULL) {
BTNode* ans = Find(root->r, x);
if (ans != NULL && ans->data == x) {
return ans;
}
}
return NULL;
}
BTree Insert(BTNode* root, char x, char fx, int flag) {
BTNode* f = Find(root, fx);
BTNode* s = (BTNode*)malloc(sizeof(BTNode));
if (s == NULL) {
printf("内存申请失败\n");
return root;
}
s->data = x;
if (flag) {
f->r = s;
}
else {
f->l = s;
}
s->l = NULL;
s->r = NULL;
return root;
}
//遍历
//----------------------------------------------------
void Visit(BTNode* k) {
printf("%c ",k->data);
}
//先序遍历
/* 引入一个指针p和一个链式栈 p始终指向根节点
p非空, 说明当前要遍历以p为根的子树,先访问p,p入栈,p指向其左孩子p=p->l,继续循环
p为NULL,说明栈顶结点的左子树访问完了,栈顶出栈 p指向栈顶的右孩子,继续循环*/
void PreOrder(BTree root) {
if (root == NULL) {
printf("空树\n");
return;
}
BTNode* p = root;
//初始化一个链式栈
sstack* top = InitStack();
BTNode* f = NULL;
while (p != NULL || !Empty(top)) {
if (p != NULL) {
Visit(p);
Push(top, p);
p = p->l;
}
else {
f = Pop(top);
p = f->r;
}
}
}
//中序遍历
/* 引入一个指针p和一个链式栈 p始终指向根节点
p非空, 说明当前要遍历以p为根的子树,先访问p的左子树,p入栈,p指向其左孩子p=p->l,继续循环
p为NULL,说明栈顶结点的左子树访问完了,栈顶出栈 访问该结点, p指向栈顶的右孩子,继续循环*/
void InOrder(BTree root) {
if (root == NULL) {
printf("树空\n");
return;
}
BTNode* p = root;
sstack* top = InitStack();
BTNode* f = NULL;
while (p != NULL || !Empty(top)) {
if (p != NULL) {
Push(top,p);
p = p->l;
}
else {
f = Pop(top);
Visit(f);
p = f->r;
}
}
}
//后序遍历
/* 引入两个指针p和pre和一个链式栈 p始终指向根节点
p非空, 说明当前要遍历以p为根的子树,先访问p的左子树,p入栈,p指向其左孩子p=p->l,继续循环
p为NULL,说明栈顶结点的某棵子树访问完了,判断栈顶有没有右子树 若有再判断刚刚访问的是不是右子树*/
void PostOrder(BTree root) {
if (root == NULL) {
printf("树空\n");
return;
}
BTNode* p = root;
sstack* top = InitStack();
BTNode* pre = NULL;
BTNode* f = NULL;
while (p != NULL || !Empty(top)) {
if (p != NULL) {
Push(top, p);
p = p->l;
}
else {
f = Get(top);
if (f->r != NULL && pre != f->r) {
p = f->r;
}
else {
f = Pop(top);
Visit(f);
pre = f;
}
}
}
}
int main() {
int n;
int flag;//flag == 0左孩子 == 1右孩子
scanf("%d", &n);
getchar();
char x, fx;
scanf("%c", &x);
BTree root = InitTree(x);
for (int i = 2; i <= n; i++) {
getchar();
scanf("%c %c %d", &x, &fx, &flag);
Insert(root, x, fx, flag);
}
//先序遍历
PreOrder(root);
printf("\n");
//中序遍历
InOrder(root);
printf("\n");
//后序遍历
PostOrder(root);
printf("\n");
return 0;
}
/*
9
A
B A 0
E A 1
C B 1
D C 0
F E 1
G F 0
H G 0
K G 1
*/
二叉树的非递归遍历:核心要点总结
一、为什么需要非递归遍历
- 效率问题:递归调用有函数开销,频繁压栈出栈影响性能
- 栈溢出风险:树深度过大时,系统栈空间可能不足
- 解决方案:手动维护栈来模拟递归过程,完全控制压栈和出栈时机
二、统一框架思路
三种遍历都遵循同一个循环框架:
- 当前节点非空时:根据遍历规则决定是否访问,然后压栈,走向左孩子
- 当前节点为空时:从栈中弹出节点,根据规则决定是否访问,然后走向右孩子
三种遍历的本质区别仅在于:访问节点的时机不同。
三、三种遍历的核心区别
| 遍历方式 | 访问时机 | 难度 | 核心要点 |
|---|---|---|---|
| 先序遍历 | 入栈前 | ⭐ 简单 | 根 → 左 → 右 |
| 中序遍历 | 出栈时 | ⭐ 简单 | 左 → 根 → 右 |
| 后序遍历 | 出栈时(有条件) | ⭐⭐⭐ 较难 | 左 → 右 → 根,需记录上次访问节点 |
四、各遍历的关键点
先序遍历
- 当前节点非空时立即访问
- 访问完再入栈,然后向左走
- 逻辑最直观,最容易理解
中序遍历
- 入栈时不访问,继续向左走
- 出栈时才访问节点
- 与先序遍历仅差一行代码的位置
后序遍历
- 需要额外指针记录上一次访问的节点
- 出栈前必须判断:右子树是否已经被访问过
- 判断依据:上一次访问的节点是否为当前节点的右孩子
- 只有右子树为空或已访问,才能访问当前节点
五、为什么后序遍历最难
因为处理完左子树后,不能立即处理当前节点,必须先去处理右子树。从右子树返回时,需要知道右子树已经处理完毕。上一次访问指针就是用来解决这个问题的——如果上次访问的是当前节点的右孩子,说明右子树已经处理完了。
六、一句话总结
先序入栈前访问,中序出栈时访问,后序出栈前先问右子树。