1. 程式人生 > >Java 7之集合型別

Java 7之集合型別

1.1  排序二叉樹之插入操作 

 已知一個關鍵字值為key的結點s,若將其插入到二叉排序樹中,只要保證插入後仍符合二叉排序樹的定義即可。插入可以用下面的方法進行: 
    (1)若二叉排序樹是空樹,則key成為二叉排序樹的根; 
    (2)若二叉排序樹非空,則將key與二叉排序樹的根進行比較。如果key的值等於根結點的值,則停止插入;如果key的值小於根結點的值,則將key插入左子樹,如果key的值大於根結點的值,則將key插入右子樹。

    (3)重複步驟2,直到找到合適的插入位置。

1.2  排序二叉樹之刪除操作

當程式從排序二叉樹中刪除一個節點之後,為了讓它依然保持為排序二叉樹,程式必須對該排序二叉樹進行維護。維護可分為如下幾種情況:

(1)被刪除的節點是葉子節點,則只需將它從其父節點中刪除即可。

(2)如果待刪除節點左子樹存在右子樹不存在,或者左子樹不存在右子樹存在。直接將其子樹中存在的一邊候補上來即可。

圖 2 顯示了被刪除節點只有左子樹的示意圖:



圖 3 顯示了被刪除節點只有右子樹的示意圖:


(3)若被刪除節點 p 的左、右子樹均非空,有兩種做法:
  • 將 pL 設為 p 的父節點 q 的左或右子節點(取決於 p 是其父節點 q 的左、右子節點),將 pR 設為 p 節點的中序前趨節點 s 的右子節點(s 是 pL 最右下的節點,也就是 pL 子樹中最大的節點)。
  • 以 p 節點的中序前趨或後繼替代 p 所指節點,然後再從原排序二叉樹中刪去中序前趨或後繼節點即可。(也就是用大於 p 的最小節點或小於 p 的最大節點代替 p 節點即可)。

圖 4 顯示了被刪除節點左右子節點不為空的情形,採用到是第一種方式維護:


圖 5顯示了被刪除節點左右子節點不為空的情形,採用到是第二種方式維護:

TreeMap 刪除節點採用圖 5 所示右邊的情形進行維護——也就是用被刪除節點的右子樹中最小節點與被刪節點交換的方式進行維護。

       在使用第二種方式進行維護時,如果使用前驅節點代替被刪除的節點,則前驅節點可能還存在左子樹(因為前驅節點是根節點左子樹中最右邊的節點),而如果是後繼節點的話,這個後繼節點可能還存在右子樹。他們的處理方法相同,直接將子樹移上去即可。

      一定要理解,無論是前驅還是後繼節點,不可能同時具有左子樹或右子樹,這就為刪除替代節點後的操作帶來了方便。

1.3  排序二叉樹之查詢操作 

       從二叉排序樹中進行查詢時,根據樹的性質,節點的左子樹必定小於根節點,右子樹必定大於根結點。如果查詢的節點值小於根節點,則進入左子樹,大於進入右子樹,重複這個比較步驟直到找到這個節點或者這個節點不存在。 

1.4  總結

      排序二叉樹雖然可以快速檢索,但在最壞的情況下:如果插入的節點集本身就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後得到 的排序二叉樹將變成連結串列:所有節點只有左節點(如果插入節點集本身是大到小排列);或所有節點只有右節點(如果插入節點集本身是小到大排列)。在這種情況 下,排序二叉樹就變成了普通連結串列,其檢索效率就會很差。

2、平衡二叉樹(AVL)

       ALV樹中任何兩個結點的高度差值最大不超過1。樹在查詢、插入和刪除時平均和最壞的情況下都是O(logn)。在一顆二叉搜尋樹查詢一個值的平均時間複雜度為log(n),但是若查詢樹的所有的節點向一邊傾斜,這時候的查詢就退化為線性查詢,複雜度為n。為了獲得更高的查詢效率,就有了AVL樹的概念,對於一顆非平衡的AVL樹,可以通過旋轉變換為AVL樹。


 一棵空樹是平衡二叉樹;若 T 是一棵非空二叉樹,其左、右子樹為 TL 和 TR ,令 hl 和 hr 分別為左、右子樹的深度。當且僅當:
①TL 、 TR 都是平衡二叉樹; 
② | hl - hr |≤ 1;
時,則 T 是平衡二叉樹。由於平衡二叉樹也是排序二叉樹,所以使用二叉樹的插入、刪除和查詢操作即可。只是在操作完成後,為了下次能夠保持一個好的檢索效率,也為了防止這個連結串列退化為普通連結串列,則需要對樹進行旋轉。旋轉可以再次達到平衡。主要包括:左旋轉、右旋轉、左右旋轉、右左旋轉2.1 LL 插入一個新節點到根節點的左子樹的左子樹,導致根節點的平衡因子由1變為2.需要 右 旋轉來解決。


2.2  LR 插入一個新節點到根節點的左子樹的右子樹,導致根節點的平衡因子由1變為2.需要先左旋後右旋轉來解決。


2.3  RR 插入一個新節點到根節點的右子樹的 右 子樹,導致根節點的平衡因子由1變為2.需要 左 旋轉來解決。2.4  RL 插入一個新節點到根節點的 右 子樹的左子樹,導致根節點的平衡因子由1變為2.需要先 右 旋後 左 旋轉來解決。

3、Java中的紅黑樹

      紅黑樹是一個更高效的檢索二叉樹,因此常常用來實現關聯陣列。典型地,JDK 提供的集合類 TreeMap 本身就是一個紅黑樹的實現。排序二叉樹的深度直接影響了檢索的效能,當插入節點本身就是由小到大排列時,排序二叉樹將變成一個連結串列,這種排序二叉樹的檢索效能最低:N 個節點的二叉樹深度就是 N-1。但是紅黑樹通過上面這種限制來保證它大致是平衡的——因為紅黑樹的高度不會無限增高,這樣保證紅黑樹在最壞情況下都是高效的,不會出現普通排序二叉樹的情況。

紅黑樹在原有的排序二叉樹增加了如下幾個要求:

  • 性質 1:每個節點要麼是紅色,要麼是黑色。
  • 性質 2:根節點永遠是黑色的。
  • 性質 3:所有的葉節點都是空節點(即 null),並且是黑色的。
  • 性質 4:每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)
  • 性質 5:從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點。

圖 6. Java 紅黑樹的示意

備註:本文中所有關於紅黑樹中的示意圖採用白色代表紅色。黑色節點還是採用了黑色表示。



  下面來看一下幾個約定:

(1)性質 3 中指定紅黑樹的每個葉子節點都是空節點,而且葉子節點都是黑色 Java 實現的紅黑樹將使用 null 來代表,因此遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每個葉子節點都是紅色的。非葉子節點,也就是非null節點稱為紅黑樹中的兒子節點。

(2) 紅黑樹從根節點到每個葉子節點的路徑都包含相同數量的黑色節點,因此從根節點到葉子節點的路徑中包含的黑色節點數被稱為樹的“黑色高度”。

        對於給定的黑色高度為 N 的紅黑樹,從根到葉子節點的最短路徑長度為 N-1,最長路徑長度為 2 * (N-1)。

        假如有一棵黑色高度為 3 的紅黑樹:從根節點到葉節點的最短路徑長度是 2,該路徑上全是黑色節點(黑節點 - 黑節點 - 黑節點)。最長路徑也只可能為 4,在每個黑色節點之間插入一個紅色節點(黑節點 - 紅節點 - 黑節點 - 紅節點 - 黑節點),性質 4 保證絕不可能插入更多的紅色節點。由此可見,紅黑樹中最長路徑就是一條紅黑交替的路徑。

3.1  讀取操作

      由於紅黑樹只是一個特殊的排序二叉樹,因此對紅黑樹上的只讀操作與普通排序二叉樹上的只讀操作完全相同,只是紅黑樹保持了大致平衡,因此檢索效能比排序二叉樹要好很多。但在紅黑樹上進行插入操作和刪除操作會導致樹不再符合紅黑樹的特徵,因此插入操作和刪除操作都需要進行一定的維護,以保證插入節點、刪除節點後的樹依然是紅黑樹。

     為了在每次插入刪除節點時,滿足紅黑樹的如上一些性質,需要進行節點顏色的變換和進行旋轉(向平衡二叉樹一樣,只是比平衡二叉樹更加嚴格要求),下面就來看一下Java 中實現的紅黑樹。

3.2   插入節點後修復

     每次插入節點後必須進行簡單修復,使該排序二叉樹滿足紅黑樹的要求。插入操作按如下步驟進行:

    (1)以排序二叉樹的方法插入新節點,並將它設為紅色。

    (2)進行顏色調換和樹旋轉。

     在插入操作中,紅黑樹的性質 1 和性質 3 兩個永遠不會發生改變,因此無需考慮紅黑樹的這兩個特性。

     而顏色呼叫和樹旋轉就比較複雜了,下面將分情況進行介紹。在介紹中,我們把新插入的節點定義為 N 節點,N 節點的父節點定義為 P 節點,P 節點的兄弟節點定義為 U 節點,P 節點父節點定義為 G 節點(參見圖7)

下面分成不同情形來分析插入操作。

情形 1:新節點 N 是樹的根節點,沒有父節點

在這種情形下,直接將它設定為黑色以滿足性質 2。

情形 2:新節點的父節點 P 是黑色

在這種情況下,新插入的節點是紅色的,因此依然滿足性質 4。而且因為新節點 N 有兩個黑色葉子節點;但是由於新節點 N 是紅色,通過它的每個子節點的路徑依然保持相同的黑色節點數,因此依然滿足性質 5。

情形 3:如果父節點 P 和父節點的兄弟節點 U 都是紅色

在這種情況下,程式應該將 P 節點、U 節點都設定為黑色,並將 P 節點的父節點設為紅色(用來保持性質 5)。現在新節點 N 有了一個黑色的父節點 P。由於從 P 節點、U 節點到根節點的任何路徑都必須通過 G 節點,在這些路徑上的黑節點數目沒有改變(原來有葉子和 G 節點兩個黑色節點,現在有葉子和 P 兩個黑色節點)。

經過上面處理後,紅色的 G 節點的父節點也有可能是紅色的,這就違反了性質 4,因此還需要對 G 節點遞迴地進行整個過程(把 G 當成是新插入的節點進行處理即可)。

圖 7 顯示了這種處理過程:


雖然圖7 繪製的是新節點 N 作為父節點 P 左子節點的情形,其實新節點 N 作為父節點 P 右子節點的情況與圖 7 完全相同。

情形 4:父節點 P 是紅色、而其兄弟節點 U 是黑色或缺少;且新節點 N 是父節點 P 的右子節點,而父節點 P 又是其父節點 G 的左子節點。

      如上的情況下,需要對新節點和其父節點進行左旋轉操作,接著按情形 5 處理以前的父節點 P(也就是把 P 當成新插入的節點即可)。這導致某些路徑通過它們以前不通過的新節點 N 或父節點 P 的其中之一,但是這兩個節點都是紅色的,因此不會影響性質 5。

圖 8. 插入節點後的樹旋轉


備註:圖 8 中 P 節點是 G 節點的左子節點,如果 P 節點是其父節點 G 節點的右子節點,那麼上面的處理情況應該左、右對調一下。

情形 5:父節點 P 是紅色、而其兄弟節點 U 是黑色或缺少;且新節點 N 是其父節點的左子節點,而父節點 P 又是其父節點 G 的左子節點。

       在這種情形下,需要對節點 G 的一次右旋轉,在旋轉產生的樹中,以前的父節點 P 現在是新節點 N 和節點 G 的父節點。由於以前的節點 G 是黑色,否則父節點 P 就不可能是紅色,我們切換以前的父節點 P 和節點 G 的顏色,使之滿足性質 4,性質 5 也仍然保持滿足,因為通過這三個節點中任何一個的所有路徑以前都通過節點 G,現在它們都通過以前的父節點 P。在各自的情形下,這都是三個節點中唯一的黑色節點。

圖 9. 插入節點後的顏色調整、樹旋轉


備註:圖 9 中 P 節點是 G 節點的左子節點,如果 P 節點是其父節點 G 節點的右子節點,那麼上面的處理情況應該左、右對調一下。


3.3   刪除節點後修復

 如果需要刪除的節點有兩個兒子,那麼問題可以被轉化成刪除另一個只有一個兒子的節點的問題,也就是典型的二叉排序樹的刪除操作。 對於二叉查詢樹,在刪除帶有兩個非葉子兒子的節點的時候,我們找到要麼在它的左子樹中的最大元素、要麼在它的右子樹中的最小元素,並把它的值轉移到要刪除 的節點中。我們接著刪除替換節點。因為只是複製了一個值而不違反任何屬性,這就把問題簡化為如何刪除最多有一個兒子的節點的問題。

       刪除只有一個兒子的節點(如果它兩個兒子都為空,即均為葉子,我們任意將其中一個看作它的兒子)是刪除節點的關鍵:

  • 刪除一個紅色節點,它的父親和兒子一定是黑色的。所以可以簡單的用它的黑色兒子替換它,並不會破壞性質3和4。通過被刪除節點的所有路徑只是少了一個紅色節點,這樣可以繼續保證性質5。
  • 被刪除節點是黑色而它的兒子是紅色的時候。如果只是去除這個黑色節點,用它的紅色兒子頂替上來的話,會 破壞性質4,但是如果我們重繪它的兒子為黑色,則曾經通過它的所有路徑將通過它的黑色兒子,這樣可以繼續保持性質4 
  • 要刪除的節點和它的兒子二者都是黑色的時候,需要進行具體的討論。

 把刪除的節點替換為樹的後繼節點(當然也可以是前驅節點)這個後繼節點假設為 N,它的兄弟節點為S。P節點為N節點的父親,SL為S的左子樹,SR為S的右子樹。如下圖。


情形 1:刪除節點是樹的根節點

從所有路徑去除了一個黑色節點,而新根是黑色的,所以屬性都保持著。舉個例子如下:


注意: 在情況2、5和6下,我們假定 N 是它父親的左兒子。如果它是右兒子,則在這些情況下的左和右應當對調。

情形2. 刪除節點的兄弟節點S是紅色

        在這種情況下在N的父節點P上做左旋轉操作,接著對調 N 的父節點和S節點的顏色。儘管所有的路徑仍然有相同數目的黑色節點,現在 N 有了一個黑色的兄弟和一個紅色的父親,所以我們可以接下去按 4、5或6情況來處理。(它的新兄弟是黑色因為它是紅色S的一個兒子)


情形3: N 的父親、S 和 S 的兒子都是黑色的

       在這種情況下,我們簡單的重繪 S 為紅色。結果是通過S的所有路徑, 它們就是以前不通過 N 的那些路徑,都少了一個黑色節點。因為刪除 N 的初始的父親使通過 N 的所有路徑少了一個黑色節點,這使事情都平衡了起來。但是,通過 P 的所有路徑現在比不通過 P 的路徑少了一個黑色節點,所以仍然違反屬性4。要修正這個問題,我們要從情況 1 開始,在 P 上做重新平衡處理。


情形4. S 和 S 的兒子都是黑色,但是 N 的父親是紅色

       在這種情況下,我們簡單的交換 N 的兄弟和父親的顏色。這不影響不通過 N 的路徑的黑色節點的數目,但是它在通過 N 的路徑上對黑色節點數目增加了一,添補了在這些路徑上刪除的黑色節點。

情形5. S 是黑色,S 的左兒子是紅色,S 的右兒子是黑色,而 N 是它父親的左兒子

      在這種情況下我們在 S 上做右旋轉,這樣 S 的左兒子成為 S 的父親和 N 的新兄弟。我們接著交換 S 和它的新父親的顏色。所有路徑仍有同樣數目的黑色節點,但是現在 N 有了一個右兒子是紅色的黑色兄弟,所以我們進入了情況 6。N 和它的父親都不受這個變換的影響。


情形6. S 是黑色,S 的右兒子是紅色,而 N 是它父親的左兒子

       在這種情況下我們在 N 的父親上做左旋轉,這樣 S 成為 N 的父親和 S 的右兒子的父親。我們接著交換 N 的父親和 S 的顏色,並使 S 的右兒子為黑色。子樹在它的根上的仍是同樣的顏色,所以屬性 3 沒有被違反。但是,N 現在增加了一個黑色祖先: 要麼 N 的父親變成黑色,要麼它是黑色而 S 被增加為一個黑色祖父。所以,通過 N 的路徑都增加了一個黑色節點。

      此時,如果一個路徑不通過 N,則有兩種可能性:

      它通過 N 的新兄弟。那麼它以前和現在都必定通過 S 和 N 的父親,而它們只是交換了顏色。所以路徑保持了同樣數目的黑色節點。 
      它通過 N 的新叔父,S 的右兒子。那麼它以前通過 S、S 的父親和 S 的右兒子,但是現在只通過 S,它被假定為它以前的父親的顏色,和 S 的右兒子,它被從紅色改變為黑色。合成效果是這個路徑通過了同樣數目的黑色節點。 
      在任何情況下,在這些路徑上的黑色節點數目都沒有改變。所以我們恢復了屬性 4。在示意圖中的白色節點可以是紅色或黑色,但是在變換前後都必須指定相同的顏色。


   同樣的,函式呼叫都使用了尾部遞迴,所以演算法是就地的。此外,在旋轉之後不再做遞迴呼叫,所以進行了恆定數目(最多 3 次)的旋轉。

3.4  紅黑樹的優勢

     紅黑樹能夠以O(log2(N))的時間複雜度進行搜尋、插入、刪除操作。此外,任何不平衡都會在3次旋轉之內解決。這一點是AVL所不具備的。


參考文獻: