盤點常用的搜索樹


樹的典型應用有很多,比如計算機的文件系統就是一棵樹,根目錄就是根節點。樹的重要應用之一就是搜索樹,搜索樹通常分為二叉搜索樹和多路搜索樹。

二叉搜索樹

二叉搜索樹是一顆有序的樹,每個結點不小於其左子樹任意結點的值,不大於右子樹任意結點的值。二叉搜索樹還有一個有趣的特性,它的中序遍歷得到的是有序數列。

二叉搜索樹能提高搜索的效率,搜索次數最多是樹的深度次,最少能到log(n)。

搜索樹有搜索,插入,刪除等操作。搜索即搜索整棵樹,查找是否有匹配的節點;還有搜索最大值和最小值操作,最大值在樹的最右側,最小值在最左側,因此都很好實現。

treenode find(tree T,int val){   /*搜索操作*/
  if(T==NULL)  return NULL;
  if(T->data > val)
    return find(T->left,val);
  else if(T->data < val)
    return find(T->right,val);
  else
    return T;
}

插入,也是采用遞歸實現,插入的結點都放到了葉子節點。

treenode insert(tree T,int val){
  if(T==NULL) {
    T=(treenode)malloc(sizeof(struct node));
    T->data=val;
    T->right=T->left=NULL;
  }
  else if(T->data >= val)
    T->left = insert(T->left,val);
  else
    T->right = insert(T->right,val);
  return T;
}

刪除,這種操作比較復雜,因為可能會破壞BST的規則,需要進行調整。刪除操作分幾種情況:

1)當刪除的結點是葉子節點,直接刪除即可,如刪除下圖中的7結點;

2)當刪除非葉節點,且該節點只有一個子節點時,我們可以刪除該節點,並讓其父節點連接到它的子節點即可;如刪除8結點,直接讓5結點連接到7即可;

3)當非葉節點,且有兩個子節點時,可以刪除該節點,然后抓出左子樹中最大的元素(或者右樹中最小的元素),來補充被刪元素產生的空缺。但抓出的元素可能也不是葉節點,所以它所產生的空缺需要它的子樹某個結點補充…… 直到最后用來填充的是一個葉節點。上述過程可以遞歸實現。如刪除2結點,可以用1替換或者用3替換。

通常刪除使用遞歸實現,如果非遞歸需要記錄父節點,並且被刪結點是根節點需要特殊處理,比較麻煩。

AVL樹

AVL樹是用發明它的人的名字命令的,AVL樹的任意節點的左子樹深度和右子樹深度相差不超過1。

因此AVL樹是一個平衡的樹,不會偏向一側,保證了搜索的時間復雜度為O(logn)。AVL樹的大部分操作和二叉搜索樹相同,只有插入和刪除操作大有不同,因為可能會破壞平衡,我們需要恢復平衡。樹的平衡需要理解旋轉操作。

(網上很多關於AVL插入調整的博客,刪除的博客較少,下面借鑒Vamei大神的博客,講講插入操作。)

先看看失衡時如何調整為不失衡的例子:

1)一次單旋調整平衡

當插入5結點時,2結點失衡,左子樹深度0,右子樹為2。結點2的右邊超重,因此我們進行左旋,旋轉中心為結點4,旋轉后4為根節點,如圖,恢復了AVL性質。這樣的旋轉稱為‘左單旋’。

同樣的,如果都是指向左邊,那就是左邊超重,進行‘右單旋’。

2)兩次單旋調整平衡

如圖如果插入的是結點3,那么旋轉中心為結點3,先進行一次右單旋,再以3為中心進行一次左單旋即恢復平衡。這種旋轉稱為‘右左旋轉’,同樣還有‘左右旋轉’。

對某個結點進行單旋的結果可以一般化為下圖:

 

總結插入的一般流程:

1.按照常規BST的插入,將節點插為葉節點;

2.從插入節點開始回溯,尋找第一個出現失衡的結點;

3.找到失衡結點(暫稱為結點A)失衡的一側,是左右哪一側導致的失衡;

4.失衡結點的失衡側的根節點(暫稱為結點B),判斷B節點的左右子樹哪個更深;

5.如果步驟3和4的方向一致,均為左(右),則以B結點進行一次右(左)單旋;如果方向不一致,如步驟3為左側失衡,步驟4為右側子樹更深,則以結點B的右結點進行兩次單旋,先左單旋,再右單旋。

#練習幾個實例:

 具體代碼要實現回溯和恢復AVL,因此節點結構需要額外記錄父節點和結點深度。代碼就不細看了,簡單看看Vamei寫的恢復AVL樹的函數代碼:

/* tr是整個AVL樹的根,np是插入節點的位置 */
TREE recover_avl(TREE tr, position np)  {     
    int myDiff;     
    while (np != NULL) {         
        update_root_depth(np);
        myDiff = depth_diff(np);
            if (myDiff > 1 || myDiff < -1) {
               if (myDiff > 1) {                 /* left rotate needed */ 
                   if(depth_diff(np->rchild) > 0) {
                        np = left_single_rotate(np);
                   }
                   else {
                        np = left_double_rotate(np);
                   }
              }
              if (myDiff < -1) {
                  if(depth_diff(np->lchild) < 0) {
                      np = right_single_rotate(np);
                  }
                  else {
                      np = right_double_rotate(np);
                  }
              }
              /* if rotation changes root node */
              if (np->parent == NULL) tr = np;
              break;
        }
        np = np->parent;/* backtracking */
    }
    
    return tr;
}
            

紅黑樹

紅黑樹也是一種自平衡的二叉查找樹,相比於AVL樹,沒有嚴格的深度差要求,但是結點多了顏色的特性。紅黑樹的時間復雜度為O(logn),紅黑樹應用廣泛,Javad的TreeMap、C++STL的map、linux的CFS調度器都是基於紅黑樹實現的。

紅黑樹的特征:

1. 節點是紅色或黑色。

2. 根節點是黑色。

3. 所有葉節點都是黑色。(紅黑樹的葉子節點指的是NULL節點)

4. 每個紅色節點的兩個子節點都是黑色。(所有路徑上不能有兩個連續的紅色節點)

5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。

性質4、5保證了紅黑樹的深度差最大為1倍,也就是保證了樹的平衡,不會倒向一側。

紅黑樹特點:

· 同AVL樹一樣,由於有平衡條件限制,插入和刪除可能需要調整平衡;

· 紅黑樹應用廣泛,實際很少用AVL樹,因為AVL樹對平衡要求過於苛刻,節點變動可能需要很多次旋轉才能恢復平衡,而紅黑樹最多只需要3次旋轉(插入最多兩次,刪除最多3次)就能恢復平衡;

· 紅黑樹查詢效率略低於AVL樹,但是插入刪除效率比AVL樹高;

· 紅黑樹是一種以2叉樹表示的平衡2-3-4叉搜索樹,只不過3叉和4叉的表示形式被做了限定。

參考:(找了幾篇講得不錯的紅黑樹博客)

紅黑樹介紹(插入刪除)

紅黑樹-插入和刪除

紅黑樹揭秘-類比2-3-4叉樹

哈夫曼樹

哈夫曼樹是一種節點帶權的最優二叉樹。最優的意思是該樹的帶權路徑長度最小。

所謂樹的帶權路徑長度,就是樹中所有的葉結點的權值乘上其到根結點的路徑長度。

哈夫曼樹主要內容是建樹,假設已有n個具有權值的結點,構造一個有n個葉節點的哈夫曼樹步驟如下:

1.將結點看成n棵樹組成的森林;

2.從森林中取出根節點權值最小的兩棵樹分別作為左右子樹,形成新的父節點的權值為子節點權值之和,然后將新樹放到森林中;

3.重復步驟2,直到森林中只剩一棵樹。

 

# 哈夫曼編碼

哈夫曼樹的由來,用於變長編碼的。哈夫曼編碼典型應用就是用於字符編碼,計算機中對字符也是采用01編碼,我們最容易想到的是每個字符都采用等長的01串進行編碼,但哈夫曼的變長編碼縮短了編碼長度。哈夫曼的思想是出現頻率高的字符用更短的編碼表示。對每個字符串設定權值,構造一顆哈夫曼樹,左側的路徑編號為0,右傾為1,那么字符的哈夫曼編碼就等於從根到各個字符路徑上01的組合。

由於哈夫曼樹的有效節點都是葉子節點,所以不會產生一個字符的編碼等於另一個字符編碼前綴的情況。解碼方采用同樣的哈夫曼樹規則即可實現解碼。

哈夫曼編碼常用於壓縮文件。

Treap樹堆

Treap = tree + heap,樹堆本身是一顆二叉搜索樹,每個結點附有不同的優先級,單看優先級形成一個堆,所以樹堆其實是樹和堆的組合。樹堆不需要保證樹的深度差,只保證優先級的堆成立。樹結點變化時,通過旋轉使得保持堆的性質。

Splay伸展樹

關於數據查詢有個90-10規律,就是90%的查找發生在10%的數據上。那如果經常查找的數據放得離根結點近就會提高效率,伸展樹就是基於這樣的想法實現的。

伸展樹也是一種自平衡的二叉查找樹,不過它左右子樹的深度差沒有規定。只是規定了每次查找完后通過旋轉將被查找結點移至根節點,旋轉操作可以分為三類,zig,zig-zag,zig-zig操作。伸展樹的平均時間復雜度為O(logn).

最壞情況下,伸展樹會惡化為一條單鏈;伸展樹空間效率很高,不需要額外的標記信息,如深度,顏色等;總體效率在搜索樹中也很高。

B樹

B樹是一種平衡多路查找樹。每個結點可以有多個子節點。

B樹是為文件系統和數據庫而生的,因為磁盤訪問的IO時間相對於計算時間很長,而每次訪問一個結點就要進行一次IO,因此樹深度越小,IO代價越小。於是B樹誕生了,它可以壓縮樹的深度到很小,以減少IO次數。

數據庫索引文件有可能很大,關系型數據存儲了上億條數據,索引文件大則上G,不可能全部放入內存中,而是需要的時候換入內存,方式是磁盤頁。一般來說樹的一個節點就是一個磁盤頁。如果使用二叉查找樹,那么每個節點存儲一個元素,查找到指定元素,需要進行大量的磁盤IO,效率很低。

而B樹解決了這個問題,通過單一節點包含多個data,大大降低了樹的高度,大大減少了磁盤IO次數。一個B樹的結點通常和一個完整的磁盤頁一樣大。

常用於文件系統和部分數據庫索引。

B+樹

和B樹很像,不過B+樹的數據節點都是葉子節點,內部節點不存儲數據。

常用於MySql數據庫的索引。

 

 搜索樹還有很多,如SBT結點平衡搜索樹、替罪羊樹、區間樹、線段樹……

隨着技術發展,不斷有更多的搜索樹被發明出來,還要一直學習呀!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM