1. 程式人生 > >資料結構與演算法(四)二叉樹結構

資料結構與演算法(四)二叉樹結構

1.二叉樹定義

樹結構產生的由來:為了解決陣列和連結串列在修改元素和查詢元素的複雜度上做平衡。樹是一種半線性結構,經過某種遍歷,即可確定某種次序。以平衡二叉搜尋樹為例,修改與查詢的操作複雜度均在O(logn)時間內完成。

樹的性質:連通無環圖,有唯一的根,每個節點到根的路徑唯一。有根有序性。

節點的深度:節點到根部的邊的數目。樹高為深度最大值。內部節點,葉節點,根部節點。節點有高度,深度,還有度數。

節點高度:對應節點子樹的高度,葉節點高度為0.,由該子樹某一葉節點的深度確定。節點度數:其孩子總數。

二叉樹:應用廣泛。每個節點的度數不超過2.有序二叉樹樹,孩子作為左右區分。

K叉樹:每個節點的孩子均不超過K個。

將有序多叉樹轉化為二叉樹:滿足條件為同一節點的所有孩子之間滿足某一線性次序。轉化條件:為每個節點指定兩個指標,分別指向其長子和下一兄弟。

應用:編碼問題。每一個具體的編碼方案都對應於一顆二叉編碼樹。

例如:原始ASCII文字經過編碼器成為二進位制流,再經過解碼器成為文字資訊。每一個文字的基本組成單位都是一個字元,由一個特定字符集構成。編碼表表示某一個字元所對應的特定二進位制串。關鍵是確定編碼表。根據編碼表來解碼和編碼。

字首無歧義編碼:各字元的編碼串互不為字首,可保證解碼無歧義。PFC編碼。

二叉編碼樹:將字元對映到二叉樹的葉節點,由葉節點到根部的二進位制串。由二叉編碼樹可構建編碼表,可順利編碼。

解碼是:從前向後掃描該串,同時在樹中移動,直至抵達葉節點,輸出字元。再次回到根節點。這一解碼過程可在接受過程中實時進行,屬於線上演算法。

關鍵問題:如何構造PFC編碼樹呢?

二,如何構建二叉樹

二叉樹的基本組成單位:節點

節點成員:節點值,父節點指標,左右孩子節點指標,節點高度。

建構函式:預設構造,初始值構造;

操作介面:節點後代總數,插入左右孩子節點(約定當前節點無左右孩子),取直接後繼節點(中序遍歷後的次序),子樹四種遍歷,比較,判等。

在二叉樹節點的類的基礎上構建二叉樹類。

樹成員:樹規模,根節點指標

樹建構函式:預設建構函式,解構函式

樹操作介面:規模,判空,樹根指標,插入根節點,插入左右孩子或左右子樹,刪除某節點子樹,遍歷,比較器,節點高度更新。

高度更新策略:每當有節點加入或離開二叉樹,則更新其所有祖先的高度。

在每一節點V處,只需讀取其左右孩子的高度並取二者之間的最大值,再計入當前節點本身,就得到了V的新高度。

樹的遍歷:按照某種約定的次序,對各節點訪問依次且一次。

各節點與其孩子之間約定某種區域性次序。V,L,R。有VLR,LVR,LRV三種選擇。先中後,可知先左後右是必須的,只是V的次序傳送變化。

輸入:樹節點位置X
輸出:向量visit,即為遍歷後的次序
遞迴呼叫:
    travaPre(x,visit){
        if x 為空,則返回;
        visit(x->data);
        travaPre(x->lc,visit);
        travaPre(x->rc,visit);
    }
遞迴版均為線性時間,但常係數較大。
可觀察知右子樹為尾遞迴,左子樹為接近於尾遞迴,且不為線性遞迴。
一般消除尾遞迴,可用while迴圈解決。
迭代版:消除尾遞迴的一般性方法,即藉助輔助棧來解決。
先序遞迴的訪問區域性次序為根,左,右。要保證每個節點均會被訪問到,且只能訪問一次。即要求每個節點均會被入棧,且也會被彈出,且均只有一次。當棧為空,則結束。可簡單推匯出棧的彈出規律。
迭代先序方法一:簡單式方法,根據訪問次序,嚴格尾遞迴解決。trePre(x,visit){
            stack<>s;
            if(x) s.push(x);
            while(!s.empty()){
                x=s.pop();visit(x);
                if(x->rc) s.push(x->rc);
                if(x->lc) s.push(x->lc);
            }
}
迭代先序方法二:批次入棧,然後訪問.一般性方法是第一批先入棧並訪問,直到葉節點。
               trepre(x,visit){
               stack<>s;
                while(true){
                visitFirst(x,visit,s);
                if(s.empty()) break;
                x = S.pop();
            }
    }
visitFirst(x.visit,s){
    while(x){
      visit(x);
      if(x->rc) s.push(x->rc);
      x=x->lc;
    }
}
迭代中序方法:trepre(x,visit){
                stack<>s;
                while(true){
                visitFirst(x,visit,s);
                if(s.empty()) break;
                x = S.pop();visit(x);
            }
    }
visitFirst(x.visit,s){
    while(x){
      s.push(x);
      x=x->lc;
    }
}
迭代版後序方法:關鍵是抓主停止入棧的條件。迭代後序停止入棧的條件為
:左節點為葉節點時,停止入棧。trepre(x,visit){
                stack<>s;
                if(x) s.push(x);
                while(true){
                if(x->parent!=s.top())               
                    {visitFirst(s.top(),visit,s);}
                if(s.empty()) break;
                x = S.pop();visit(x);
            }
    }
visitFirst(x.visit,s){
    while(x){
      if(x->right) s.push(x->right);
      if(x->left) ) s.push(x->left);x=x->left;
      if(!x->left && x->right) x=x->right;
      if(!x->left && !x->right) break;
    }
}
由以上總結可知,不管是先序,中序還是後序,均可用同一種演算法解決。只是先序的第一種演算法更加簡單。

 

樹的層次遍歷:也即廣度優先遍歷。節點訪問次序為先上後下,先左後右。輔助佇列的規模為n/2,包含滿二叉樹。演算法如下:

迭代式層次遍歷:佇列來解決。
                travel(x,visit){
                Queue<> q;
                if(x) q.enqueue(x);
                while(!q.empty()){
                    x=q.dequenue();visit(x);
                    if(x->lc) q.enqueue(x->lc);
                    if(x->rc) q.enqueue(x->rc);
                }
}
按入隊的次序將從0起將各節點X編號為r(x).則從0-n都對應於完全二叉樹中的某一個節點。將所有節點存入向量結構,各節點的rank即為其編號。即完全二叉樹節點以層次遍歷所得到的順序存入向量結構中。即可提高對樹的儲存和處理效率。那麼又如何知道節點之間的關係呢?滿足以下規律:
r(L)=r(x)*2+1;即可。

完全二叉樹:葉節點只能出現在最底部的兩層,且最底層葉節點均處於次底層葉節點的左側。高度為h的完全二叉樹,規模介於2h和2h-1之間。規模為n的完全二叉樹,高度為log2N.

滿二叉樹:所有葉節點均處於最底層。

三 如何構建PFC編碼樹

ASCII文字---->編碼器(編碼樹,向量實現編碼森林)---->解碼器(基於樹的遍歷)---->文字。

根據字符集構造編碼樹,從而得編碼表也就是字典得形式,從而將文字轉換為二進位制流。

根據編碼樹得遍歷對二進位制流解碼為字元。 

可自底而上地構造PFC編碼樹。首先,由每一個字元分別構造一顆單節點二叉樹,並將其視作一個森林。此後,反覆從森林中取出兩顆樹合二為一。經過n-1次迭代後,初始森林中得n顆樹將合併為一顆完整得PFC編碼樹。接下來,再將PFC編碼樹轉譯為編碼表。演算法如下:

演算法總體框架:向量實現PFC森林,其中各元素對應於一顆編碼樹,其data為相應字元。
1.初始化PFC森林:
        建立空森林,對每一個可列印得字元,建立一顆相應得PFC編碼樹,並
        將字元作為根節點插入到PFC編碼樹中。返回PFC森林。
2.構造完整得PFC編碼樹:
        設定隨機數time
        while迴圈字元數-1次:
            建立新樹S“^”;隨機選取森林中的第r1顆樹,將其作為S的左子樹接入,
            然後剔除森林中的r1樹,隨機選取森林中的r2樹,將其作為S的右子樹接入。
            然後剔除森林中的r2樹,合併後的PFC樹重新植入森林。
        最後,該向量只剩一棵樹,並返回。
3.生成PFC編碼表:
        通過遍歷的方式獲取從根節點到葉節點的字串。
        如何記錄該字串?用string或者點陣圖。
        類似先序遍歷的遞迴模式。區域性子結構為VLR。也就是說先序遍歷模式可用來獲取從根節點到葉節點的每一條路徑。
        結果,返回字典,記錄每一個字元所對應的字串。


該樹的葉節點均為字元樹,內部節點和根節點均為字元“^”.
PFC編碼樹的高度不統一,不平衡的狀態表明其並不一定是最優編碼樹。還可以優化。
平均編碼長度也就是葉節點平均深度。最優編碼樹不唯一但存在。其特點是:真二叉樹,葉節點深度之差不超過一。真完全樹滿足要求。構造方法:建立包含2*n-1個節點的真完全二叉樹,並將字元分配給n個葉節點,即可得到一顆最優編碼樹。

四,如何構建Huffman編碼演算法

最優編碼樹的實際應用價值並不大,所以如何衡量平均編碼長度?

1.帶權平均編碼長度  與字元出現概率有關。退出最優帶權編碼方案。

策略與演算法:對於字元出現概率已知的任一字符集A,可採用如下演算法構造以下編碼樹:

HUFFMAN編碼演算法:
1.對於字符集中的每一個字元,分別建立一顆樹,其權重為該字元的頻率。
2.從該森林中取出兩顆權重最小的樹,建立一個新節點,合併它們,其權重取作二者權重之和。依次迭代即可
3.再次強調HUFFMAN編碼樹只是最優帶權編碼樹中 的一顆。
關鍵點是如何找到森林中權重最小的兩顆樹?用遍歷法。
首先在計算字符集的頻率時就已知其順序。那麼在構造森林時,即可按順序排列,用向量來做。從小到大。
首先取出兩個最小的,移除後,再合併插入原向量,就要查詢位置,用二分查詢。然後插入。從而更新順序。
移除和查詢,還有插入均花時間。移除O(n),查詢o(logn),插入。
用列表來做,查詢最小值花時間,插入和刪除很快。

五,平衡二叉搜尋樹

要求物件集合的組成可以高效率的調整,又可以高效率的查詢,所以需要有樹。查詢分為循RANK訪問,循關鍵碼訪問。資料物件均表示為詞條形式。詞條擁有兩個變數KEY ,VALUE。KEY可以比較。

二叉搜尋樹。條件:順序性。任一節點的左子樹的所有節點必不大於該節點,其右子樹的所有節點必不小於該節點。也就是說:R>=V>=L。

特點:中序遍歷單調非降。中序遍歷一致的二叉搜尋樹為等價二叉搜尋樹。

查詢演算法:減而治之策略,與二分查詢類似。返回查詢位置,若成功則返回該節點,若失敗返回其父親位置和返回空。

控制查詢時間,必須控制二叉搜尋樹的高度。

插入演算法:先查詢具體位置,再插入,再更新祖先高度。若有相同節點則失敗。取決於樹高。

刪除演算法:分為兩種情況,一是隻有一個孩子時:將其替換為其孩子也就是其父節點指向其孩子,同時釋放該節點,更新祖先高度。

雙分支情況:1.找到該節點後繼,交換二者的資料項,將後繼節點等效視為待刪除的目標節點。轉到情況一。總體複雜度也取決於樹的高度。

平衡二叉搜尋樹:採取的平衡為適度平衡,而不是理想平衡。AVL樹,伸展樹,紅黑樹,kd-樹均屬於平衡二叉搜尋樹。

適度平衡性是通過對樹中的每一區域性增加某種限制條件形成的。任何二叉搜尋樹均可等價變換為平衡二叉搜尋樹,但在最壞情況下可花費O(n)時間。

區域性性失衡調整方法:圍繞特定點的旋轉。

ZIG:兩個節點,三個子樹,旋轉。節點,C,V,子樹X,Y,Z,。C為V的左孩子,Z為V的右孩子,X,Y為C的左右孩子。

V的ZIG旋轉:V的父節點指向C,C的左右孩子為X,V。V的左右孩子為Y,Z。V的右旋,V成為C的右孩子。

同理:zag:節點C,V。C為V的右孩子。V的父節點指向C,C的左右孩子為V,Z。V的左右孩子為X,Y。V的左旋,V成為C的左孩子。

六,AVL樹

定義:平衡因子受限的二叉搜尋樹,各節點的左右子樹高度相差不超過一。插入刪除均在O(LOGN)時間內完成。

1.完全二叉搜尋樹必是AVL樹。

經過插入與刪除而失衡的搜尋樹重新恢復平衡的調整演算法。

插入節點後失衡的節點為X的祖先且高度不低於X的祖父。

平衡演算法:從X節點自低向上找到第一個失衡者。記為G,在X與G的通路上,P為G的孩子,V為P的孩子。
V可能為X,也可能為X的祖先。
最重要的是G,P,V三個節點,找到它們。經過旋轉,使得G重新平衡,則整樹可恢復平衡。

插入演算法:
        確認目標節點不存在,返回其父節點。
        從父節點出發,找到第一個失衡節點:
            若失衡則:
                該節點為G,找到節點V,根據G的孩子高的為P,P的孩子高的為V。若等高,優先取V與P同向                者。

                1.根據G,P,V的不同情況,而進行不同的旋轉。G,P,V的高度發生變化。
                2.根據3+4演算法使其恢復平衡。
                退出。
            不失衡:更新該節點高度。

刪除與插入演算法一樣,只是刪除演算法中只有一個失衡節點。

“3+4演算法”:
        根據G,P,V三者的順序不同,所以connect34的引數也不同。
        P,V同一方向節點,則P->PARENT=G->PARENT,不同則V->PARENT=G->PARENT;
        connect34(a,b,c,T0,T1,T2,T3);
        a,b,c代表G,P,V三者的中序遍歷順序。T0,T1,T2,T3代表四顆子樹的遍歷順序。

connect34: a->lc=T0;if(T0) T0->parent=a;
           a->rc=T1,if(T1) T1->parent=a;updateHeight(a);
           c->lc=T2,if(T2) T2->parent=c;
           c->rc=T3;if(T3) T3->parent=c;updateheight(c);
           b->lc=a;a->parent=b;
           b->rc=c;c->parent=b;updateheight(b);
           return b;

依次類推:對於調整區域性的旋轉問題,也可按類似方法解決。