树 -- 二叉树遍历方法思路大总结(10种方法)

Posted Y_ZhiWen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树 -- 二叉树遍历方法思路大总结(10种方法)相关的知识,希望对你有一定的参考价值。

遍历是二叉树的一类重要操作,也是二叉树的其它操作和应用的算法基本框架

二叉树(Binary Tree)

  • 定义:含有n(n>=0)个结点的有限集合。当n=0时为空二叉树,
    在非空二叉树中:有且仅有一个根结点;其余节点划分为两互不相交的子集L和R,其中L和R也是一棵二叉树,分别称为左子树和右子树
  • 术语(部分)
    • 层次:根为第1层,根的孩子为第2层,依次计数
    • 深度(高度):最大层次称为高度
    • 度:结点的孩子个数
    • 内部结点(分支结点):非叶子结点
    • 叶子结点:度为0的结点
  • 满二叉树(Full Binary Tree):一棵深度为k且有 2k1 个结点的二叉树
  • 完全二叉树(Complete Binary Tree):深度为k且含有n个结点的二叉树,其每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应。
  • 性质:
    • 在非空二叉树的第i层最多有 2k1 个结点(i≥1)。 —- 可用数学归纳法证明
    • 深度为k的二叉树最多有 2k1 —- 基于上面性质,求等比数列和
    • 对于任意的一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0 = n2+1
    • 具有n个结点的完全二叉树的深度为「log2n」+1
    • 对于含n个结点的完全二叉树中的编号为i(i≤i≤n)的结点:
      • 如果i = 1,则该结点为数的根,没有双亲。否则其双亲为⌊i/2⌋
      • 如果2i>n,则i结点没有左孩子,否则其左孩子编号为2i
      • 如果2i+1>n,则i结点没有右孩子,否则其右孩子编号为2i+1

二叉树的存储结构

顺序存储结构

  • 采用数组形式存储,根据上面最后一条性质判定父子关系,容易造成空间浪费,例如:

链式存储结构

二叉链表

typedef struct BiTNode
    int data;                         // 数据域 
    struct BiTNode *lchild, *rchild;  // 左右孩子 
BiTNode, BiTree;

三叉链表

typedef struct BiTNode
    int data;                         // 数据域 
    struct BiTNode *lchild, *rchild, *parent;  // 左右孩子 ,及双亲
BiTNode, BiTree;

二叉树的遍历

遍历方式可分为:深度优先遍历,广度优先遍历。

深度优先遍历

而深度优先遍历又可分为:先序遍历、中序遍历、后序遍历。其中又可区分递归遍历跟非递归遍历。

递归遍历

如果L、D、R表示左子树、根、右子树,那么3种算法可表示(都是争对跟结点D而言的):
DLR:先序
LDR:中序
LRD:后序

这里只展示中序递归遍历:

// 中序递归遍历
void InOrderTraverse(BiTree T,Status (*visit)(int elem))
    if( T == NULL ) return ; 
    InOrderTraverse(T->lchild,visit);
    visit(T->data);
    InOrderTraverse(T->rchild,visit);

非递归遍历:

这里又可分为使用栈和不使用栈的情况

使用栈非递归遍历
  • 使用栈的中序非递归遍历

// 从T结点出发,沿左分支不断深入,直到左分支为空的结点,沿途结点入栈S 
BiTNode *GoFastLeft(BiTree T,LStack &S)

    if( NULL == T ) return NULL;
    while( T->lchild != NULL )
        Push(S,T);
        T = T->lchild;
    
    return T;


void InOrderTraverse(BiTree T, Status (*visit)(int elel))
    LStack s; InitStack(s);
    BiTree p;
    p = GoFastLeft(T,s);    // 找到最左下的结点 
    while(p!=NULL)
        visit(p->data); 

        if( p->rchild != NULL )            // 令p指向其右孩子为根的子树的最左下结点 
            p = GoFastLeft(p->rchild,s);    
        else if( !StackEmpty(s) )
            Pop(s,p);
        else
            p = NULL;
        
    
 

示意图:

  • 使用栈的先序非递归遍历
    根据示意图,可以改成先序非递归遍历,只是将访问语句改变一下位置,代码为:

// 从T结点出发,沿左分支不断深入,直到左分支为空的结点,沿途结点入栈S 
BiTNode *GoFastLeft(BiTree T,LStack &S,Status (*visit)(int elem))

    if( NULL == T ) return NULL;
    while( T->lchild != NULL )
        visit(T->data); // ...
        Push(S,T);
        T = T->lchild;
    
    visit(T->data);// ...
    return T;


void InOrderTraverse(BiTree T, Status (*visit)(int elem))
    LStack s; InitStack(s);
    BiTree p;
    p = GoFastLeft(T,s);    // 找到最左下的结点 
    while(p!=NULL)

        if( p->rchild != NULL )            // 令p指向其右孩子为根的子树的最左下结点 
            p = GoFastLeft(p->rchild,s,visit);  
        else if( !StackEmpty(s) )
            Pop(s,p);
        else
            p = NULL;
        
    
 
  • 使用栈的后序非递归遍历
    那么怎么改成后序呢??

二叉树的非递归后序遍历算法:
前序、中序、后序的非递归遍历中,要数后序最为麻烦,如果只在栈中保留指向结点的指针,那是不够的,必须有一些额外的信息存放在栈中。方法有很多,这里只举一种,先定义栈结点的数据结构。

typedef struct
    Node * p;
    int rvisited;
SNode //Node 是二叉树的结点结构,rvisited==1代表p所指向的结点的右结点已被访问过。

lastOrderTraverse(BiTree bt)
  //首先,从根节点开始,往左下方走,一直走到头,将路径上的每一个结点入栈。
  p = bt;
  while(bt)
    push(bt, 0); //push到栈中两个信息,一是结点指针,一是其右结点是否被访问过
    bt = bt.lchild;
  

  //然后进入循环体
  while(!Stack.empty()) //只要栈非空
    sn = Stack.getTop(); // sn是栈顶结点

    //注意,任意一个结点N,只要他有左孩子,则在N入栈之后,N的左孩子必然也跟着入栈了(这个体现在算法的后半部分),所以当我们拿到栈顶元素的时候,可以确信这个元素要么没有左孩子,要么其左孩子已经被访问过,所以此时我们就不关心它的左孩子了,我们只关心其右孩子。

    //若其右孩子已经被访问过,或是该元素没有右孩子,则由后序遍历的定义,此时可以visit这个结点了。
    if(!sn.p.rchild || sn.rvisited)
      p = pop();
      visit(p);
    
    else //若它的右孩子存在且rvisited为0,说明以前还没有动过它的右孩子,于是就去处理一下其右孩子。
     
      //此时我们要从其右孩子结点开始一直往左下方走,直至走到尽头,将这条路径上的所有结点都入栈。

      //当然,入栈之前要先将该结点的rvisited设成1,因为其右孩子的入栈意味着它的右孩子必将先于它被访问(这很好理解,因为我们总是从栈顶取出元素来进行visit)。由此可知,下一次该元素再处于栈顶时,其右孩子必然已被visit过了,所以此处可以将rvisited设置为1。
      sn.rvisited = 1;

      //往左下方走到尽头,将路径上所有元素入栈
      p = sn.p.rchild;
      while(p != 0)
        push(p, 0);
        p = p.lchild;
      
    //这一轮循环已结束,刚刚入栈的那些结点我们不必管它了,下一轮循环会将这些结点照顾的很好。
  

而我自己写的算法则是不断访问左边子树后将其切断,虽然能实现,但是不是很好,这里就不展示。

不使用栈
  • 不使用栈的先序非递归遍历

void PreOrderTraverse(TriTree T, Status (*visit)(int elem))
    TriTree p, pr;
    if( T == NULl ) return ;

    p = T;
    while(p!=NULL)
        visit(p->data);
        if(p->lchild != NULL)  
            p = p->lchild;
        else if( p->rchild != NULL )
            p = p->rchild;          
        else
            // ★★往回查找,找到第一个有右孩子的p结点,并且该右子树没被访问过(由pr标记),找不到程序结束 
            while(p!=NULL && (p->rchild==pr||p->rchild==NULL))
                pr = p;
                p = p->parent;
            
            if( p!=NULL ) p = p->rchild;
        
    
  • 不使用栈的中序非递归遍历

TriTree GoFastLeft(TriTree T)
    if( T == NULL ) return NULL;

    while(T->lchild != NULL )
        T = T->lchild;
    
    return T;

void InOrder(TriTree PT, void (*visit)(TElemType))
    TriTree t;

    if( PT == NULL ) return ;    
    t = GoFastLeft(PT);

    while(t != NULL)
        visit(t->data);

        if( t->rchild != NULL)
            t = GoFastLeft(t->rchild);
        else
            // 右元素为NULL,返回上一层

            if( t->parent != NULL )
                // 如果是其双亲的左孩子,说明双亲还没visit操作
                if( t->parent->lchild == t ) 
                    t = t->parent;
                else
                    // 如果是其双亲的右孩子,说明双亲已经被visit操作,继续向上
                    while( t->parent != NULL && t->parent->rchild == t )
                        t = t->parent;      
                     
                    if( t->parent == NULL )
                            t = NULL;
                    else
                        t = t->parent;
                    
                
            else
                t = NULL;
             
         
               
  • 不使用栈的后序非递归遍历

typedef struct TriTNode 
  TElemType  data;
  struct TriTNode  *lchild, *rchild, *parent;
  int mark;  // 标志域(在三叉链表的结点中增设一个标志域
             // (mark取值0,1或2)以区分在遍历过程中到达该结点
             // 时应继续向左或向右或访问该结点。)
 TriTNode, *TriTree;

void PostOrder(TriTree T, void (*visit)(TElemType))
    TriTree t;
    if( T == NULL ) return ;     
    t = T;                

    while( t!= NULL )

        if( t->mark == 0 ) // 向左(左边是否为空)     
            if( t->lchild == NULL && t->rchild != NULL ) // 左边为空,直接标记为1,并访问操作,
                t->mark = 1; // 表示待visit操作,并向上
                t = t->rchild;
            else if( t->lchild == NULL && t->rchild == NULL )  // 左右为空,直接访问并向上   
                t->mark = 1; 

                visit(t->data);
                t = t->parent;
            else
                t->mark = 2;    // 进入左边,标记表示待向右
                t = t->lchild; 
            
         else if( t->mark == 2 )  // 向右操作  (可能为空)
            if( t->rchild == NULL )
                t->mark = 1; //  表示待visit操作,并向上

                visit(t->data);
                t = t->parent;
            else 
                t->mark = 1;
                t = t->rchild;
                  
         else if( t->mark == 1 )    //visit操作,并向上
            visit(t->data);
            t = t->parent;
                    
    

广度优先遍历


void LevelOrderTraverse(BiTree T, Status (*visit)(int elem))
    if( T == NULL ) return ;

    LQueue Q; InitQueue(Q);
    BiTree p = T;
    visit(T->data);
    EnQueue(Q,p);

    while( !QueueEmpty(Q) )
        DeQueue(Q,p);

        if(p->lchild != NULL)      // 处理左孩子 
            visit(T->lchild->data);
            EnQueue(Q,p->lchild);
         

        if(p->rchild != NULL)      // 处理右孩子 
            visit(T->rchild->data);
            EnQueue(Q,p->rchild);
        
       
 

最后,如果有哪里错误或者不足,希望能够指出,谢谢!

以上是关于树 -- 二叉树遍历方法思路大总结(10种方法)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构题目二叉树遍历,哪位大神帮忙解答下,谢谢!

LeetCode(114): 二叉树展开为链表

二叉树遍历

Swift 数据结构 - 二叉树(Binary Tree)

二叉树遍历大总结

二叉树遍历大总结