学了这么久的二叉树,了解了一些二叉树的遍历方法,在这里总结一下。
大致有三类遍历方法吧
1.递归
2.通过栈来迭代,或通过队列来迭代
3.Morris Traversal
下面来介绍一下这三种方法
首先把树节点给定义一下
struct TreeNode{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x):val(x),left(NULL),right(NULL){}
TreeNode():val(0),left(NULL),right(NULL){}
};
刚刚来学习二叉树时,关于二叉树的遍历方法最早接触的便是递归了
void PreorderTraverse(TreeNode *root){
if(!root)
return;
cout<<root->val;
PreorderTraverse(root->left);
PreorderTraverse(root->right);
}
void InorderTraverse(TreeNode *root){
if(!root)
return;
InorderTraverse(root->left);
cout<<root->val;
InorderTraverse(root->right);
}
void PostorderTraverse(TreeNode *root){
if(!root)
return;
PostorderTraverse(root->left);
PostorderTraverse(root->right);
cout<<root->val;
}
这里二叉树的所有结点都需要访问一次,所以时间复杂度是O(n),同理由于递归需要开辟工作栈,所以空间的复杂度也为O(n)。
通过栈的方式与递归相同,只不过利用栈将递归转换为非递归而已。在这个转换的过程中主要的问题是在访问完子节点后怎样回到父节点。
例如在中序访问过程中,在访问完5这个结点后如何返回到1这个结点。
下面将对不同的遍历顺序进行分析
二叉树的前序遍历先访问本身节点的值,再访问左右子节点的值。
思路:可以把该节点储存到栈中,然后访问该节点,把该节点弹出,,如果左右节点不为空,再依次把右节点和左节点压入栈中,接下来重复上述操作,直到全部的节点被访问为止。
void PreorderTraverse(TreeNode *root){
if(!root)
return;
TreeNode *p;
stack<TreeNode*> stktre;
stktre.push(root);
while(!stktre.empty()){
p=stktre.top();
stktre.pop();//将根节点弹出
cout<<p->val;//访问根节点
if(root->right)//右子树不为空,就将右子树入栈
stktre.push(root->right);
if(root->left)//同理
stktre.push(root->left);
}
}
中序遍历先要先访问根的左节点,然后再是根节点,再是根的右节点。
思路:在遍历的过程中,一直要不断的将左节点压栈,直到某个节点的左子节点为空,就访问该节点,并将该节点弹出,如果该节点右子节点存在就将右子节点压栈,重复上述过程,如果不存在,就访问栈中的下一个节点,并重复上述过程。
void InorderTraverse(TreeNode *root){
TreeNode *p=root;
stack<TreeNode*> stktre;
while(!stktre.empty()||p){
while(p){
stktre.push(p);
p=p->left;
}
p=stktre.top();
stktre.pop();
cout<<p->val;
p=p->right;
}
}
后序遍历是先访问根节点的左右子节点再访问,最后再访问根节点的值。
思路:首先依然是不断的将左子节点压入栈内,直到某个节点的左子节点为空,然后判断该节点的右子节点是否为空,如果为空,则访问该节点,并弹出。如果不为空,则应先访问右子节点,所以应先把右字节点压入栈中,再重复上述操作。但是这里有个问题,就是如果该右子节点不为空时,我们将右子节点压入,并访问完弹出后,该根节点依然在栈内,就会导致陷入死循环,所以我们应该对这类节点做好标记,来表示不需要再进行右子节点入栈操作,而是直接访问,而对于标记操作,则可以用到哈希表了。
void Postorder(TreeNode *root){
TreeNode *p=root;
stack<TreeNode*> stktre;
map<TreeNode*,int> mymap;//用于标记节点
while(p||!stktre.empty()){
while(p){
stktre.push(p);
p=p->left;
}
p=stktre.top();
if(p->right&&!mymap.count(p)){
mymap[p]=1;
p=p->right;
}
else{
stktre.pop();
cout<<p->val;
p=NULL;
}
}
}
层序遍历也叫做BFS(宽度优先遍历),而上述三种,前中后序遍历也叫DFS(深度优先遍历);
与上面前中后序遍历不同的是,层序遍历采用的是队列作为辅助空间的,而且在算法思想上也是不一样的,BFS是从某一分支不断的深入直到到达叶子节点,再返回,从另一分支进入到达叶子节点,直到所有节点都被访问完全。层序遍历是按层次顺序,第一层访问完后,访问第二层,接着依次类推,直到访问完所有节点。
思路:可以利用队列的先进先出的特点,依次把第一层,第二层的放入队列中。那么如何一层一层的将树节点放入队列中呢?我们可以在访问某个节点时,将该节点弹出队列,并将该节点的左右子节点依次放入队列中即可。如下图:
void SequenceTraverse(TreeNode* root){
if(!root)
return;
TreeNode *p;
queue<TreeNode*> qtre;
qtre.push(root);
while(!qtre.empty()){
p=qtre.front();
qtre.pop();
cout<<p->val;
if(p->left)
qtre.push(p->left);
if(p->right)
qtre.push(p->right);
}
}
上面四种方法的时间与空间复杂度都为O(n),因为对每个节点都访问了一次。
Morris算法与前面几种较为不同,前面几种遍历在时间与空间的复杂度上都为O(n),而Morris算法致力于在空间复杂上为O(1),而时间复杂度上依然为O(n)。关于Morris Traversal这里有一篇写的特别优秀的文章,我就直接转载过来了 Morris Traversal。
最后欢迎大家留言讨论。