栈和队列在遍历二叉树中的使用

标签: 二叉树 遍历 迭代
1533人阅读 评论(0) 收藏 举报
分类:

二叉树的遍历分为pre-order traverse(先序遍历),in-order traverse(中序遍历),post-order traverse(后序遍历),level-order traverse(层序遍历)。他们除了可以使用递归很方便的写出之外,也都可以借助堆栈或者队列迭代完成。前三种比较适合用栈实现,层序比较适合用队列实现。
下面基于算法层面分别介绍四种方法
(1)栈在前三种中的使用
就像三种顺序的名字一样,这三种遍历的区别在于对根节点的访问次序,是在左右节点之前,之中还是之后。我们使用栈的时候,可以在储存节点的同时,储存一个计数器,counter,(实现的时候可以自己建一个结构体或者类,存TreeNode加一个计数器)。

Each object placed on the stack contains:–A reference to a node;–A counterthat stores how many times the item has been popped from the stack (0 or 1 or 2).

pre-order:
After a node is popped, check its counter and do:
•If counter is 0:
–Increment counter to 1 and push back the node–Push the left child (if any) onto the stack (with counter=0). 
–Pop
•If counter is 1:
–Increment counter to 2 and push back the node
–Push the right child (if any) onto the stack (with counter=0). 
–Pop
•If counter is 2:
–Visit the node.
–Pop if stack is non-empty
•Repeat until stack becomes empty.
in-order:
•Start with an empty stack; push(root,0); pop
•After a node is popped, check its counter and do:
•If counter is 0:
-Increment counter to 1 and push back the node–Push the left child (if any) onto the stack (with counter=0). 
–Pop if stack is non-empty.
•If counter is 1:
–Visit the node.
–Push the right child (if any) onto the stack (with counter=0). 
–Pop if stack is non-empty
•Repeat until stack becomes empty.

先序遍历不需要计数器:

pre-order:
Start with an empty stack; push(root); pop.
•After a node is popped from the stack:
•Visit the node.
•Push the right child (if any) onto the stack. 
•Push the left child (if any) onto the stack. 
•Pop again if stack non-empty.
•Repeat until the stack is empty.

(2)队列实现层序遍历
使用队列来存储尚未被访问的节点。具体实现如下:

Start with an empty queue; enqueue(root); dequeue;
•After a node is dequeued do
•Visit node
•Enqueue left child (if any)
•Enqueue right child (if any)
•Dequeue again if queue is non-empty
•Repeat until the queue becomes empty

因为上面的方法十分的清晰,所以大家不需要源码也可以轻松将算法流程转化为实际代码,下面给出一些不太好说明的源码,教科书式的非递归遍历方法。
(1)先序遍历
先序遍历非递归访问,使用栈即可实现。先序遍历的非递归访问在所有的遍历中算是最简单的了。主要思想就是先将根结点压入栈,然后根结点出栈并访问根结点,而后依次将根结点的右孩子、左孩子入栈,直到栈为空为止。代码如下:

void preOrderIter(struct node *root)  
{  
    if (root == NULL) return;  
    stack<struct node *> s;  
    s.push(root);  
    while (!s.empty()) {  
        struct node *nd = s.top();  
        cout << nd->data << " ";  
        s.pop();  
        if (nd->right != NULL)  
            s.push(nd->right);  
        if (nd->left != NULL)  
            s.push(nd->left);  
    }  
    cout << endl;  
}  

或者也可以回溯操作,每次只push一个入栈,保留父节点,确保可以从左节点找到右节点。

void preOrderIter2(struct node *root)  
{  
    stack<struct node *> s;  
    while (root != NULL || !s.empty()) {  
        if (root != NULL) {  
            cout << root->data << " "; //访问结点并入栈  
            s.push(root);                
            root = root->left;         //访问左子树  
        } else {  
            root = s.top();            //回溯至父亲结点  
            s.pop();  
            root = root->right;        //访问右子树  
        }  
    }  
    cout << endl;  
}  

本算法有一个地方要注意的是,每次从栈中pop出结点时,表示该结点以及该的左子树已经访问完了,接下来访问其右子树。
(2)中序遍历
跟前序遍历很相似,就是访问的时机变了。

void inOrderIter(struct node *root)  
{  
    stack<struct node *> s;  
    while (root != NULL || !s.empty()) {  
        if (root != NULL) {  
            s.push(root);  
            root = root->left;  
        }  
        else {  
            root = s.top();  
            cout << root->data << " ";  //访问完左子树后才访问根结点  
            s.pop();  
            root = root->right;        //访问右子树  
        }  
    }  
    cout << endl;  
}  

(3)后序遍历
后序遍历的非递归算法较复杂,使用一个栈可以实现,但是过程很繁琐,这里可以巧妙的用两个栈来实现后序遍历的非递归算法。注意到后序遍历可以看作是下面遍历的逆过程:即先遍历某个结点,然后遍历其右孩子,然后遍历其左孩子。这个过程逆过来就是后序遍历。算法步骤如下:

Push根结点到第一个栈s中。
从第一个栈s中Pop出一个结点,并将其Push到第二个栈output中。
然后Push结点的左孩子和右孩子到第一个栈s中。
重复过程23直到栈s为空。
完成后,所有结点已经Push到栈output中,且按照后序遍历的顺序存放,直接全部Pop出来即是二叉树后序遍历结果。
void postOrderIter(struct node *root)  
{  
    if (!root) return;  
    stack<struct node*> s, output;  
    s.push(root);  
    while (!s.empty()) {  
        struct node *curr = s.top();  
        output.push(curr);  
        s.pop();  
        if (curr->left)  
            s.push(curr->left);  
        if (curr->right)  
            s.push(curr->right);  
    }  

    while (!output.empty()) {  
        cout << output.top()->data << " ";  
        output.pop();  
    }  
    cout << endl;  
}  

(4)层序遍历
如果不需要要求每一层分开打印或者储存,之前的算法很好实现,一个队列就可以。但如果要区分层,算法就要复杂些。
方法一:使用两个队列
第一个队列currentLevel用于存储当前层的结点,第二个队列nextLevel用于存储下一层的结点。当前层currentLevel为空时,表示这一层已经遍历完成,可以打印换行符了。
然后将第一个空的队列currentLevel与队列nextLevel交换,然后重复该过程直到结束。这个算法比较好理解。

void levelOrderIter(struct node* root)  
{  
    if (!root) return;  
    queue<struct node *> currentLevel, nextLevel;  
    currentLevel.push(root);  
    while (!currentLevel.empty()) {  
        struct node *currNode = currentLevel.front();  
        currentLevel.pop();  
        if (currNode) {  
            cout << currNode->data << " ";  
            nextLevel.push(currNode->left);  
            nextLevel.push(currNode->right);  
        }  
        if (currentLevel.empty()) {  
            cout << endl;  
            swap(currentLevel, nextLevel);  
        }  
    }  
}  

void swap(queue<struct node *> &curr, queue<struct node*> &next)  
{  
    while (!next.empty()) {  
        struct node *nd = next.front();  
        next.pop();  
        curr.push(nd);  
    }  
}  

方法二:使用一个队列
只使用一个队列的话,需要额外的两个变量来保存当前层结点数目以及下一层的结点数目。其实也挺好理解的,就是一边pop一边push,同时一边计数。

void levelOrderIter2(struct node *root)  
{  
    if (!root) return;  
    queue<struct node*> nodesQueue;  
    int nodesInCurrentLevel = 1;  
    int nodesInNextLevel = 0;  
    nodesQueue.push(root);  
    while (!nodesQueue.empty()) {  
        struct node *currNode = nodesQueue.front();  
        nodesQueue.pop();  
        nodesInCurrentLevel--;  
        if (currNode) {  
            cout << currNode->data << " ";  
            nodesQueue.push(currNode->left);  
            nodesQueue.push(currNode->right);  
            nodesInNextLevel += 2;  
        }  
        if (nodesInCurrentLevel == 0) {  
            cout << endl;  
            nodesInCurrentLevel = nodesInNextLevel;  
            nodesInNextLevel = 0;  
        }  
    }  
}  

以上的这些非递归操作都没有改动二叉树本身的性质和参数,虽然操作时间是线性,但我们使用了O(h)的内存,h是树高,递归算法也是一样,因为递归的本质是使用的函数调用栈,我们自己使用栈可以看做是在模拟这个过程。还有另外一套Morris遍历算法,我们将在下一篇算法博客介绍。将在线性算法的同时,实现常数内存。

查看评论

二叉树的三种遍历方式:递归、栈、循环

三种方法中,递归最为简单,栈次之,循环最为麻烦。递归的深度如果太大则会导致栈溢出;栈的方式需要额外的辅助空间;循环编程最麻烦。           首先是递归: //递归方法 void midPri...
  • Walker19900515
  • Walker19900515
  • 2015-06-21 09:47:55
  • 3607

使用栈,非递归先序遍历二叉树T

/********** 【题目】试利用栈及其基本操作写出二叉树T的非递归 的先序遍历算法。 二叉链表类型定义: typedef struct BiTNode { TElemType data; ...
  • qq1094417747
  • qq1094417747
  • 2016-10-16 12:12:26
  • 1859

利用栈实现二叉树的后序遍历算法

二叉树的后序遍历算法是按照左子树->右子树->节点的访问顺序来遍历二叉树的。所以需要将节点依次入栈,然后分别找到他们的左子树和右子树。而且在右子树没有被访问完之前,节点要一直在栈中。所以,当我们访问左...
  • W895972478
  • W895972478
  • 2015-08-06 12:37:09
  • 2511

利用栈后序遍历二叉树

#include using namespace std; #include #include #include /* malloc()等 */ #include /* INT_MAX等 */ #in...
  • lingadobe
  • lingadobe
  • 2016-11-18 15:46:58
  • 871

利用栈实现二叉树的先序,中序,后序遍历的非递归操作

[cpp] view plaincopy #include    #include    #include    #include    #include    ...
  • diaoguangqiang
  • diaoguangqiang
  • 2013-11-07 11:12:55
  • 2962

【二叉树】关于二叉树的后续遍历遍历以及栈

我们知道,二叉树的遍历当中,后序非递归的遍历i是一个非常guanjian
  • winghare
  • winghare
  • 2015-11-03 22:29:56
  • 902

使用栈的二叉树前序与中序遍历

使用栈的二叉树前序与中序遍历 使用栈的前序与后序遍历比较简单,只不过改变输出位置。但是原理是要清楚的。 template struct BiNode { T data; BiNode* lch...
  • qq_27802405
  • qq_27802405
  • 2016-03-15 10:29:23
  • 2394

二叉树遍历出栈入栈图

  • 2016年02月28日 15:30
  • 46KB
  • 下载

基于栈和队列实现二叉树的遍历

如何基于栈实现二叉树的后序遍历呢?通过观察可以发现 前序遍历的次序为root,left,right; 后序遍历的次序为left,right,root; 将前序遍历的left和right调换,在倒过来输...
  • JDLin
  • JDLin
  • 2015-12-24 16:30:28
  • 1968

不使用递归和栈遍历二叉树

二叉树遍历一共三种方式(暂且不把层次遍历算在内),前序,中序和后序,而每种遍历又分为递归和非递归版本。不管是递归还是非递归,都用到了栈。为什么要用栈?那是因为其他的方式没法记录当前节点的parent,...
  • LaoJiu_
  • LaoJiu_
  • 2016-03-07 16:09:36
  • 1558
    个人资料
    持之以恒
    等级:
    访问量: 3万+
    积分: 1819
    排名: 2万+
    文章分类
    最新评论