关闭

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

标签: 二叉树遍历迭代
1150人阅读 评论(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遍历算法,我们将在下一篇算法博客介绍。将在线性算法的同时,实现常数内存。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:20744次
    • 积分:1708
    • 等级:
    • 排名:千里之外
    • 原创:146篇
    • 转载:4篇
    • 译文:1篇
    • 评论:2条
    文章分类
    最新评论