二叉树遍历算法

首先,个人认为,二叉树是很能体会递归算法思想的,因为二叉树的结构是leftTree->root<-rightTree,对于每个非叶子节点,该规律都适用,因此关于二叉树的很多算法也都能用递归思想搞定。递归的优点在于代码简洁,但效率却是问题。其次,对于各种顺序的遍历,又有着相应的非递归算法,非递归算法的代码量相对大一点,但更容易掌控,而且效率更优。

先看节点结构:

1 struct Bitree{
2   int val;
3   Bitree *left, *right;
4   Bitree(int x):val(x), left(nullptr), right(nullptr){
5   };
6 };

1. 中序遍历

  •  递归算法

显然,中序遍历的顺序为leftTree, root, rightTree,显然先遍历左子树,然后是根,最后右子树。中序遍历的递归算法自然也就出来了。

 1 void inOrderTraverse1(Bitree *root){
 3   if(root){
 5     inOrderTraverse1(root->left);
 7     visit(root);
 9     inOrderTraverse1(root->right);
11   }
13 }
  • 非递归算法1.

非递归算法的思想也比较简单,按从左到右进行访问。先指针往左探寻到底,然后观察最后一个非空节点是否有右节点,若有,将该右节点作为新的探寻起点,再进行下一轮的探寻。显然,“一探到底”的思路需要使用stack来帮助缓存之前的节点。

 1 void inOrderTraverse2(Bitree *root){
 2   stack<Bitree *> S;
 3   S.push(root);
 4   Bitree *p = root;
 5   while(!S.empty()){
 6     p = S.top();
 7     while(p){
 8       S.push(p->left);
 9       p = p->left;
10     }
11     S.pop();//pop out the nullptr
12     if(!S.empty()){
13       p = S.top();
14       visit(p);
15       S.pop();
16       S.push(p->right);//push its right child into the stack
17     }
18   }
19 }
  • 非递归算法2
 1 void inOrderTraverse3(Bitree *root){
 2   stack<Bitree *> S;
 3   Bitree *p = root;
 4   while(p || !S.empty()){
 5     if(p){
 6       S.push(p);
 7       p = p->left;
 8     }else{
 9       p = S.top();
10       visit(p);
11       S.pop();
12       p = p->right;
13     }
14   }
15 }

个人认为,虽然两种非递归算法的思路完全一样,但非递归算法2比非递归算法1代码要更为简洁,更值得推荐。

2. 前序遍历

  • 递归算法

前序遍历的顺序为root,leftTree,rightTree,直接上代码

1 void preOrderTraverse1(Bitree *root){
2   if(root){
3     visit(root);
4     preOrderTraverse1(root->left);
5     preOrderTraverse1(root->right);
6   }
7 }
  • 非递归算法1

对于前序遍历的非递归算法,和中序遍历的非递归算法非常相似,不过是在进栈时就访问该节点,而不是之后再访问。由于代码相似,先给出一种

 1 void preOrderTraverse2(Bitree *root){
 2   stack<Bitree *> S;
 3   Bitree *p = root;
 4   while(p || !S.empty()){
 5     if(p){
 6       visit(p);
 7       S.push(p);
 8       p = p->left;
 9     }else{
10       p = S.top();
11       S.pop();
12       p = p->right;
13     }
14   }
15 }
  •  非递归算法2

该算法采用了和前序遍历相同的思想,即root节点先进栈,root节点出栈时,将其右节点先进栈,然后是左节点进栈。这样,利用栈先进后出的性质,访问顺序自然变为了root,左子树,右子树。

 1 void preOrderTraverse3(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   stack<Bitree *> S;
 6   Bitree *p = root;
 7   S.push(root);
 8   while(!S.empty()){
 9     p = S.top();
10     visit(p);
11     S.pop();
12     if(p->right){
13       S.push(p->right);
14     }
15     if(p->left){
16       S.push(p->left);
17     }
18   }
19 }

3. 后续遍历

  • 递归算法

后续遍历的顺序是leftTree,rightTree和root,因此递归算法也自然出来了

1 void postOrderTraverse1(Bitree *root){
2   if(root){
3     postOrderTraverse1(root->left);
4     postOrderTraverse1(root->right);
5     visit(root);
6   }
7 }
  • 非递归算法1

和之前中序和前序算法不同,后续遍历的root节点要最后才能被访问,因此,我们若想访问某节点,那么我们需要知道该节点的右节点是否已经被访问过。只有该节点的右节点为null,或者已被访问过,那么该节点才能被访问;否则需要先将右节点访问完。为了判断该节点的右节点是否已经被访问过,需另外设一个记录指针last来指示已经访问过的节点,如果之前访问过的节点last恰为该节点的右节点,说明其右子树已经访问完,应该访问该节点。

 1 void postOrderTraverse2(Bitree *root){
 2   Bitree *last = nullptr;
 3   Bitree *p = root;
 4   stack<Bitree *> S;
 5   while(p || !S.empty()){
 6     while(p){
 7       S.push(p);
 8       p = p->left;
 9     }
10     p = S.top();
11     if(p->right && p->right != last){
12       p = p->right;
13     }else{
14       visit(p);
15       S.pop();
16       last = p;
17       p = nullptr;//p needs to be updated to null for next loop
18     }
19   }
20 }

tip 1:后续遍历中,root节点最后才能被访问到,因此,栈能记录每一个节点的路径,包括叶子节点。这一点性质可用于求解和树的路径有关的问题。

  •  非递归算法2

和前序遍历的非递归算法2一样,这里也给出后续遍历对应的非递归算法2,思路也是类似。由于后序遍历中,根节点要最后才能被访问到,不像前序遍历中刚访问到便可以输出。但在实际查找过程中,我们又只能先从根节点开始查找,才能接着查找左子树和右子树,由此可以再利用栈先进后出的特性来存储根节点。

 1 void postOrderTraverse3(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   Bitree *p = root;
 6   stack<Bitree *> S;
 7   stack<Bitree *> postOrder;
 8   S.push(p);
 9   while(!S.empty()){
10     p = S.top();
11     postOrder.push(p);
12     S.pop();
13     if(p->left){
14       S.push(p->left);//first IN, later OUT
15     }
16     if(p->right){
17       S.push(p->right);//later IN, first OUT
18     }
19   }
20   while(!postOrder.empty()){
21     p = postOrder.top();
22     visit(p);
23     postOrder.pop();
24   }
25 }

4. 层序遍历

层序遍历的思想和之前三种遍历方式不同,需要借助queue来对节点进行缓存,先进队列的节点需要先离开。这和图的BFS思想一样,毕竟树本质上也是一种特殊的图。

 1 void levelOrderTraverse(Bitree *root){
 2   if(!root){
 3     return;
 4   }
 5   queue<Bitree *> Q;
 6   Bitree *p = nullptr;
 7   Q.push(root);
 8   while(!Q.empty()){
 9     p = Q.front();
10     Q.pop();
11     visit(p);
12     if(p->left){
13       Q.push(p->left);
14     }
15     if(p->right){
16       Q.push(p->right);
17     }
18   }
19 }

Tip 2: 层序遍历思想简单,利用queue来一层一层输出。因此,可用于求解数的宽度和高度。

原文地址:https://www.cnblogs.com/wuiCoding/p/6670620.html