【內功修煉】二叉樹的那些事

最近上數據結構的課講到了二叉樹,課上聽得雲裏霧裏的,媽蛋,還不如老子自學呢!儘管具體算法都基本搞懂,但知識需要總結才能串起來。
目錄:

二叉樹的創建與刪除

給出創建算法之前先給出二叉樹類和二叉樹結點結構體的定義吧(^__^)

//定義結點結構體
template <class T>
struct BintreeNode
{
    T data;
    BintreeNode<T>* pLeftTree;
    BintreeNode<T>* pRightTree;
    BintreeNode<T>* parent;
    BintreeNode() :pLeftTree(NULL), pRightTree(NULL){}
    BintreeNode(T x, BintreeNode<T>* pLeftTree=NULL,BintreeNode<T>* pRightTree = NULL) :data(x),pLeftTree(pLeftTree), pRightTree(pRightTree){}
};
//定義二叉樹類
template <class T>
class Bintree{
public:
    Bintree() :pRoot(NULL){};
    ~Bintree(){ _destroyTree(pRoot); }
    BintreeNode<T>* createBintree();
    BintreeNode<T>* createBintree(T * VLR, T* LVR, int n);
    void preOrder(BintreeNode<T>* root);
    void inOrder(BintreeNode<T>* root);
    void postOrder(BintreeNode<T>* root);
    void levelOrder(BintreeNode<T>* root);
    void preOrder();
    void inOrder();
    void postOrder();
    void levelOrder();
private:
    BintreeNode<T>* pRoot;
    void _destroyTree(BintreeNode<T>* p);
    void visit(BintreeNode<T>* p);
};

類似於廣義表的創建算法

就是要求用戶按格式輸入字符串來創建二叉樹,跟廣義表的創建非常類似。
創建過程需要一個輔助棧以及輔助變量k用於標記是在創建左子樹還是右子樹。
1、輸入data時,比如一個’A’, new一個結點(*p),這時分兩個情況。a、根節點pRoot爲空,那麼新節點(p)就是根節點了,pRoot指向新節點(p);b、根節點pRoot不爲空,那麼如果k==-1(表示正在創建左子樹)就讓棧頂的節點的左指針指向新節點(P),如果k==1(表示正在創建右子樹)就讓棧頂的節點的右子針指向新節點。
2、輸入’(‘時,k=-1,標誌開始創建左子樹,將節點p壓棧。
3、輸入’,’時,k=1,標誌開始創建右子樹。
4、輸入’)’時,表示該節點p的孩子已經安置好了(^-^)V,出棧,pop();
好,上代碼<( ̄︶ ̄)>

template <class T>
BintreeNode<T>* Bintree<T>::createBintree(){
    if (pRoot != NULL){
        cout << "該二叉樹實例是非空樹,無法創建" << endl;
        return pRoot;
    }
    char RefValue = '#';//結束創建的標誌
    stack<BintreeNode<T>*> s;
    BintreeNode<T>*p, *r;//p用來指向new出來的節點,r用來取出棧頂的節點
    int k;//輔助變量k用於標記是在創建左子樹還是右子樹。
    char in;
    cin >> in;
    while (in != RefValue){
        switch (in)
        {
        case '(': k = -1; s.push(p);//表明p指向的樹節點有兒子,調整k的值進入左子樹,p入棧
            break;
        case ')': s.pop();
            break;
        case ',':k = 1;
            break;
        default: p = new BintreeNode<T>(in);
            if (pRoot == NULL){
                pRoot = p;
            }
            else if (k == -1){
                r = s.top();
                r->pLeftTree = p;
            }
            else{
                r = s.top();
                r->pRightTree = p;
            }
            break;
        }
        cin >> in;
    }
    return pRoot;
    }

利用前序和中序遍歷的遞歸創建算法

這個算法很優美,因爲用到了遞歸( ^_^ )。理論基礎是前序和中序遍歷一起唯一確定一棵二叉樹。
下面直接上代碼

template<class T>
BintreeNode<T>* Bintree<T>::createBintree(T* VLR, T* LVR, int n)
{//VLR和LVR分別表示存儲前序和中序的數組,傳過來的是數組的指針其實可以理解成數組的首地址,n表示當前結點數
    if (n == 0)
        return NULL;
    int k = 0;
    while (VLR[0] != LVR[k])//因爲前序遍歷的首個元素是子樹的父節點,該循環在中序遍歷中找出該跟結點對應的數組下標k
        k++;
    BintreeNode<T>* p = new BintreeNode<T>(VLR[0]);新建父節點
    if (pRoot == NULL)
        pRoot = p;
    p->pLeftTree = createBintree(VLR + 1, LVR, k);//遞歸構建左子樹
    p->pRightTree = createBintree(VLR + k + 1, LVR + k + 1, n - k - 1);//遞歸構建右子樹
    return p;//返回父節點
}

遞歸刪除算法

這個真沒啥好說的,就是跟後序遍歷的遞歸算法幾乎一樣。

template <class T>
void Bintree<T>::_destroyTree(BintreeNode<T>* p){
    if (p != NULL){
        _destroyTree(p->pLeftTree);
        _destroyTree(p->pRightTree);
        delete p;
    }
}

二叉樹遍歷的非遞歸遍歷算法

二叉樹遍歷的遞歸算法代碼非常簡潔優美,然而我們需要付出的是效率方面的代價啊。其實把它們改成非遞歸的算法也不是很難,關鍵理清楚回退路徑,要解決的問題就是何時訪問,何時壓棧。順便提一下,線索二叉樹雖然很巧妙,但實用性太低,而且對程序猿也很不友好啊,我表示已經被繞暈了╮(╯﹏╰)╭,所以這裏就不介紹了。

二叉鏈表的二叉樹

前序遍歷

格式化寫法:這種寫法的好處是好記,容易理清思路,但缺點是還有不少可以優化的點。

template <class T>
void Bintree<T>::preOrder(){
//小p開始了艱險的先序遍歷歷險記
    BintreeNode<T>* p = pRoot;
    stack<BintreeNode<T>*> s;//輔助棧s
    while (p != NULL || !s.empty()){
        while (p)
        {
          visit(p);
          s.push(p);
          p = p->pLeftTree;//殺入左路子樹,見一個就闖進去(visit)並且壓棧(豐厚的戰利品哦),一直到左子樹的底部
        }
        if (!s.empty()){//棧裏的東西遲早是要還的哦,就在你到了左路的窮途末路的時候,柳暗花明右一路,繼續殺入棧頂節點的右路,當然要留下買路錢咯(彈出棧頂節點呀)(∩_∩)
          p = s.top();
          s.pop();
          p = p->pRightTree;
        }
    }
}

下面是我們數據結構老師(據說是位牛人哦)優化後的算法,分享給大家(^o^)/。
優化的地方在哪呢?大家有沒有發現,就在你一路殺入左路的時候,並不是所有節點都需要入棧的哦,
只有左右孩子兼備的時候才需要入棧,只有左孩子或者只有右孩子的話直接進入那一路就ok了,如果兩個孩子都沒有的話就是葉節點咯,處理跟上面一樣。

template <class T>
void Bintree<T>::preOrder(){
    BintreeNode<T>* p = pRoot;
    stack<BintreeNode<T>*> s;
    while (p != NULL)
    {
        visit(p);
        if (p->pLeftTree&&p->pRightTree){
            s.push(p);
            p = p->pLeftTree;
        }
        else if (p->pLeftTree){
            p = p->pLeftTree;
        }
        else if (p->pRightTree){
            p = p->pRightTree;
        }
        else{
            if (!s.empty()){
                p = s.top();
                s.pop();
                p = p->pRightTree;
            }
            else break;
        }
    }
}

中序遍歷

格式化寫法:

template <class T>
void Bintree<T>::inOrder(){
    BintreeNode<T>* p = pRoot;
    stack<BintreeNode<T>*> s;
    //小p開始了艱險的中序遍歷歷險記
    while (!s.empty() || p != NULL)
    {
        while (p != NULL){//徑直殺入左路,跟前序遍歷的區別是不要隨便闖入(visit)啊,畢竟是風能進雨能進國王不能進呀,但是一路壓棧可是跑不掉的哦。
            s.push(p);
            p = p->pLeftTree;
        }
        if (!s.empty()){//到左路山窮水盡的時候了,怎麼辦呢,從棧頂彈出一個精靈球(其實是節點),搜刮(visit)這個節點,完了後竟然出現了該節點的右路傳送門,果斷進去,又是重複如上過程。
            p = s.top();
            s.pop();
            visit(p);
            p = p->pRightTree;
        }
    }
}

優化後的寫法:
該寫法也是數據結構老師優化的,分享給大家。

template <class T>
void Bintree<T>::inOrder(){
    BintreeNode<T>* p = pRoot;
    stack<BintreeNode<T>*> s;
    while (true)
    {
        while (p)//這個過程還是一路殺入左路,但左路末尾的那個節點是不用進棧的,這裏優化了一次入棧。不要怪我摳門咯(*^__^*)
        {
            if (p->pLeftTree){
                s.push(p);
                p = p->pLeftTree;
            }
            else{
                visit(p);
                p = p->pRightTree;
            }
        }
        if (!s.empty()){//這個出棧入右路的過程並沒有啥優化的地方。
            p = s.top();
            s.pop();
            visit(p);
            p = p->pRightTree;
        }
        else {
            break;
        }
    }
}

後序遍歷

到此,大家可能會覺得,二叉樹遍歷的非遞歸算法也不過如此嘛,別急,更大的挑戰在後頭呢。
如下是格式化寫法:
因爲後序遍歷是訪問完左右路才訪問父節點的,所有這裏讓父節點出棧可沒那麼容易啦。爲了方便起見,我用了一個節點輔助棧和一個左右路標記棧,具體過程請欣賞(好吧,小編水平有限,讀者就湊合着閱讀吧)如下代碼+註釋。

template <class T>
void Bintree<T>::postOrder(){
    BintreeNode<T>* p = pRoot;
    stack<BintreeNode<T>*> s;
    int tag[100];//這是自定義的一個簡陋棧,用於儲存左右路標記,左路壓入值0,右路壓入值1,至於爲何不用stl模板類後面有解釋哦。
    int top=0;//這是與棧配套使用的棧頂指針
    //小T開始了艱險的後序遍歷歷險記(爲啥不是小p呢?因爲最艱鉅的任務有我身體力行呀。(*^__^*)
    while (!s.empty()||p!=NULL)
    {
        while (p){//同樣的,向左一路殺入到底,這時小T揹着兩個棧,除了沿途遍歷節點要入棧以外,因爲是左路,所以要向標誌棧壓入值0,也是先不要訪問節點
            s.push(p);
            tag[++top] = 0;//++的用法大家應該熟知了吧
            p = p->pLeftTree;
        }
        if (!s.empty()){//這裏也可以寫成while (tag[top] == 1&&!s.empty()),寫成這樣是因爲我想盡量將代碼格式化,便於記憶而已。
            while (tag[top] == 1){//把標誌棧棧頂爲1的節點彈出來,因爲父節點的右路已經訪問完了,所以隨便在退棧以後訪問父節點,後序遍歷嘛!
                p = s.top();
                s.pop();
                top--;
                visit(p);
            }
        }
        if (!s.empty()){//到了左路的盡頭,小T發現標誌棧棧頂的節點標誌爲0,表示該節點的右路可走,那麼修改該節點標誌爲1(以防迷路哦^O^ ),殺入右路去咯,歐耶!
            p = s.top();
            tag[top] = 1;//這裏就是爲什麼用自定義棧的原因,因爲stl棧類的數據是封裝的,只讀不能寫,一定要改的話只能將棧頂彈出,修改完後再重新壓棧,這樣效率遠沒有直接給棧頂數據賦值的效率高
            p = p->pRightTree;
        }
        else break;//這句必不可少,否則會跳不出循環,因爲始終p!=null
    }
}

其實後序遍歷還可以優化的,因爲左路一路到底最後一個節點是不需要壓棧的。

三叉鏈表的二叉樹

三叉鏈表與二叉鏈表的主要區別在於,它的結點比二叉鏈表的結點多一個指針域,該域用於存儲一個指向本結點雙親的指針。這樣遍歷時找回退路徑就更加方便了,不需要用棧了,所以這種數據結構的遍歷算法效率是最高的,然而要付出的是空間的額外開銷,用空間換時間。
三叉鏈表二叉樹

前序遍歷

先上代碼:

template <class T>
void Bintree<T>::preOrder(){
  if (NULL == pRoot) {  
    return ;  
  }  
  BintreeNode<T>* p = pRoot;
  BintreeNode<T>* pr; 
  while (p != NULL) {//不知道大家有沒有發現,幾乎所有遍歷算法首先都是要一路殺入左路
    visit(p); //前序遍歷都是走-訪節點
    if (p->pLeftTree != NULL) {  
      p = pLeftTree;
    }  
    else if (p->pRightTree != NULL){//左子樹到底了,如果有右子樹就走右子樹
      p = p->pRightTree;
    }  
    else {//左子樹到底了,如果 沒有右子樹就說明到了葉節點了,好,難點來了。
      //關鍵在於怎麼回溯到雙親結點  
      //這個循環是往回查找第一個有沒訪問過的右子樹的父結點
      do {//回溯找爹的操作
        pr = p;  
        p = p->parent;  
      } while (p != NULL && (p->pLeftTree != pr || NULL == pRightTree)); 
      //當p == NULL的時候意味着到達樹根了,不能再回溯了
      //當p->pLeftTree == p && p->pRightTree != NULL的時候,就找到了第一個沒有被訪問的右子樹,跳出循環。
      if (p != NULL) {
        p = p->pRightTree;  
      }  
    }  
  }  
}  

中序遍歷

template <class T>
void Bintree<T>::inOrder(){
  if (NULL == pRoot) {
    return ;  
  }  
  BintreeNode<T>* p = pRoot;
  BintreeNode<T>* pr;  
  while(NULL != p)  
  {  
    if (p->pLeftTree != NULL) {  
      p = p->pLeftTree;  
    }  
    else {
      visit(p); 
      if (p->pRightTree != NULL) {  
        p =p->pRightTree;  
      }//有沒有發現以上的代碼有點似曾相識呢,沒錯它跟前序遍歷的代碼就差在visit(p);的位置上,這跟也之前的傳統而參數的非遞歸算法幾乎一樣的,重頭戲在於,到達葉節點的回溯找爹的處理上。
      else {  
        //回溯雙親結點,同樣是找到第一個沒有被訪問的右子樹  
        //細微的差距就是,因爲在遞歸左子樹的過程中,並沒有輸出雙親結點,所以一旦回溯到父節點時,走之前記得拜訪一下這個祖先哦。 
        pr = p;  
        p = p->parent;  
        while (p != NULL && (p->pLeftTree != pr || NULL == p->pRightTree)) {  
          if (p->pLeftTree == pr) {//說明是從左路回溯過來的
            visit(p); //一旦回溯到父節點時,走之前記得拜訪一下這個祖先哦。 
          }  
          pr = p;  
          p = p->parent;  
        }
        if (NULL != p) {  
          visit(p); //一旦回溯到父節點時,走之前記得拜訪一下這個祖先哦。 
          p = p->pRightTree;  
        }  
      }  
    }  
  }  
}  

後序遍歷

template <class T>
void Bintree<T>::postOrder(){
  if (NULL == T) {  
    return ;  
  }  
    BintreeNode<T>* p = pRoot;
    BintreeNode<T>* pr;
  while (p != NULL) {  
    if (p->pLeftTree != NULL){  
      p = p->pLeftTree;
    }  
    else {
      if (p->pRightTree != NULL) {  
        p =p->pRightTree;  
      }
      else {
//到了葉節點後就可以輸出當前結點並回溯雙親結點。  
        visit(p);
        pr = p;
        p = p->parent;
        //同樣的,利用循環回溯找到第一個右路可走的父節點  
        while(p != NULL && (p->pLeftTree != pr || NULL == p->pRightTree)) {  
          //如果是從右路回溯的話,就說明,這時候的左子樹和右子樹都被輸出了,這時候父結點就應該被訪問了。
          if(p->pRightTree == pr || p->pRightTree == NULL) {  
            visit(p);
          }  
          pr = p;  
          p = p->parent;  
        }  
        if(NULL != p) {                      
          p = p->pRightTree;  
        }  
      }              
    }  
  }  
}  

總結

大家有沒有發現,非遞歸的遍歷算法並不是那麼複雜,相信大家按照思路認真閱讀了代碼+註釋,一定會發現其中的規律。其實算法就只有一個,只是用不同的數據結構或者不同的數據處理方式使代碼效率得到了優化。
這麼多遞歸方法有驚人一致的主線—外套一個大循環,大循環內部先套一層小循環,小p沿左路一路到底,若到了左路盡頭,跳出小循環;判斷右路可走,則走右路;否則就是到達了葉節點,接着處理葉節點(回溯找爹)。
三叉鏈表的遍歷算法和傳統的二叉鏈表的非遞歸遍歷算法的區別就在於二叉鏈表的非遞歸遍歷算法是用棧實現回溯找爹的過程的,而三叉鏈表因爲本身有一個指向父節點的指針域,回溯找爹方便而且效率高。
前序,中序,後序遍歷算法之間的區別在於訪問父節點的時機,以此衍生的就是父節點壓棧和退棧的時機而已。
(^-^)V希望讀者讀到這兒能會心的說一句,二叉樹也不過如此嘛。
其實,這篇是我寫的第一篇技術博客,雖然不是什麼高大上的東東,但當我看到我竟然不知不覺產出這麼多有意義的,好玩的文字,心裏真的滿滿的成就感,希望以後還能有時間和機會去寫博客。總結所學,不僅能分享給他人,還能方便自己,提升自己理解的深度,不是嗎?(^o^)/YES!

參考的技術博客和文獻:
http://blog.csdn.net/sky453589103/article/details/45831105
http://www.cnblogs.com/hicjiajia/archive/2010/08/27/1810055.html
特別鳴謝我的數據結構老師—-楊老師

發佈了32 篇原創文章 · 獲贊 45 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章