文中提到的相关知识所在专栏:《数据结构与课程设计》
本文将会详细介绍链式存储二叉树的非递归遍历算法,共有三种,分别是前序、中序和后序。并用这些遍历算法加一些扩展来完成经典题目,例如求树高、逆序遍历等等。
简单回顾链式存储二叉树的递归遍历方法:
给出二叉树存储结构的代码:
typedef struct Node {
char val;
Node* lchild; // 左子树
Node* rchild; // 右子树
}BiNode,*BiTree; // 重命名
前序遍历算法访问结点的次序为:根、左、右。
递归代码写为:
void PreOrder(BiTree T) {
if (T) {
printf("%c", T->val);// 根
PreOrder(T->lchild);// 左
PreOrder(T->rchild);// 右
}
}
那类似的,中序的结点访问次序为:左、根、右。
代码表示为:
void InOrder(BiTree T) {
if (T) {
InOrder(T->lchild);// 左
printf("%c", T->val);// 根
InOrder(T->rchild);// 右
}
}
后序的结点访问次序为:左、右、根。
void PostOrder(BiTree T) {
if (T) {
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%c", T->val);
}
}
大家如果清楚函数调用过程就会知道二叉树的这种递归遍历使用了函数的工作栈
,栈内存放着函数的返回地址并为函数内的形式参数分配内存。所以对于非递归算法我们可以使用 栈 来操作。
使用非递归遍历需要借助栈,因此先定义栈的相关操作。
我这里使用链栈,给出代码定义:
typedef struct SNode {
BiNode* node; // 存放二叉树结点
SNode* next; // 指针域
}Stack,*TreeStack;
栈的特点是先入先出,在单链表中就对应着头插和头删,写成Push
和Pop
函数:
// 压栈
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
:
bool IsEmpty(TreeStack S) {
if (S == NULL) return true;
else return false;
}
void 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;
}
}
}
void 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;
}
}
}
算法思想:
后序遍历的访问顺序是 左右根,根结点最后访问,因此在结点出栈时要先判断右子树是否存在,若存在必须要先处理右子树。
其次要注意不要重复压栈右子树,因此可以设置辅助变量
来指向刚刚弹栈的结点。
算法代码:
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;
}
}
}
}
思路一:利用非递归的后序遍历算法,程序运行过程中栈的最大深度就是树高。
这是因为后序遍历只有在处理完任一结点的所有子树后才开始弹栈,因此可以设置一个变量来记录站内结点的数量,最大值即是该树的高度。
思路二:采用后序遍历的递归算法。
给出代码:
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;
}
递归算法的思考过程很考验对程序的理解,推荐大家动脑自行思考。