引言
二叉排序樹簡單的實現在多數情況能夠達到預期的查找效率,但是每個節點只能存儲一個元素和只能有兩個孩子,使得在大量數據下會造成二叉排序樹的深度特別大,那么在進行查找時多次的訪問會造成查找效率的下降,同時,在對二叉查找樹進行插入時,可能會破壞二叉查找樹的平衡。為了降低對於樹的訪問次數,實現樹的平衡,我們需要新的數據結構來處理這樣的問題。
多路查找樹
多路查找樹的每一個節點的孩子樹可以多於兩個,且每個節點處可以存儲多個元素。多路查找樹是一種特殊的查找樹,所以其元素之間存在某種特定的排序關系。
2-3樹
定義2-3樹中每一個節點都具有兩個孩子(我們稱它為2節點)或三個孩子(我們稱它為3節點)。
- 一個2節點包含一個元素和兩個孩子(只能包含兩個孩子或沒有孩子,不能出現有一個孩子的情況),且與二叉排序樹類似,左子樹包含的元素小於該元素,右子樹包含的元素大於該元素。
- 一個3節點包含一小一大兩個元素和三個孩子(只能包含三個孩子或沒有孩子,不能出現有一個孩子或有兩個孩子的情況)。如果某個3節點有孩子,左子樹包含小於較小元素的元素,右子樹包含大於較大元素的元素,中間子樹包含介於兩元素之間的元素。
一顆完美平衡的2-3查找樹中的所有空鏈接到根結點的距離都是相同的。
查找
要判斷查找的鍵值是否在樹中,我們先將它和根結點中的鍵比較。如果它和其中的任何一個相等,查找命中。否則我們就根據比較的結果找到指向相應區間的鏈接,並在其指向的子樹中遞歸地繼續查找。如果這是個空鏈接,查找未命中。
2-3樹的插入實現
要在2-3樹中插入一個新結點,我們可以和二叉查找樹一樣先進行一次未命中的查找,然后把新結點掛在樹的底部。但這樣的話樹無法保持完美平衡性。我們使用2-3樹的主要原因就在於它能夠在插入之后繼續保持平衡。
2-3樹插入可以分為三種情況
1. 對於空樹,插入一個2節點即可
2. 插入節點到一個2節點的葉子上
由於其本身只有一個元素,所以只需要將其升級為3節點即可。
3. 往3節點中插入一個新數據
因為3節點本省就是2-3樹的最大容量(已經有兩個元素),因此需要拆分。分情況討論如下所示:
a. 只有一個3-結點的樹,向其插入一個新鍵
這棵樹唯一的結點中已經沒有可插入的空間了。我們又不能把新鍵插在其空結點上(破壞了完美平衡)。為了將新鍵插入,我們先臨時將新鍵存入該結點中,使之成為一個4-結點。創建一個4-結點很方便,因為很容易將它轉換為一顆由3個2-結點組成的2-3樹(如圖所示),這棵樹既是一顆含有3個結點的二叉查找樹,同時也是一顆完美平衡的2-3樹,其中所有空鏈接到根結點的距離都相等。
b. 向一個父節點為2節點的3節點中插入數據
假設未命中的查找結束於一個3-結點,而它的父結點是一個2-結點。在這種情況下我們需要在維持樹的完美平衡的前提下為新鍵騰出空間。
我們先像剛才一樣構造一個臨時的4-結點並將其分解,但此時我們不會為中鍵創建一個新結點,而是將其移動至原來的父結點中。
這次轉換並沒有影響2-3樹的主要性質,樹仍然是有序的,因為中鍵被移動到父節點中去了。
c. 向一個父節點為3節點的3節點中插入數據
假設未命中的查找結束於一個3-結點,而它的父結點是一個3-結點。
我們再次和剛才一樣構造一個臨時的4-結點並分解它,然后將它的中鍵插入它的父結點中。但父結點也是一個3-結點,因此我們再用這個中鍵構造一個新的臨時4-結點,然后在這個結點上進行相同的變換,即分解這個父結點並將它的中鍵插入到它的父結點中去。
我們就這樣一直向上不斷分解臨時的4-結點並將中鍵插入更高的父結點,直至遇到一個2-結點並將它替換為一個不需要繼續分解的3-結點,或者是到達3-結點的根。
d. 父節點到根節點均是3節點
假設未命中的查找結束於一個3-結點,而它的父結點是一個3-結點。通過觀察,到根節點都是滿3節點。
我們發現(1、3)、(4、6)、(8、12)都是3節點,無法插入,意味着當前我們的樹結構是三層已經不滿足當前節點增加的需要了,於是將(1、3)拆分,(4、6)拆分,(8、12)拆分,樹的深度增加一層。
2-3樹的刪除節點
對於2-3樹的刪除操作,分為3種情況
所刪除元素位於一個3節點的葉子節點上
只需要在該節點處刪除該元素即可,不會影響到整棵樹的其他節點結構。
所刪除的元素位於非葉子的分支節點
通常是將樹按中序遍歷后得到此元素的前驅或后繼元素,讓他們來補位即可,再刪除用於補位的前驅或后繼元素。
如果我們要刪除的分支節點是2節點,如上圖,對於要刪除的元素4,它的前驅是1,后繼是6,顯然(6、7)是3節點,只需要用6來補位即可。這里我們就不講解刪除的分支節點是3節點的情況了,和上述類似。
所刪除的元素位於一個2節點上
我們分析,如果刪除一個2節點,很有可能造成2-3樹平衡破壞的情況,因為對於每一個2節點,要么有兩個子樹要么沒有,對於每一個3節點要么有三個子樹要么沒有,貿然刪除一個2節點,很可能出現平衡遭到破壞,所以我們需要分情況討論。
a. 此節點的雙親節點是2節點,兄弟節點是3節點
將雙親節點移動到當前位置,再將兄弟節點中最接近當前位置的key移動到雙親節點中。
b. 此節點的雙親是2節點,兄弟節點也是2節點
先通過移動兄弟節點的中序遍歷直接后驅到兄弟節點,以使兄弟節點變為3節點;再進行a的操作
此時刪除4,如果直接執行a會造成沒有右孩子,因此需要對整棵樹變形。我們讓節點7變成3節點,那就讓7的直接后繼8下來,再讓比8大的9補充到8的位置,在執行a操作。
c. 此節點的雙親是一個3節點
拆分雙親節點使其成為2節點,再將再將雙親節點中最接近的一個拆分key與中孩子合並,將合並后的節點作為當前節點
d. 當前2-3樹是一個滿二叉樹
將2-3樹層樹減少,並將兄弟節點合並到雙親節點中,同時將雙親節點的所有兄弟節點合並到雙親節點的雙親節點中,如果生成了4節點,再分解4節點即可。
2-3-4樹
2-3-4樹是對2-3樹的概念擴展,包括了4節點的使用。一個4節點中包含小中大三個元素和四個孩子(要么有四個孩子要么沒有,不存在其他情況),如果某個4節點有孩子的話,左子樹包含小於最小元素的元素;第二子樹包含大於最小元素,小於第二元素的元素;第三子樹包含大於第二元素,小於最大元素的元素;右子樹包含大於最大元素的元素。如下圖就是一個2-3-4樹:
總結
- 先找插入結點,若結點有空(即2-結點),則直接插入。如結點沒空(即3-結點),則插入使其臨時容納這個元素,然后分裂此結點,把中間元素移到其父結點中。對父結點亦如此處理。(中鍵一直往上移,直到找到空位,在此過程中沒有空位就先搞個臨時的,再分裂。)
- 2-3樹插入算法的根本在於這些變換都是局部的:除了相關的結點和鏈接之外不必修改或者檢查樹的其他部分。每次變換中,變更的鏈接數量不會超過一個很小的常數。所有局部變換都不會影響整棵樹的有序性和平衡性。
- 同時,通過上面樹的深度增加的例子,可以看出2-3樹和標准二叉樹不同,標准的二叉樹的的深度是由上到下的增加的,而2-3樹的深度生長是由下至上的。