再谈二叉树的遍历

前言

   以前在学习数据结构的时候曾经写过一些关于二叉树遍历的博客。最近在刷题的时候,对二叉树的遍历又有了一些新的解法和思路,这里做一总结,希望可以便人便己。

一、解法一:递归解法

   关于二叉树的遍历,最简单而又最直接的方式就是利用其本身的特点—即二叉树的左右子树同样是一棵二叉树。这是一个递归定义,所以非常适合采用递归的解法。思路很简单,这里就不赘述了,直接上代码。

 void PreOrderDfs(TreeNode * root){
        
        if(root != nullptr){
            visit(root);
            PreOrderDfs(root->left);
            PreOrderDfs(root->right);
        }
    }

    void InOrderDfs(TreeNode* root){
        
        if(root != nullptr){
            InOrderDfs(root->left);
            visit(root);
            InOrderDfs(root->right);
        }
    }

    void PostOrderDfs(TreeNode* root){
        
        if(root != nullptr){
            PostOrderDfs(root->left);
            PostOrderDfs(root->right);
            visit(root);
        }
    
    }

总结:随着对专业的不断了解,我现在越发的感觉虽然调优很重要,但是控制软件的复杂度同样很重要(尤其是在目前硬件的性能已经极大提高的情况下)。一个优但是复杂的算法和一个还不错(注意是普通不是劣)但是很简单的算法,情况允许的范围内我可能更偏向于后者。
递归对于二叉树来说算是很简单清晰了, 虽然效率上可能比后面介绍的非递归要低些(现在的编译器已经比较聪明了,对于递归来说,它可能会采取一些优化的编译算法,从编译的角度帮我们把递归优化成非递归) 如果实际项目允许的话,可以尝试使用它。

二、解法二:迭代法

2.1 先序和中序

   这是属于二叉树遍历的非递归算法,基本的思路就是使用栈来保存中间的结点(以防后面还能用得到)。

   下面解法的具体逻辑是(先序和中序为例,后序遍历要稍微复杂一点点):
(1)、从根结点出发,一直迭代访问其左子树(并不断压栈)
(2)、直到某个左子树结点为空,然后弹出栈顶元素,继续遍历其右子树
(3)、返回(1)…

在这里插入图片描述
在这里插入图片描述
   基本步骤如上面两幅图所示(只画了入栈、出栈的一部分)。

   而这里访问先序和中序遍历的区别就在于,具体visit的时机,对于先序遍历来说,其入栈的时候就需要visit 根元素(注意,这里的根元素指的不是整棵树的根,而是当前“所遍历的树”的根,理解这点很重要),而对于中序遍历来说,其出栈的时候需要才需要visit 对应树的根元素。 这个其实也挺好理解,因为先序遍历是NLR, 中序遍历是LNR, 后者在根N 所有的左子树都访问完才可以访问N(此时N正处于出栈状态)。

   先序遍历和中序遍历具体的代码如下所示:

    void PreOrderIter(){
    
        stack<TreeNode*> nodeSta;

        TreeNode* tmpRoot = root;
        while(tmpRoot != nullptr || nodeSta.empty() == false){
            
            if(tmpRoot != nullptr){
                
                visit(tmpRoot);
                nodeSta.push(tmpRoot);		//push from the stack

                tmpRoot = tmpRoot->left;
            }else{
                tmpRoot = nodeSta.top();	//get the top of the stack
                nodeSta.pop();
                tmpRoot = tmpRoot->right;
                    
            }
        

        }
        
    }
    
    void InOrderIter(){
    
        stack<TreeNode*> nodeSta;

        TreeNode* tmpRoot = root;

        while(tmpRoot!=nullptr || nodeSta.empty() == false){
            
            if(tmpRoot != nullptr){
                nodeSta.push(tmpRoot);
                tmpRoot = tmpRoot->left;
            }else{
                tmpRoot = nodeSta.top();
                visit(tmpRoot);
                nodeSta.pop();
                tmpRoot = tmpRoot->right;
            }
        }    
    }

2.2 后序

   关于后序其入栈的顺序和上述的类似,区别在于其出栈顺序。 对于后序遍历来说,不再是遇到nullptr就弹出一个栈顶元素,而是其左右子树都访问完毕之后,返回的时候弹出“栈顶”根元素。 而且,这里面有些饶人的点在于,如何判断它是从左子树返回、还是从右子树返回,只有从右子树返回才需要弹出栈顶“根元素”(因为是LRN)。

   判断是从左子树返回还是从右子树返回可以由不同的方式,在我看来比较简单的方式是标志法(给从左子树还是右子树返回打上不同的标志,这个和下面介绍的颜色标记法本质上是相同的)。不过这种方式需要改变二叉树结点的结构(添加一个标志位),在空间上需要一点损失。 具体的解法, 可以参照我以前写的博客非递归遍历后序二叉树

   这里介绍另外一种做法(比上面的方式要稍微复杂些),这种做法简单来说就是使用一个指针记录刚刚访问的结点,如果是左孩子结点,则继续访问根结点的右子树,如果是右节点,则访问根元素(并弹出栈)。代码示例如下:

void PostOrderIter(){
     
      stack<TreeNode*> nodeSta;
      
      TreeNode* tmpRoot = root;
      TreeNode* lastVisit = nullptr;		//record last visit node 

      while(tmpRoot != nullptr || nodeSta.empty() == false){
      
          if(tmpRoot != nullptr){
              
              nodeSta.push(tmpRoot);
              tmpRoot = tmpRoot->left;    //visit the left 

          }else {

              tmpRoot = nodeSta.top();  //get the top of stack
             
              if(tmpRoot->right!=nullptr && tmpRoot->right != lastVisit){    //right child not yet visit 
                  tmpRoot = tmpRoot->right;
              }else{
                  
                  visit(tmpRoot);
                  nodeSta.pop();  //pop from the stack
                  lastVisit = tmpRoot;
                  tmpRoot = nullptr;          //reset to null
              }   
          }
      }
  }

总结:对于二叉树遍历的非递归算法本质上大都是自己利用栈,来实现递归的逻辑。从某种程度上来说算是人工充当了“编译器”。

三、解法三:颜色标记法

   这第三种介绍的方法,以统一的代码形式进行上述三种遍历,其基本逻辑也是利用的栈来实现的非递归遍历。与前述不同的是, ,它借鉴了层次遍历和颜色标记这两种idea。

   对于后者颜色标记很好理解,就是把不同状态的结点打上不同的颜色,还未访问的标记成黑色,刚访问完的标记成灰色,需要弹出的标记成白色。

   对于借鉴前者层次遍历,这儿要稍微好好说道说道。 我们知道,层次遍历需要使用一个队列记录以后需要访问的元素。先入队的先访问,后入队的后访问,这种数据结构和层次遍历很好的契合在了一起,即先访问结点的孩子的访问时机,也是先于后访问结点孩子的。如下图所示,结点3的孩子4,一定是早于5结点的孩子6的。
在这里插入图片描述
   那么问题来了,先序(NLR)、中序(LNR)和后序(LRN)遍历有没有这类似的特点呢? 也可以让我们用某种数据结构记录以后访问的结点。

   有的,不过与层次遍历有些不同。因为NLR、LNR、LRN这三种遍历顺序都是递归的。什么叫做遍历顺序是递归的呢? 比如说对于NLR来说,根的左孩子一定要比根的右孩子要先访问,根子树左孩子的右孩子,一定要比根子树右孩子的右孩子要先访问… 也就是说,根的左子树要比根的右子树要先访问。

   不理解? 看看下图。
在这里插入图片描述
   对于根结点为1的结点,其左子树的任何一个结点都要比右子树的任何一个结点先访问。
在这里插入图片描述
   当我们以结点3为根结点的时候,其左子树的任何一个结点要比右子树的任何一个结点要先访问。

   以上就叫做递归的遍历。正是因为这个递归的遍历,我们无法使用队列的数据结构来记录其以后将要访问的结点,但是“栈”这个数据结构刚好可以契合这种递归的特点。

   既然(这里的“既然” 虽然说得如此义正言辞,但是说实话,其本质我还没有深刻的理解。 ̄□ ̄||)要使用“栈”这种数据结构来实现遍历,所以入栈的时候要做一些改变,因为栈是“后进先出”的形式,所以在入栈的时候需要一遍历顺序相反的形式入栈,即
(1)、对于先序遍历(NLR),那么入栈的时候就需要RLN。
(2)、对于中序遍历(LNR),那么入栈的时候就需要RNL。
(3)、对于后序遍历(LRN),那么入栈的时候就需要NRL。

   这样的话,栈中任意时刻保存的都是与目的遍历顺序相反的结点(出栈之后就是相同的)。当然有些还未访问的结点,需要把其理解成以此结点为根的树在栈中对应的位置。

   这里还需要注意两点:
(1)、每次栈顶元素都需要出栈,然后还需要根据其与左右子树的关系重新入栈,只有第二次访问遇到这个元素的时候,才可以出栈输出。
(2)、由于(1)的需求,这里使用颜色标记法来标识每个结点的不同状态,入栈时为黑色、第一次访问后为灰色,第二次访问后为白色,成为白色的结点可以出栈输出。

说实话,要深刻理解这种方式确实有些困难。下面来份示意图。
在这里插入图片描述
在这里插入图片描述

代码部分如下:

//traversal with Color mark
    void PostOrderCol(){
    
        stack<TreeNode*> nodeSta
        TreeNode* tmpRoot = nullptr;
        
        nodeSta.push(root);

        while(nodeSta.empty() == false){
        
            tmpRoot = nodeSta.top();
            nodeSta.pop();

            if(tmpRoot != nullptr){
                
                switch(tmpRoot->colTag){
                    case BLACK:
                        tmpRoot->colTag = GRAY;
                        break;

                    case GRAY:
                        tmpRoot->colTag = WHITE;
                        break;
                }

                if(tmpRoot->colTag == WHITE){
                    visit(tmpRoot);
                    
                }else{		//NRL
                    nodeSta.push(tmpRoot);
                    nodeSta.push(tmpRoot->right);
                    nodeSta.push(tmpRoot->left);
                }
            }
        }
    }

    void InOrderCol(){
    
        stack<TreeNode*> nodeSta;
        TreeNode* tmpRoot = nullptr;
        nodeSta.push(root);

        while(nodeSta.empty() == false){
        
            tmpRoot = nodeSta.top();
            nodeSta.pop();

            if(tmpRoot != nullptr){
                
                switch(tmpRoot->colTag){
                    case BLACK:
                        tmpRoot->colTag = GRAY;
                        break;

                    case GRAY:
                        tmpRoot->colTag = WHITE;
                        break;
                }

                if(tmpRoot->colTag == WHITE){
                    visit(tmpRoot);
                    
                }else{      //RNL
                 
                    nodeSta.push(tmpRoot->right); 
                    nodeSta.push(tmpRoot);
                    nodeSta.push(tmpRoot->left);
                }
            }
        }
  
    }

 void PreOrderCol(){
        stack<TreeNode*> nodeSta;
        TreeNode* tmpRoot = nullptr;
        nodeSta.push(root);

        while(nodeSta.empty() == false){
        
            tmpRoot = nodeSta.top();
            nodeSta.pop();

            if(tmpRoot != nullptr){
                
                switch(tmpRoot->colTag){
                    case BLACK:
                        tmpRoot->colTag = GRAY;
                        break;

                    case GRAY:
                        tmpRoot->colTag = WHITE;
                        break;
                }

                if(tmpRoot->colTag == WHITE){
                    visit(tmpRoot);
                    
                }else{		//RLN
                    nodeSta.push(tmpRoot->right);
                    nodeSta.push(tmpRoot->left);
                    nodeSta.push(tmpRoot);
                }
            }
        }
    }

总结:
颜色标记法也是非递归遍历,但是从某种程度上统一了三种非递归遍历的代码形式,虽然一开始不是很容易理解,但是便于记忆,是我比较推荐的方式。

四、总结

   在我看来,学习二叉树(或者说更一般的树结构),一定要建立起树的递归定义的概念。也就是说树的子树也是棵树。上文中在讨论二叉树时说到的根,并不一定是指整棵树的根,也有可能泛指任意一棵子树的根。 具体是哪一种,要看我们当前关注的重点是哪一棵树(确切的说是哪一棵子树)。

   理解了树的递归定义之后,对于LNR这样的遍历,就会有更深刻的理解,L是一棵子树,是一棵只针对于当前N(根结点)的左子树。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值