之前写的二叉树的非递归遍历,几乎都用了previsou指针和current指针,判断这两个指针的相对位置,决定此时的动作。
后来在算法导论的二叉搜索树章节中看到,其实有两种办法写非递归遍历。一种是用栈去模拟递归的情况,另一种则用判断两个指针是否相等来做。
我的显然是后者。虽然我也用到了栈,但那是因为我的二叉树节点没有父节点指针的属性。
如果用栈去模拟递归遍历的话,是不需要判断两个指针是否相等的。
下面依次分析前序、中序和后序遍历。
首先给出二叉树的节点定义。
typedef struct BNode
{
int data;
struct BNode *left;
struct BNode *right;
} BNode;
然后给出递归的前序遍历和中序遍历:
void preorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
cout << t->data << ends;
preorderRec(t->left);
preorderRec(t->right);
}
}
void inorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
inorderRec(t->left);
cout << t->data << ends;
inorderRec(t->right);
}
}
递归函数在形式上都非常简单。
下面讨论非递归实现前序和中序遍历。
一般来说,用非递归来模拟递归过程,都是用栈来模拟递归的函数栈来实现的。
下面给出前序和中序的非递归函数。虽然仔细看也能看懂,但后面接着会有一些问题和改进。
void preorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNode*> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(t);
cout << t->data << ends;
t = t->left;
}
t = stk.top();
stk.pop();
t = t->right;
}
}
}
void inorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNode*> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(t);
t = t->left;
}
t = stk.top();
cout << t->data << ends;
stk.pop();
t = t->right;
}
}
}
在前序和中序的非递归遍历函数中,入栈的情况是很好理解的,每次把左孩子入栈,直到左孩子为空,对应了递归调用中的preorder(t->left)或inorder(t->left)。
那么什么时候出栈,为什么会出栈?其实看上去没有那么直观。这个问题后面会讲。
现在不妨给出后序遍历的递归函数过程:
void postorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
postorderRec(t->left);
postorderRec(t->right);
cout << t->data << ends;
}
}
在这里,暂时看不出后序和前/中序的递归遍历有什么区别。
但是,要用栈去模拟后序遍历的递归调用过程,却是非常麻烦的。这是因为后序遍历的递归过程中包含了两次递归调用。
那么为什么前序和中序貌似很轻易就转换过来了呢?它们不也是两次递归调用吗?
实际上,仔细观察的话,前序和中序遍历有一个特点,就是输出语句的位置,一个在两次递归的前面,另一个在两次递归之间——它们的共性是,函数在递归返回时的最后一条语句是一个递归调用!
这种情况,就可以把递归调用进行尾递归优化,把两次递归调用改为一次递归,并将外面套上一层循环结构。
尾递归优化后的前/中序递归遍历函数:
void preorderRec(BNode *t)
{
while(t)
{
cout << t->data << ends;
preorderRec(t->left);
t = t->right;
}
}
void inorderRec(BNode *t)
{
while(t)
{
inorderRec(t->left);
cout << t->data << ends;
t = t->right;
}
}
这样一来,我们只需要用栈去模拟第一个递归调用就可以了。原本的第二个递归调用被优化成了循环。
所以,前/中序遍历的递归函数可以很容易转成用栈模拟的循环结构。
而后序遍历则要复杂一些。
我本想用两个栈去模拟两次递归调用,可即使用两个栈,那么在两个栈中还要考虑何时从第一个栈中弹出元素,何时从第二个栈中弹出元素,很麻烦,不一定能实现。
在网上看了看别人的代码(主要是自己懒),又自己稍加改动,写了下面的代码:
typedef struct BNodeTag
{
BNodeTag(BNode *t):node(t), isRightRec(false){}
BNode *node;
bool isRightRec;
}BNodeTag;
void postorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNodeTag> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(BNodeTag(t));
t = t->left;
}
if(false == stk.top().isRightRec) // 从左递归返回
{
stk.top().isRightRec = true;
t = stk.top().node->right;
}
else // 从右递归返回
{
cout << stk.top().node->data << ends;
stk.pop();
}
}
}
}
仍然用一个栈去模拟递归。
但因为有两次递归调用,所以重定义了栈的元素,使其多了一个标签符号,表示这次栈的弹出是从左递归返回还是右递归返回。
注意到在if(false == stk.top().isRightRec)的语句块中,实际上应该从栈中弹出栈顶,修改isRightRec元素,再压入栈。但由于效果相同,所以就直接修改了栈顶元素的属性。
另外,注意到,若进入了从右递归返回的else语句块,执行完后,下次循环时,是一定不会到while(t)的循环语句块中的。因为从右递归返回之后,表示当前节点的子节点已经遍历完毕,所以会不断返回,直到从左递归返回时,再次压栈,变量t才有了新的意义。
二叉树遍历充分说明了用栈去模拟递归的很强的技巧性。
之前一直想用非递归改写汉诺塔问题的递归算法,苦于汉诺塔中的两个递归调用想不清楚。经过二叉树遍历的分析之后,发现了尾递归优化来简化非递归改写的规律。有空的时候可以实现一下汉诺塔的非递归算法。
最后,给出本文代码的整体实现。
#include <iostream>
#include <stack>
using namespace std;
typedef struct BNode
{
int data;
struct BNode *left;
struct BNode *right;
} BNode;
void preorderRec(BNode *t)
{
while(t)
{
cout << t->data << ends;
preorderRec(t->left);
t = t->right;
}
}
/*
void preorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
cout << t->data << ends;
preorderRec(t->left);
preorderRec(t->right);
}
}*/
void preorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNode*> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(t);
cout << t->data << ends;
t = t->left;
}
t = stk.top();
stk.pop();
t = t->right;
}
}
}
void inorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
inorderRec(t->left);
cout << t->data << ends;
inorderRec(t->right);
}
}
/*void inorderRec(BNode *t)
{
while(t)
{
inorderRec(t->left);
cout << t->data << ends;
t = t->right;
}
}
*/
void inorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNode*> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(t);
t = t->left;
}
t = stk.top();
cout << t->data << ends;
stk.pop();
t = t->right;
}
}
}
void postorderRec(BNode *t)
{
if(!t)
{
return;
}
else
{
postorderRec(t->left);
postorderRec(t->right);
cout << t->data << ends;
}
}
typedef struct BNodeTag
{
BNodeTag(BNode *t):node(t), isRightRec(false){}
BNode *node;
bool isRightRec;
}BNodeTag;
void postorder(BNode *t)
{
if(!t)
{
return;
}
else
{
stack<BNodeTag> stk;
while(t || stk.size() != 0)
{
while(t)
{
stk.push(BNodeTag(t));
t = t->left;
}
if(false == stk.top().isRightRec) // 从左递归返回
{
stk.top().isRightRec = true;
t = stk.top().node->right;
}
else // 从右递归返回
{
cout << stk.top().node->data << ends;
stk.pop();
}
}
}
}
int main()
{
BNode a1 = {1, NULL, NULL};
BNode a2 = {2, NULL, NULL};
BNode a3 = {3, NULL, NULL};
BNode a4 = {4, NULL, NULL};
BNode a5 = {5, NULL, NULL};
BNode a6 = {6, NULL, NULL};
BNode a7 = {7, NULL, NULL};
a1.left = &a2;
a1.right = &a3;
a2.left = &a4;
a2.right = &a5;
a3.left = &a6;
a3.right = &a7;
preorderRec(&a1);
cout << endl;
preorder(&a1);
cout << endl;
cout << endl;
inorderRec(&a1);
cout << endl;
inorder(&a1);
cout << endl;
cout << endl;
postorderRec(&a1);
cout << endl;
postorder(&a1);
cout << endl;
return 0;
}