樹 -- 二叉樹遍歷方法思路大總結(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);
        }
    }   
} 

最後,如果有哪裏錯誤或者不足,希望能夠指出,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章