文中提到的相关知识所在专栏:《数据结构与课程设计》
前言
本文将会详细介绍链式存储二叉树的非递归遍历算法,共有三种,分别是前序、中序和后序。并用这些遍历算法加一些扩展来完成经典题目,例如求树高、逆序遍历等等。
目录
1、递归算法回顾
简单回顾链式存储二叉树的递归遍历方法:
给出二叉树存储结构的代码:
cpp
typedef struct Node {
char val;
Node* lchild; // 左子树
Node* rchild; // 右子树
}BiNode,*BiTree; // 重命名
前序遍历算法访问结点的次序为:根、左、右。
递归代码写为:
cpp
void PreOrder(BiTree T) {
if (T) {
printf("%c", T->val);// 根
PreOrder(T->lchild);// 左
PreOrder(T->rchild);// 右
}
}
那类似的,中序的结点访问次序为:左、根、右。
代码表示为:
cpp
void InOrder(BiTree T) {
if (T) {
InOrder(T->lchild);// 左
printf("%c", T->val);// 根
InOrder(T->rchild);// 右
}
}
后序的结点访问次序为:左、右、根。
cpp
void PostOrder(BiTree T) {
if (T) {
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%c", T->val);
}
}
大家如果清楚函数调用过程就会知道二叉树的这种递归遍历使用了函数的工作栈
,栈内存放着函数的返回地址并为函数内的形式参数分配内存。所以对于非递归算法我们可以使用 栈 来操作。
2、非递归遍历算法
使用非递归遍历需要借助栈,因此先定义栈的相关操作。
2.1、链栈的定义与相关操作
我这里使用链栈,给出代码定义:
cpp
typedef struct SNode {
BiNode* node; // 存放二叉树结点
SNode* next; // 指针域
}Stack,*TreeStack;
栈的特点是先入先出,在单链表中就对应着头插和头删,写成Push
和Pop
函数:
cpp
// 压栈
void Push(TreeStack& S,BiTree &T) {
Stack* pnew = new Stack(); // 创建结点
pnew->node = T; // 赋值
pnew->next = S; // 与下面一行结合为头插
S = pnew;
}
// 弹栈
void Pop(TreeStack& S, BiTree &T) {
TreeStack p = S; // 记录栈顶结点
T = p->node; // T记录栈顶结点并返回
S = S->next; // 栈顶结点指向相邻结点
free(p); // 删除栈顶结点
}
判空函数 IsEmpty
:
cpp
bool IsEmpty(TreeStack S) {
if (S == NULL) return true;
else return false;
}
2.2、非递归前序遍历
-
算法思想:
- 从根结点开始把二叉树的左子树全部访问并依次压栈,直到最左边子树为空
- 弹栈得到最左边的非空结点,对其右子树进行判断
- 若为空,继续弹栈,若不为空,对该结点的左子树判断
- 算法流程就是先访问根结点的左侧,等弹栈得到结点为根结点时,才会访问其右侧
-
算法代码:
cvoid NoRecursionPreOrder(BiTree T) { TreeStack S = NULL; BiTree p = T; // 结点非空或栈非空执行循环 while (p || !IsEmpty(S)) { if (p) { printf("%c", p->val); Push(S, p); p = p->lchild; } else { Pop(S, p); p = p->rchild; } } }
2.3、非递归中序遍历
-
算法思想:
- 中序遍历最先访问的是最左下方的结点,因此从根结点开始依次让左子树入栈
- 访问栈顶结点并出栈,判断其有无右子树
- 若无右子树,弹栈访问并继续判断右子树的存在情况
- 若有右子树,则入栈执行前面的步骤
- 代码与先序不同的仅是负责执行访问代码的位置
-
代码实现:
cvoid NoRecursionInOrder(BiTree T) { TreeStack S = NULL; BiTree p = T; // 结点非空或栈非空执行循环 while (p || !IsEmpty(S)) { if (p) { Push(S, p); p = p->lchild; } else { Pop(S, p); printf("%c", p->val); p = p->rchild; } } }
2.4、非递归后序遍历
算法思想:
后序遍历的访问顺序是 左右根,根结点最后访问,因此在结点出栈时要先判断右子树是否存在,若存在必须要先处理右子树。
其次要注意不要重复压栈右子树,因此可以设置辅助变量
来指向刚刚弹栈的结点。
算法代码:
cpp
void NoRecursionPostOrder(BiTree T) {
Stack* S = NULL;
BiNode* p = T, * r = NULL; // r 记录最近出栈的结点
while (p || !IsEmpty(S)) {
if (p) {
Push(S, p);
p = p->lchild;
}
else {
p = S->node; // p 取栈顶结点
if (p->rchild && p->rchild != NULL) {
p = p->rchild;
}
else {
Pop(S, p);
printf("%c", p->val); // 访问
r = p;
p = NULL;
}
}
}
}
3、求二叉树的高度
思路一:利用非递归的后序遍历算法,程序运行过程中栈的最大深度就是树高。
这是因为后序遍历只有在处理完任一结点的所有子树后才开始弹栈,因此可以设置一个变量来记录站内结点的数量,最大值即是该树的高度。
思路二:采用后序遍历的递归算法。
给出代码:
cpp
int getH(BiTree T) {
if (!T) return 0;
int m = getH(T->lchild);
int n = getH(T->rchild);
return m > n ? m + 1 : n + 1;
}
递归算法的思考过程很考验对程序的理解,推荐大家动脑自行思考。