对于二叉树而言,有三种遍历方式:前序,中序,后序。由于树本身就是递归定义的,所以递归地去遍历二叉树,不仅容易理解,而且代码简洁。我们知道,递归与栈有关,那我们能否用栈来以非递归的方式实现二叉树的遍历呢?答案是肯定的,不过代码相较递归方式肯定会复杂一些。
递归是一个调用自身的特殊函数,需要编译器来维护一个栈。那么,我们先了解一下函数与栈的关系。
函数与栈
当程序执行到某个函数时,将这个函数进行入栈操作,在入栈之前,通常需要完成三件事。
1、将所有的实参、返回地址等信息传递给被调函数保存。
2、为被调函数的局部变量分配存储区。
3、将控制转移到被调函数入口。
当一个函数完成之后会进行出栈操作,出栈之前同样要完成三件事。
1、保存被调函数的计算结果。
2、释放被调函数的数据区。
3、依照被调函数保存的返回地址将控制转移到调用函数。
每当运行一个函数时,就在栈顶分配空间,函数退出后,释放这块空间。所以当前运行的函数一定在栈顶。
递归的过程其实是编译器帮我们处理了压栈和出栈的操作,转换为普通的函数就需要手动地处理压栈和出栈。
前序遍历
前序遍历是按照 根结点--左孩子--右右孩子的顺序进行遍历
1.递归实现
template <typename Object>void BinaryTree<Object>::preOrder(BinaryNode<Object> *t) {
if(t){
std::cout<<t->element<<" ";
preOrder(t->left);
preOrder(t->right);
}
}
2.非递归实现
前序遍历的访问顺序:优先访问根结点,然后分别访问其左孩子和右孩子。对于任一节点,我们均可将其看做根结点,所以对于每个节点都可以直接访问。访问完一个根结点之后,若其左孩子不为空,则可以按同样的规则去访问他的左子树。在访问其左子树的时候,再去访问他的右子树。
处理过程如下:
1.对于任意一个结点p,访问,并将右孩子其入栈,继续访问其左孩子。
2.判断左孩子是否为空,若为空,弹出栈顶元素并将其赋值给p,访问对应的右孩子。
3.若栈为空或者p为NULL,遍历结束。
template <typename Object>void BinaryTree<Object>::preOrder(BinaryNode<Object>*root){
stack<BinaryNode<Object>* >s;
BinaryNode<Object>*t=root;
while(!s.empty()||t){
if(t){
cout<<t->element<<" ";
if(t->right) s.push(t->right);
t=t->left;
}
else{
t=s.top();
s.pop();
}
}
}
可以有另外一种思路,对于任一节点p,优先访问当前节点,即保证p在其子节点之前被访问。再分别将右孩子和左孩子入栈,保证左孩子优先于右孩子被访问。
template <typename Object>void BinaryTree<Object>::preOrder(BinaryNode<Object>*root){
BinaryNode<Object>*t=NULL;
stack<BinaryNode<Object>*>s;
s.push(root);
while(!s.empty()){
t=s.top();
s.pop();
if(t){
cout<<t->element<<" ";
if(t->right)s.push(t->right);
if(t->left)s.push(t->left);
}
}
}
中序遍历
中序遍历是按照 左孩子--根结点--右孩子的顺序进行遍历
1.递归实现
template <typename Object>void BinaryTree<Object>::inOrder(BinaryNode<Object> *t) {
if(t){
inOrder(t->left);
cout<<t->element<<" ";
inOrder(t->right);
}
}
2.非递归实现
对于任一节点,中序遍历优先访问左孩子,而左孩子由可以看做是一个根结点,然后继续访问其左孩子,直道遇到左孩子为空的根结点。这时访问该节点,然后按相同的规则访问其右子树。
处理过程如下:1.对于任一节点p,若其左孩子不为空,将p入栈并将p赋值为其左孩子,然后继续对当前p节点进行相同的处理直到p的左孩子为空。
2.若p的左孩子为空,弹出栈顶元素并访问栈顶节点,然后将p赋值为栈顶节点的右孩子,按相同规则访问当前p的右子树。
3.若p为NULL或者栈空,遍历结束。
template <typename Object>void BinaryTree<Object>::inOrder(BinaryNode<Object>*root){
BinaryNode<Object>*t=root;
stack<BinaryNode<Object>*> s;
while(!s.empty()||t){
if(t){
s.push(t);
t=t->left;
}
else{
t=s.top();
s.pop();
cout<<t->element<<" ";
t=t->right;
}
}
}
后序遍历
后序遍历的顺序是 左子树--右子树--根结点
1.递归实现
template <typename Object>void BinaryTree<Object>::postOrder(BinaryNode<Object> *t) {
if(t){
postOrder(t->left);
postOrder(t->right);
cout<<t->element<<" ";
}
}
2.非递归实现
对于任一节点,后序遍历是根结点的访问要在其左孩子和右孩子之后。若节点p不存在左孩子和右孩子,则可以直接访问p;若节点p存在左孩子或者右孩子,但已经被访问过了,也可以直接访问p。如果不是这两种情况的话,分别将p的右孩子和左孩子入栈。这样就保证了每次左孩子在右孩子之前被访问,且左孩子和右孩子都在根结点之前被访问。
template <typename Object>void BinaryTree<Object>::postOrder(BinaryNode<Object>*root){
stack<BinaryNode<Object>*>s;
BinaryNode<Object>*t=NULL;
BinaryNode<Object>*pre=NULL;//前一次访问的节点
s.push(root);
while(!s.empty()){
t=s.top();//当前节点
//如果当前节点是叶子节点,或者其左右孩子都被访问过了
if((t->left==NULL&&t->right==NULL)||(pre&&(t->left==pre||t->right==pre))){
cout<<t->element<<" ";//访问该节点
s.pop();
pre=t;//将前一次访问过的节点设置为当前节点
}
//非上述情况,将右孩子和左孩子分别入栈
else{
if(t->right)s.push(t->right);
if(t->left)s.push(t->left);
}
}
}