原:https://blog.csdn.net/u011934885/article/details/52383704
栈和队列在遍历二叉树中的使用
二叉树的遍历分为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中。
重复过程2和3直到栈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遍历算法,我们将在下一篇算法博客介绍。将在线性算法的同时,实现常数内存。