《數據結構與算法分析》5000字縮寫(中)
作者:matrix67 日期:2006-05-31
研究堆麻煩的地方就是堆的合併。如何把兩個堆合併成一個堆?這個解決了很有用,至少上面的這些操作跟着全部統一了:插入就是與一個單節點的堆合併,刪除根就是把根不要了,把根的左右兩邊(顯然還是堆)合併起來。一個簡單的辦法就是遞歸地不斷把根大的堆往根小的堆的右邊合併,把新得到的堆替換原來的右兒子。注意遞歸過程中哪個根大哪個根小是不停在改變的。這樣下來的結果就是典型的“右傾錯誤”,而且破壞了完全二叉樹的完美。爲此,我們想要隨時保證堆的最右邊儘量少。於是,乾脆不要完全二叉樹了,不過是多寫幾行代碼嘛。這個不存在像二叉查找樹那樣“某一邊越做越多”的退化問題,因爲對於一個堆來說,反正我只管最頂上的東西,下面平不平衡無所謂,只要不擋我合併的道就行。於是,我們想到人爲下一個能讓堆儘量往左邊斜的規定。這個規定就是,對於左右兩個兒子來說,左邊那個離它下面最近的兩個兒子不全(有可能一個都沒有)的節點的距離比右邊那個的遠。這規定看着麻煩,其實還真有效,最右邊的路徑的長比想像中的變得短得多。這就叫左式堆(左偏樹)。這下合併倒是方便了,但合併着合併着要不了多少次右邊又多了。解決的辦法就是想辦法隨時保持左式堆的性質。辦法很簡單,你合並不是遞歸的嗎?每次遞歸一層後再看看左右兩邊兒子離它下面沒有兩個兒子的節點哪個遠,如果右邊變遠了就把左邊右邊調一下。由於我們已經沒有用數組實現這玩意了,因此鏈表搞起很簡單。這個對調左右的方法給了我們一個啓發:哪裏還要管什麼到沒有兩個兒子的節點的距離嘛,既然我每次都在往右合併,我爲什麼不每次合併之後都把它對調到左邊去呢?這種想法是可行的,事實上它還有一個另外的名字,叫斜堆。
二項堆更強,它也是堆,也能合併,不過它已經超越了堆的境界了:它不是一個堆,而是滿屋子的堆。也就是說,找最小值不能再一下子找到了,而是要把二項堆中的每個堆的頂部都看一下。二項堆的合併也很強,直接把根大的堆放在根小的堆的下面。這意味着二項堆的每個堆都可能不是二叉樹了。這增加了編程的難度,不過可以用一個叫做“左兒子右兄弟”的技巧來解決問題。這個技巧,說穿了就是仍然用二叉樹來表示多叉樹:把樹畫好,然後規定節點的左兒子是下一層的最左邊那個,右兒子就是它右邊那個。就是說,左兒子纔是真正的兒子,右兒子不過是一起生出來的。爲了讓二項堆好看些,讓堆的個數和大小保持在一個能快速操作的數目和比例內,二項堆作出了一個明智的規定:每個堆的大小(總的節點個數)只能是1、2、4、8、16…中的一個,且每種大小的堆只能有一個。若干個互不相同的2的冪足以表示任意一個正整數,因此這個規定可以保證不管多大的二項堆都能表示出來。保持這個性質很簡單,遇到兩個大小相等的堆就合併起來成爲一個大一號的堆。由於總是兩個大小相等的堆在合併,因此二項堆中的每一個堆都有一個奇妙的樣子,看看本文結束後下面附的一個大小爲16的堆的示意圖,再看一下,再看一下,你就能體會到了。圖下面有一個用“左兒子右兄弟”法表示的同樣的樹,其中,往下走的線是左兒子,往右走的線是右兒子。
最後簡單說一下Fibonacci堆。保持一個跟着變的數組記錄現在某個節點在堆中的位置,我們還是可以對堆裏的數據進行一些操作的,至少像刪除、改變數值等操作是完全可以的。但這個也需要耗費一些時間。Fibonacci堆相當開放,比二項堆更開放,它可以不花任何時間減少(只能是減少)某個節點的值。它是這樣想的:你二項堆都可以養一屋子的堆,我爲什麼不行呢?於是,它懶得把減小了的節點一點一點地浮上去,而是直接就把它作爲根拿出來當成一個新的堆。每次我要查最小值時我就再像二項堆一樣(但不要求堆的大小了)一個個合併起來還原成一個堆。當然,這樣的做法是有適用範圍的,就是前面說的數值只能是減少。在什麼時候需要一個數值只減少不增加的堆結構呢?莫過於Dijkstra一類的圖論算法了。所以說,這些圖論算法用Fibonacci堆優化可以進一步提速。
Matrix67原創
做人要厚道 轉帖請註明出處