二叉树的前序建立递归算法以及前中后序遍历的递归算法已经是人尽皆知了,递归算法也确实为代码的编写带来了很大的方便。然而,有时我们也确实需要它们的非递归算法。将递归算法转化为非递归算法可以帮助我们深入了解函数的调用与栈的原理。这里总结一下二叉树的这些重要的非递归算法。
一、前序建树
前序建树的基本思路是,接收用户输入的一组字符串,其中‘#’代表空树,其他代表树结点的数据域值。例如,要建立如下一棵树
需要输入“AB#D##C##”。
而非递归的思路是,1、设一个标志位来判断当前创建的结点是左孩子还是右孩子。2、用一个栈记录已经创建好的结点,当一个结点创建完成后,压栈,目的是以后弹栈创建其左右孩子。3、第一步将根节点压栈,然后循环。当栈空的时候,说明该树已经创建完成。4、如果当前遍历到得数据是#,那么要做的事有两件。一方面,如果当前标志位指示的是正在创建左孩子,那么将其改为创建右孩子。另一方面,弹栈让指针指向栈顶元素,准备创建其右孩子。换句话说,当#出现是,要么说明建树完成,要么创建右孩子。
具体代码如下
Status PreCreateBiTree(BiTree *T)
{
Stack S;
BiTNode *p;
char buff[MAX_BUFF_SIZE];
fgets(buff, sizeof(buff), stdin);
InitStack(&S);
char *buffPtr = buff;
if (*buffPtr == '#') {
return NO_TREE;
}
*T = (BiTree)malloc(sizeof(BiTNode));
if (!*T) {
exit(OVERFLOW);
}
p = *T;
(*T)->data = *buffPtr;
buffPtr++;
Push(&S, *T);
BOOL isCreatingRight = FALSE;
for (int i = 0; (*buffPtr); i++) {
if (*buffPtr != '#') {
//如果当前结点不为空
//如果创建的是左孩子,入栈
if (isCreatingRight == FALSE) {
BiTNode *newNode = (BiTree)malloc(sizeof(BiTNode));
newNode->data = *buffPtr;
p->lchild = newNode;
p = newNode;
Push(&S, newNode);
} else {
//此时创建的是右孩子,入栈,同时置flag为FALSE
BiTNode *newNode = (BiTree)malloc(sizeof(BiTNode));
newNode->data = *buffPtr;
p->rchild = newNode;
p = newNode;
Push(&S, newNode);
isCreatingRight = FALSE;
}
} else {
//如果当前结点为空,弹栈
//如果正在创建的是左孩子,弹栈创建右孩子
if (isCreatingRight == FALSE) {
isCreatingRight = TRUE;
}
//如果该树已经创建完成,break出去
if (StackEmpty(S)) {
break;
} else {
//让p指向栈顶元素,准备创建其右孩子
Pop(&S, &p);
}
}
buffPtr++;
}
return OK;
}
二、前序遍历
思路:前序遍历的特点是,先遍历树根,然后左孩子,在之后右孩子。所以我们可以让一个指针p指向树根,输出树根信息,【(分析)然后指向其左孩子,输出,再指向左孩子输出……直到指空,如果这样做,怎么找到右孩子呢?】为了保证每次遍历左孩子的同时不与右孩子“失去联系”,可以将根节点压栈,当需要遍历其右孩子时,弹栈指向其右孩子即可。
具体代码:
Status PreOrderTraverse(BiTree T)
{
Stack S;
InitStack(&S);
BiTree p = T;
while (p != NULL || !StackEmpty(S)) {
if (p != NULL) {
printf("%c", p->data); //先遍历根节点
Push(&S, p);
p = p->lchild; //访问左子树
} else { //当前结点为空,则转到右子树
Pop(&S, &p);
p = p->rchild;
}
}
printf("\n");
return OK;
}
思路:与前序基本类似。中序的特点是先左子树,然后树根,最后右子树。因此我们需要先找到最左孩子,同样,为了不与右孩子失联,每次指向左孩子之前压栈即可。唯一需要注意的是当p指向NULL时表明找到最左了,应该输出了。这时先弹栈再输出,就是因为如果不弹栈,因为p当前指的是NULL;
具体代码:
Status InOrderTraverse(BiTree T)
{
Stack S;
InitStack(&S);
BiTree p = T;
while (p != NULL || !StackEmpty(S)) {
if (p != NULL) {
Push(&S, p);
p = p->lchild; //找到最左结点
} else { //当左子树为空
Pop(&S, &p);
printf("%c", p->data); //输出当前结点,代表先输出左侧结点
p = p->rchild; //然后转去右子树
}
}
printf("\n");
return OK;
}
三、后序遍历
思路:最麻烦的就是后序遍历。为什么麻烦呢,后序遍历的特点是先左子树,然后右子树,然后树根。我们需要通过树根找到左子树,然后同时需要树根找到右子树。换句话说,我们将会遇到两次弹栈到树根的情况,分别是从左孩子和右孩子回来。因此我们需要一个标志位表明到底是从哪个孩子回来的。如果是左孩子,那么树根的任务还没完,还要靠它找右孩子,所以重新压栈,如果是右孩子,就可以输出树根了。注意最后输出树根后,让p指向NULL,目的是下次循环时弹栈从而继续访问p的上级根节点。
这里标志位不像建树那样简单,我们采用的思路是,设立两个栈,一个是正常的树结点的栈,另一个表示左右孩子标志位的栈。两栈的操作同步,即,当左孩子入栈是,标志位栈中也压入一个标志左孩子的结点。同理,树栈弹栈时,标志位栈也弹栈并判断是左孩子还是右孩子。
具体代码:
Status PostOrderTraverse(BiTree T)
{
//后序遍历时,分别从左子树和右子树两次回到根节点
//只有当从右子树回到根节点时才遍历根节点
//所以增加一栈标记到达结点的次序
Stack S, tag; //tag存储标志位
BiTree temp, left, right, p = T; //left和right是标志位,temp用于检测是从left来还是从right来
left = (BiTNode *)malloc(sizeof(BiTNode));
left->data = 1;
left->lchild = NULL;
left->rchild = NULL;
right = (BiTNode *)malloc(sizeof(BiTNode));
right->data = 2;
right->lchild = NULL;
right->rchild = NULL;
InitStack(&S);
InitStack(&tag); //标志位栈,从左子树返回时为1,从右子树返回时为2
while (p != NULL || !StackEmpty(S)) {
if (p != NULL) {
Push(&S, p);
Push(&tag, left); //第一次入栈,此时标志位压的是左孩子标志
p = p->lchild;
} else {
Pop(&S, &p);
Pop(&tag, &temp);
if (temp == left) {
//如果是从左子树返回,则第二次入栈,此时标志位压的是右孩子标志
Push(&S, p);
Push(&tag, right);
p = p->rchild;
} else {
//从右子树返回,直接访问根节点
printf("%c", p->data);
p = NULL; //目的是使下一次继续退栈,从而访问根节点
}
}
}
printf("\n");
return OK;
}