非递归遍历二叉树

 

在网上看了一些用非递归实现先序中序后序遍历二叉树的代码,都很混乱,while、if各种组合嵌套使用,逻辑十分不清晰,把我也搞懵了。想了大半天,写了大半天,突然开了窍

,实际上二叉树的这三种遍历在逻辑上是十分清晰的,所以才可以用递归实现的那么简洁。既然逻辑如此清晰,那么用非递归实现也应该是清晰的。

自认为自己的代码比网上搜到的那些都清晰得多,好理解得多。

稍微解释一下:

先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺

序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。

中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,再将右孩子入栈。如果栈非

空就重复上述过程直到栈空为止,结束算法。

后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过,则将其左孩子入栈,否则如果其右孩子未被访问过,则将其右孩子入栈,如果都已经访问过

,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

其实,这只不过是保证了先序中序后序三种遍历的定义。对于先序,保证任意一个节点先于其左右孩子被访问,还要保证其左孩子先于右孩子被访问。对于中序,保证任意一个节

点,其左孩子先于它被访问,右孩子晚于它被访问。对于后序,保证任意一个节点的左孩子右孩子都先于它被访问,其中左孩子先于右孩子被访问。如是而已。

代码里应该体现得比较清楚。这里不光给出了非递归版本,也给出了递归版本。

 

在网上看了一些用非递归实现先序中序后序遍历二叉树的代码,都很混乱,while、if各种组合嵌套使用,逻辑十分不清晰,把我也搞懵了。想了大半天,写了大半天,突然开了窍

,实际上二叉树的这三种遍历在逻辑上是十分清晰的,所以才可以用递归实现的那么简洁。既然逻辑如此清晰,那么用非递归实现也应该是清晰的。

自认为自己的代码比网上搜到的那些都清晰得多,好理解得多。

稍微解释一下:

先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺

序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。

中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,再将右孩子入栈。如果栈非

空就重复上述过程直到栈空为止,结束算法。

后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过,则将其左孩子入栈,否则如果其右孩子未被访问过,则将其右孩子入栈,如果都已经访问过

,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

其实,这只不过是保证了先序中序后序三种遍历的定义。对于先序,保证任意一个节点先于其左右孩子被访问,还要保证其左孩子先于右孩子被访问。对于中序,保证任意一个节

点,其左孩子先于它被访问,右孩子晚于它被访问。对于后序,保证任意一个节点的左孩子右孩子都先于它被访问,其中左孩子先于右孩子被访问。如是而已。

代码里应该体现得比较清楚。这里不光给出了非递归版本,也给出了递归版本。

#include <iostream>
#include <stack>
 
using namespace std;
 
struct TreeNode
{
 int data;
 TreeNode* left;
 TreeNode* right;
 int flag;
};
 
typedef TreeNode *TreePtr;
 
TreePtr CreateTree()
{
 TreePtr root = new TreeNode;
 cout<<"input the data :\n";
 int n;
 cin>>n;
 if (n == -1)
 {
  return NULL;
 }
 else
 {
  root->data = n;
  root->flag = 0;
  root->left = CreateTree();
  root->right = CreateTree();
 }
 return root;
}
 
void PreOrderRecursion(TreePtr p)
{
 if (p == NULL)
 {
  return;
 }
 cout<<p->data<<" ";
 PreOrderRecursion(p->left);
 PreOrderRecursion(p->right);
}
 
void InOrderRecursion(TreePtr p)
{
 if (p == NULL)
 {
  return;
 }
 InOrderRecursion(p->left);
 cout<<p->data<<" ";
 InOrderRecursion(p->right);
}
 
void PostOrderRecursion(TreePtr p)
{
 if (p == NULL)
 {
  return;
 }
 PostOrderRecursion(p->left);
 PostOrderRecursion(p->right);
 cout<<p->data<<" ";
}
 
void PreOrderNoRecursion(TreePtr p)
{
 cout<<"PreOrderNoRecursion\n";
 
 stack<TreeNode> stk;
 TreeNode t = *p;
 stk.push(t);
 
 while (!stk.empty())
 {
  t = stk.top();
  stk.pop();
  cout<<t.data<<" ";
 
  if (t.right != NULL)
  {
   stk.push((*t.right));
  }
 
  if (t.left != NULL)
  {
   stk.push((*t.left));
  }
 }
}
 
void InOrderNoRecursion(TreePtr p)
{
 cout<<"InOrderNoRecursion\n";
 
 stack<TreeNode> stk;
 TreeNode t = *p;
 stk.push(t);
 
 while (!stk.empty())
 {
  if (stk.top().flag == 0)
  {
   stk.top().flag++;
   if (stk.top().left != NULL)
   {
    stk.push(*(stk.top().left));
   }
  }
  else
  {
   t = stk.top();
   stk.pop();
   cout<<t.data<<" ";
   if (t.right != NULL)
   {
    stk.push(*(t.right));
   }
  }
 }
}
 
void PostOrderNoRecursion(TreePtr p)
{
 cout<<"PostOrderNoRecursion\n";
 
 stack<TreeNode> stk;
 TreeNode t = *p;
 stk.push(t);
 
 while (!stk.empty())
 {
  if (stk.top().flag == 0)
  {
   stk.top().flag++;
   if (stk.top().left != NULL)
   {
    stk.push(*(stk.top().left));
   }
  }
  else if (stk.top().flag == 1)
  {
   stk.top().flag++;
   if (stk.top().right != NULL)
   {
    stk.push(*(stk.top().right));
   }
  }
  else
  {
   cout<<stk.top().data<<" ";
   stk.pop();
  }
 }
}
 
int main()
{
 TreePtr t = CreateTree();
 
 cout<<"PreOrderRecursion\n";
 PreOrderRecursion(t);
 cout<<"\n";
 
 cout<<"InOrderRecursion\n";
 InOrderRecursion(t);
 cout<<"\n";
 
 cout<<"PostOrderRecursion\n";
 PostOrderRecursion(t);
 cout<<"\n";
 
 PreOrderNoRecursion(t);
 cout<<"\n";
 
 InOrderNoRecursion(t);
 cout<<"\n";
 
 PostOrderNoRecursion(t);
 cout<<"\n";
}

 

转自:http://www.sunhongfeng.com/2010/11/bintree_pre_in_pos/

另一篇

 

 1.先序遍历

从递归说起

  1. void preOrder(TNode* root)
  2. {
  3.     if (root != NULL)
  4.     {
  5.         Visit(root);
  6.         preOrder(root->left);
  7.         preOrder(root->right);
  8.     }
  9. }

递归算法非常的简单。先访问跟节点,然后访问左节点,再访问右节点。如果不用递归,那该怎么做呢?仔细看一下递归程序,就会发现,其实每次都是走树的左分支(left),直到左子树为空,然后开始从递归的最深处返回,然后开始恢复递归现场,访问右子树。

其实过程很简单:一直往左走 root->left->left->left...->null,由于是先序遍历,因此一遇到节点,便需要立即访问;由于一直走到最左边后,需要逐步返回到父节点访问右节点,因此必须有一个措施能够对节点序列回溯。有两个办法:
1.用栈记忆:在访问途中将依次遇到的节点保存下来。由于节点出现次序与恢复次序是反序的,因此是一个先进后出结构,需要用栈。
使用栈记忆的实现有两个版本。第一个版本是模拟递归的实现效果,跟LX讨论的,第二个版本是直接模拟递归。
2.节点增加指向父节点的指针:通过指向父节点的指针来回溯(后来发现还要需要增加一个访问标志,来指示节点是否已经被访问,不知道可不可以不用标志直接实现回溯?想了一下,如果不用这个标志位,回溯的过程会繁琐很多。暂时没有更好的办法。)

(还有其他办法可以回溯么?)
这3个算法伪代码如下,没有测试过。

先序遍历伪代码:非递归版本,用栈实现,版本1

  1. // 先序遍历伪代码:非递归版本,用栈实现,版本1
  2. void preOrder1(TNode* root)
  3. {
  4.     Stack S;
  5.     while ((root != NULL) || !S.empty())
  6.     {
  7.         if (root != NULL)
  8.         {
  9.             Visit(root);
  10.             S.push(root);       // 先序就体现在这里了,先访问,再入栈
  11.             root = root->left;  // 依次访问左子树
  12.         }
  13.         else
  14.         {
  15.             root = S.pop();     // 回溯至父亲节点
  16.             root = root->right;
  17.         }
  18.     }
  19. }

preOrder1每次都将遇到的节点压入栈,当左子树遍历完毕后才从栈中弹出最后一个访问的节点,访问其右子树。在同一层中,不可能同时有两个节点压入栈,因此栈的大小空间为O(h),h为二叉树高度。时间方面,每个节点都被压入栈一次,弹出栈一次,访问一次,复杂度为O(n)

 

 先序遍历伪代码:非递归版本,用栈实现,版本2
  1. // 先序遍历伪代码:非递归版本,用栈实现,版本2
  2. void preOrder2(TNode* root)
  3. {
  4.     if ( root != NULL)
  5.     {
  6.         Stack S;
  7.         S.push(root);
  8.         while (!S.empty())
  9.         {
  10.             TNode* node = S.pop(); 
  11.             Visit(node);          // 先访问根节点,然后根节点就无需入栈了
  12.             S.push(node->right);  // 先push的是右节点,再是左节点
  13.             S.push(node->left);
  14.         }
  15.     }
  16. }

preOrder2每次将节点压入栈,然后弹出,压右子树,再压入左子树,在遍历过程中,遍历序列的右节点依次被存入栈,左节点逐次被访问。同一时刻,栈中元素为m-1个右节点和1个最左节点,最高为h。所以空间也为O(h);每个节点同样被压栈一次,弹栈一次,访问一次,时间复杂度O(n)


先序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针

  1. // 先序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针
  2. void preOrder3(TNode* root)
  3. {
  4.     while ( root != NULL ) // 回溯到根节点时为NULL,退出
  5.     {
  6.         if( !root->bVisited )
  7.         {   // 判定是否已被访问
  8.             Visit(root);
  9.             root->bVisited = true;
  10.         }
  11.         if ( root->left != NULL && !root->left->bVisited )      // 访问左子树
  12.         {
  13.             root = root->left;
  14.         }
  15.         else if( root->right != NULL && !root->right->bVisited ) // 访问右子树
  16.         {
  17.             root = root->right;
  18.         }
  19.         else   // 回溯
  20.         {
  21.             root = root->parent;
  22.         }
  23.     }
  24. }

preOrder3的关键在于回溯。为了回溯增加指向父亲节点的指针,以及是否已经访问的标志位,对比preOrder1与preOrder2,但增加的空间复杂度为O(n)。时间方面,每个节点被访问一次。但是,当由叶子节点跳到下一个要访问的节点时,需要先回溯至父亲节点,再判断是否存在没有被访问过的右子树,如果没有,则继续回溯,直至找到一颗没有被访问过的右子树,这个过程需要很多的时间。每个叶子节点的回溯需要O(h)时间复杂度,叶子节点最多为(2^(h-1)),因此回溯花费的上限为O(h*(2^(h-1))。这个上限应该可以缩小。preOrder3唯一的好处是不需要额外的数据结构-栈。

 

2.中序遍历
根据上面的先序遍历,可以类似的构造出中序遍历的三种方式。仔细想一下,只有第一种方法改过来时最方便的。需要的改动仅仅调换一下节点访问的次序,先序是先访问,再入栈;而中序则是先入栈,弹栈后再访问。伪代码如下。时间复杂度与空间复杂度同先序一致。

  1. // 中序遍历伪代码:非递归版本,用栈实现,版本1
  2. void InOrder1(TNode* root)
  3. {
  4.     Stack S;
  5.     while ( root != NULL || !S.empty() )
  6.     {
  7.         while( root != NULL )   // 左子树入栈
  8.         {
  9.             S.push(root);
  10.             root = root->left;
  11.         }
  12.         if ( !S.empty() )
  13.         {
  14.             root = S.pop();
  15.             Visit(root->data);   // 访问根结点
  16.             root = root->right;  // 通过下一次循环实现右子树遍历
  17.         }
  18.     }
  19. }

第二个用栈的版本却并不乐观。preOrder2能够很好的执行的原因是,将左右节点压入栈后,根节点就再也用不着了;而中序和后序却不一样,左右节点入栈后,根节点后面还需要访问。因此三个节点都要入栈,而且入栈的先后顺序必须为:右节点,根节点,左节点。但是,当入栈以后,根节点与其左右子树的节点就分不清楚了。因此必须引入一个标志位,表示 是否已经将该节点的左右子树入栈了。每次入栈时,根节点标志位为true,左右子树标志位为false。
伪代码如下:

  1. // 中序遍历伪代码:非递归版本,用栈实现,版本2
  2. void InOrder2(TNode* root)
  3. {
  4.     Stack S;
  5.     if( root != NULL )
  6.     {
  7.         S.push(root);
  8.     }
  9.     while ( !S.empty() )
  10.     {
  11.         TNode* node = S.pop(); 
  12.         if ( node->bPushed )
  13.         {   // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
  14.             Visit(node);        
  15.         }
  16.         else
  17.         {   // 左右子树尚未入栈,则依次将 右节点,根节点,左节点 入栈
  18.             if ( node->right != NULL )
  19.             {
  20.                 node->right->bPushed = false// 左右子树均设置为false
  21.                 S.push(node->right);
  22.             }
  23.             node->bPushed = true;  // 根节点标志位为true
  24.             S.push(node);
  25.             if ( node->left != NULL )
  26.             {
  27.                 node->left->bPushed = false;
  28.                 S.push(node->left);
  29.             }
  30.         }
  31.     }
  32. }

对比先序遍历,这个算法需要额外的增加O(n)的标志位空间。另外,栈空间也扩大,因为每次压栈的时候都压入根节点与左右节点,因此栈空间为O(n)。时间复杂度方面,每个节点压栈两次,作为子节点压栈一次,作为根节点压栈一次,弹栈也是两次。因此无论从哪个方面讲,这个方法效率都不及InOrder1。

 

至于不用栈来实现中序遍历。头晕了,暂时不想了。后面再来完善。还有后序遍历,貌似更复杂。对了,还有个层序遍历。再写一篇吧。头都大了。

 

9.8续

中序遍历的第三个非递归版本:采用指向父节点的指针回溯。这个与先序遍历是非常类似的,不同之处在于,先序遍历只要一遇到节点,那么没有被访问那么立即访问,访问完毕后尝试向左走,如果左孩子补课访问,则尝试右边走,如果左右皆不可访问,则回溯;中序遍历是先尝试向左走,一直到左边不通后访问当前节点,然后尝试向右走,右边不通,则回溯。(这里不通的意思是:节点不为空,且没有被访问过)

  1. // 中序遍历伪代码:非递归版本,不用栈,增加指向父节点的指针
  2. void InOrder3(TNode* root)
  3. {
  4.     while ( root != NULL ) // 回溯到根节点时为NULL,退出
  5.     {
  6.         while ( root->left != NULL && !root->left->bVisited )
  7.         {                  // 沿左子树向下搜索当前子树尚未访问的最左节点           
  8.             root = root->left;
  9.         }
  10.         if ( !root->bVisited )
  11.         {                  // 访问尚未访问的最左节点
  12.             Visit(root);
  13.             root->bVisited=true;
  14.         }
  15.         if ( root->right != NULL && !root->right->bVisited )
  16.         {                  // 遍历当前节点的右子树  
  17.             root = root->right;
  18.         }
  19.         else
  20.         {                 // 回溯至父节点
  21.             root = root->parent;
  22.         }
  23.     }
  24. }

这个算法时间复杂度与空间复杂度与第3个先序遍历的版本是一样的。

 

3.后序遍历

从直觉上来说,后序遍历对比中序遍历难度要增大很多。因为中序遍历节点序列有一点的连续性,而后续遍历则感觉有一定的跳跃性。先左,再右,最后才中间节点;访问左子树后,需要跳转到右子树,右子树访问完毕了再回溯至根节点并访问之。这种序列的不连续造成实现前面先序与中序类似的第1个与第3个版本比较困难。但是按照第2个思想,直接来模拟递归还是非常容易的。如下:

  1. // 后序遍历伪代码:非递归版本,用栈实现
  2. void PostOrder(TNode* root)
  3. {
  4.     Stack S;
  5.     if( root != NULL )
  6.     {
  7.         S.push(root);
  8.     }
  9.     while ( !S.empty() )
  10.     {
  11.         TNode* node = S.pop(); 
  12.         if ( node->bPushed )
  13.         {   // 如果标识位为true,则表示其左右子树都已经入栈,那么现在就需要访问该节点了
  14.             Visit(node);        
  15.         }
  16.         else
  17.         {   // 左右子树尚未入栈,则依次将 右节点,左节点,根节点 入栈
  18.             if ( node->right != NULL )
  19.             {
  20.                 node->right->bPushed = false// 左右子树均设置为false
  21.                 S.push(node->right);
  22.             }
  23.             if ( node->left != NULL )
  24.             {
  25.                 node->left->bPushed = false;
  26.                 S.push(node->left);
  27.             }
  28.             node->bPushed = true;            // 根节点标志位为true
  29.             S.push(node);
  30.         }
  31.     }
  32. }

和中序遍历的第2个版本比较,仅仅只是把左孩子入栈和根节点入栈顺序调换一下;这种差别就跟递归版本的中序与后序一样。

 

4.层序遍历

这个很简单,就不说老。

  1. // 层序遍历伪代码:非递归版本,用队列完成
  2. void LevelOrder(TNode *root)
  3. {
  4.     Queue Q;
  5.     Q.push(root);
  6.     while (!Q.empty())
  7.     {
  8.         node = Q.front();        // 取出队首值并访问
  9.         Visit(node);
  10.         if (NULL != node->left)  // 左孩子入队
  11.         {          
  12.             Q.push(node->left);    
  13.         }
  14.         if (NULL != node->right) // 右孩子入队
  15.         {
  16.             Q.push(node->right);
  17.         }
  18.     }
  19. }

小结一下:

用栈来实现比增加指向父节点指针回溯更方便;

采用第一个思想,就是跟踪指针移动 用栈保存中间结果的实现方式,先序与中序难度一致,后序很困难。先序与中序只需要修改一下访问的位置即可。

采用第二个思想,直接用栈来模拟递归,先序非常简单;而中序与后序难度一致。先序简单是因为节点可以直接访问,访问完毕后无需记录。而中序与后序时,节点在弹栈后还不能立即访问,还需要等其他节点访问完毕后才能访问,因此节点需要设置标志位来判定,因此需要额外的O(n)空间。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值