學習數據結構--第四章:樹與二叉樹(二叉樹的遍歷和線索二叉樹)

第四章:樹與二叉樹(二叉樹的遍歷和線索二叉樹)

上篇文章中講了 學習數據結構–第四章:樹與二叉樹(二叉樹的順序存儲和鏈式存儲) 下面學習二叉樹的遍歷和線索二叉樹

1.二叉樹的遍歷

二叉樹的遍歷:按某條搜索路徑訪問樹中的每個結點,樹的每個結點均被訪問一次,而且只訪問一次。

在這裏插入圖片描述
我們按照訪問根結點的順序分爲

  • 先序遍歷:先根->左子樹->右子樹
  • 中序遍歷:先左子樹->根->右子樹
  • 後序遍歷:先左子樹->右子樹->根

注意無論根什麼時候訪問,都是先訪問左子樹後訪問右子樹。

1.1先序遍歷

先序遍歷:

  • 訪問根節點;
  • 採用先序遞歸遍歷左子樹;
  • 採用先序遞歸遍歷右子樹;

:每個節點的分支都遵循上述的訪問順序,體現“遞歸調用”

時間複雜度:O(n)

上圖先序遍歷結果:A BDFE CGHI

思維過程:

  • (1) 先訪問根節點A,
  • (2)A分爲左右兩個子樹,因爲是遞歸調用,所以左子樹也遵循“先根節點-再左-再右”的順序,所以訪問B節點,
  • (3) 然後訪問D節點,
  • (4) 訪問F節點的時候有分支,同樣遵循“先根節點-再左–再右”的順序,
  • (5) 訪問E節點,此時左邊的大的子樹已經訪問完畢,
  • (6) 然後遵循最後訪問右子樹的順序,訪問右邊大的子樹,右邊大子樹同樣先訪問根節點C,
  • (7) 訪問左子樹G,
  • (8) 因爲G的左子樹沒有,所以接下倆訪問G的右子樹H,
  • (9) 最後訪問C的右子樹I

先序遍歷的遞歸算法:

void PreOrder(BiTree T){
    if(T!=null){
       visit(T);
       PreOrder(T->lchild);
       PreOrder(T->rchild);
    }
}

1.2中序遍歷

按照左子樹->根節點->右子樹的順序訪問


中序遍歷:

  • 採用中序遍歷左子樹;
  • 訪問根節點;
  • 採用中序遍歷右子樹

時間複雜度:O(n)

上圖中序遍歷結果:DBEFAGHCI

中序遍歷的遞歸算法:

void PreOrder(BiTree T){
    if(T!=null){
       PreOrder(T->lchild);
       visit(T);
       PreOrder(T->rchild);
    }
}

1.3後序遍歷

按照左子樹->右子樹–>根節點的順序訪問

後序遍歷:

  • 採用後序遞歸遍歷左子樹;
  • 採用後序遞歸遍歷右子樹;
  • 訪問根節點;

時間複雜度:O(n)

上圖後序遍歷的結果:DEFB HGIC A

後序遍歷的遞歸算法:

void PreOrder(BiTree T){
    if(T!=null){
       PreOrder(T->lchild);
       PreOrder(T->rchild);
       visit(T);
    }
}

2.二叉樹的非遞歸遍歷

上述講的三種遍歷方法都是使用遞歸進行遍歷,下面講如何使用非遞歸算法遍歷,我們需要藉助 ,以中序遍歷爲例:

算法思想

  • 初始時依次掃描根結點和根節點的所有左側結點並將它們依次進棧
  • 出棧一個結點,訪問
  • 掃描該結點的右孩子結點並將其進棧
  • 依次掃描右孩子結點的所有左側結點並—進棧
  • 反覆該過程直到棧空爲止

注意區分掃描和訪問


按照上面的算法思想講解如上圖二叉樹,我們使用非遞歸算法遍歷:

1:依次掃描根結點和根節點的所有左側結點並將它們依次進棧

2:出棧一個結點並訪問

3:接着掃描7號結點的右孩子結點並進棧,它的右孩子結點爲空,故無任何結點壓入棧中。

4:然後繼續出棧4號結點並訪問它

5:接着掃描4號結點的右孩子結點並進棧,它的右孩子結點爲空,故無任何結點壓入棧中。

6:然後繼續出棧2號結點並訪問它

7:接着掃描2號結點的右孩子結點並進棧,它的右孩子結點爲5,故將5號結點壓入棧中。

8:接着掃描5號結點的所有左側結點並依次進棧,它的左側結點爲空,故無任何結點壓入棧中。

9:然後繼續出棧5號結點並訪問它,同樣他右孩子結點爲空,無任何結點進棧。

10:接着出棧1結點並訪問它。

11:同樣掃描1結點的右孩子結點,依次進棧

12:右孩子結點進棧後,依次將該節點的左側結點依次進棧,然後繼續循環步驟二,知道棧爲空


代碼實現:

void InOrder2(BiTree T){
   InitStack(S);
   BiTree p=T;
   //循環判斷
   while(p||!IsEmpty(S)){ //棧非空 
      if(p){
         Push(S,p);
         p-p->lchild; //將p指向左孩子
      }else{
         Pop(S,p); //左孩子爲空,出棧一個結點
         visit(p); //並訪問它
         p=p->rchild; //指向右孩子
      }
   }
}

3.層次遍歷

層次遍歷:顧名思義,從上到下從左到右依次遍歷,遍歷順序及時標號順序。

層次遍歷需要藉助隊列,算法思想:

  • 初始將根入隊並訪問根結點
  • 若有左子樹,則將左子樹的根入隊
  • 若有右子樹,則將右子樹的根入隊
  • 然後出隊節點並訪問
  • 反覆該過程直到隊列爲空

代碼實現

void leveOrder(BiTree T){
    InitQueue(Q);
    BiTree p; //輔助變量
    EnQueue(Q,T); //根節點入隊
    while(!isEmpty(Q)){
      DeQueue(Q,P); //出隊隊首元素
      visit(p); //並訪問
      if(p->lchild!=NULL){ //左孩子節點不爲空,入隊
         EnQueue(Q,p->lchild);
      }
      if(p->rchild!=NULL){//右孩子節點不爲空,入隊
         EnQueue(Q,p->rchild);
      }
    }
}

4.遍歷結果逆置

我們由一個二叉樹可以得到遍歷序列,那麼我們是否可以通過遍歷序列得到一個二叉樹嗎?

首先我們只通過一個遍歷序列可以得到二叉樹嗎?例如先序遍歷序列:124536,我們直到先遍歷的肯定是根節點,但是2是左節點還是右節點缺無法確定,所以根據一個遍歷序列無法全程逆置。

其實,(後)先序遍歷序列和中序遍歷序列可以確定一顆二叉樹,而後續遍歷序列和先序遍歷序列不可以確定一顆二叉樹。

在學習遍歷結果逆置的時候請務必清楚三種遍歷方式.

先序遍歷序列和中序遍歷序列逆置思想:

  • 在先序序列中,第一個節點是根結點;
  • 根結點將中序遍歷序列劃分爲兩部分;
  • 然後在先序序列中確定兩部分的結點,並且兩部分的第一個結點分別爲左子樹的根和右子樹的根;
  • 在子樹中遞歸重複該過程,便能唯一確定一棵二叉樹。

例如:先序序列:124536 中序序列爲:425163,請畫出該二叉樹。

先序遍歷我們直到第一個結點時根節點,後序遍歷序列我們知道最後一個結點爲根結點,所以後序遍歷序列加中序遍歷序列的操作大同小異。

另外根據層次遍歷序列中序遍歷序列也可以確定一個唯一二叉樹。

5.線索二叉樹

上面講到二叉鏈表,我們知道不管二叉樹的形態如何,空鏈域的個數總是多過非空鏈域的個數。準確的說,n各結點的二叉鏈表共有2n個鏈域,非空鏈域爲n-1個,但其中的空鏈域卻有n+1個。

因此,提出了一種方法,利用原來的空鏈域存放指針,指向樹中其他結點,這種指針稱爲線索。同時提升了查找速度。

我們稱這個線索二叉樹的建立過程爲:線索化

若無左子樹,則將左指針指向其前驅結點。
若無右子樹,則將右指針指向其後繼結點。

5.1先序線索化

結點1有左孩子2->結點2有左孩子->結點4沒有左孩子故左指針指向前驅結點2->結點4沒有右孩子故右指針指向後繼結點5->結點5沒有左孩子故左指針指向前驅結點->結點5沒有右孩子故右指針指向後繼結點3->結點3有左孩子6->結點6沒有左孩子故左指針指向前驅節點3->結點6沒有右孩子且沒有後繼結點->接着看結點3,它沒有右孩子則將右指針指向後繼結點6。

5.2中序線索化

5.3後序線索化

最常用的還是中序線索二叉樹

顯然,在決定lchild是指向左孩子還是前驅,rchild是指向右孩子還是後繼,需要一個區分標誌的。因此,我們在每個結點再增設兩個標誌域ltagrtag

線索二叉樹的結點結構


typedef struct ThreadNode{
    ElemType data;
    struct ThreadNode *lchild,*rchild;
    int ltag,rtag;
}ThreadNode,*ThreadTree;

這種結點結構構成的二叉鏈表作爲二叉樹的存儲結構,稱爲線索鏈表。對於指向前驅和後繼的指針稱爲 線索,線索化的二叉樹就稱爲:線索二叉樹

5.4中序線索二叉樹

中序線索二叉樹線索化代碼

//傳入一個根節點和前驅結點
void InThread(ThreadTree &p,ThreadTree &pre){
    if(p!=NULL){
        InThread(p->lchild,pre);  //遞歸左子樹線索化
        if(p->lchild==NULL){ //沒有左孩子
            p->lchild = pre; //左孩子指針指向前驅
            p->ltag = 1;    //前驅線索
        }
        if(pre!=NULL && pre->rchild==NULL){ //沒有右孩子
            pre->rchild = p; //前驅右孩子指針指向後繼(當前結點p)
            pre->rtag = 1;  //後繼線索
        }
        pre = p; //修改前驅結點爲當前結點
        InThread(p->rchild,pre);  //遞歸右子樹線索化
    }
}

初始化和收尾

//傳入線索二叉樹的根節點
void CreateInThread(ThreadTree T){
     ThreadTree pre=NULL;
     if(T!=NULL){
        InThread(T,pre); //實現線索化
        pre->rchild=NULL; //收尾 最後遍歷的結點的右孩子至爲空
        pre->rtag=1;
     }
}

引入頭節點的線索二叉樹

中序線索二叉樹的遍歷

//找最左側的孩子結點
ThreadNode *Firstnode(ThreadNode *p){
     while(p->ltag==0){
          p=p->lchild;
     }
     return p;
}
//找後繼結點
ThreadNode *Nextnode(ThreadNode *p){
     if(p->rtag == 0){
          return Firstnode(p->rchild);
     }
     return p->rchild;
}
void InOrder(ThreadNode *T){
     for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p)){
         visit(p);
     }
}

關於數據結構的知識公衆號 理木客同步更新中,下次將會講解:樹與森林相關知識,歡迎大家的關注

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