非递归遍历二叉树

来源:互联网 发布:软件测试技术提高 编辑:程序博客网 时间:2024/04/28 15:36

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)空间。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 淘宝申请退款又不想退了怎么办 淘宝申请退款后又不想退了怎么办 申请退款后如果不想退了怎么办 世纪明德申请退款但不想退了怎么办 天猫申请换货商家不换怎么办 乐视手机刷机不想清除数据怎么办 捡的苹果手机刷机了要id怎么办 苹果6plus玩王者荣耀卡怎么办 华为荣耀3c的手机内存不足怎么办 红米4x玩王者荣耀卡怎么办 华为手机荣耀10一直重启怎么办 荣耀9青春版老自动重启怎么办 手机开不开机停在华为界面怎么办 华为荣耀9老是反复的重启怎么办 华为荣耀4x老是反复的重启怎么办 手机更新系统后开不了机怎么办 荣耀畅玩7x没有4g网络怎么办 华为4x数字解锁不对中怎么办 华为手机需要解锁后才能刷机怎么办 畅玩6x锁屏壁纸黑了怎么办 指纹密码解锁的手机解不开了怎么办 客户说物流太慢了要退货怎么办 天猫买的手机商家不给发票怎么办 天猫超市下单付款后缺货怎么办 淘宝卖家填写假的单号不发货怎么办 天猫商家72小时未发货怎么办 天猫精灵方糖不按顺序播放怎么办 在天猫购物已付款不发货怎么办 淘宝退货商家收到货不退款怎么办 被有实名认证的闲鱼卖家骗了怎么办 我收到了方正的提示函怎么办 淘宝刷q币单被骗了怎么办 中通快递已签收但是东西丢了怎么办 手机不版本低不支持微信下载怎么办 淘宝虚拟商品不支持7天退货怎么办 卖虚拟物品遇到恶意退款买家怎么办 淘宝极速退款后卖家拒绝退款怎么办 我的天猫积分不让换券了怎么办 微信手机话费充错了怎么办 自己进货在淘宝卖被投诉假货怎么办 京东买的电器售后后服务差怎么办