《數據結構與算法分析》5000字縮寫(中)

《數據結構與算法分析》5000字縮寫(中)

    堆,就是一陀一陀的東西。頭重腳輕不算堆,要上面小下面大才算一個堆。堆是一棵二叉樹,滿足下面的始終比上面的大。它和二叉查找樹比較起來既有好的又有不好的:好的就是要想知道數據裏的最小值時根本就不用找了,直接就是最頂上的那個了;不好的就是堆除了這個以外基本上不能做別的事了。除了最頂上的那個以外,你幾乎沒辦法控制其餘的部分。當然,插入和刪除數據這種基本操作還是可以做的。插入就是把數據暫時先放在最下面的某個位置,然後通過與它上面一個進行比較、交換不斷往上冒直到已經到了自己的位置不能再向上爲止。刪除反起來,通過不斷交換往下沉一直沉到底。因爲是往下走,所以要考慮到一個把左邊的放上來還是把右邊的放上來的問題。當然,爲了保證堆上小下大的性質,應該把小的一邊換上來。剛纔說過,由於你只能“看”到最頂上的東西,不知道中間部分是什麼樣,我們通常只刪除最小的(最上面的)那個節點。其實堆還有一個最大的好處:容易寫代碼。因爲我們可以有意讓數據把樹“排得滿滿的”,滿到它是一行一行挨着排下來的。這叫做“完全二叉樹”。我們可以給完全二叉樹編個號,從上到下從左到右挨着數下來。根是1,找左兒子就乘2,找右兒子就乘2加1,找它爸就 div 2。以後叫誰就是誰,很方便。這樣整個樹就可以用一個數組實現了。由於堆基本上只用來找最小,因此如果某個問題要求很複雜的話,最好還是用成二叉查找樹;當然,如果問題只要求插入、刪除和找最小三種操作,你應該毫不猶豫地選擇堆,畢竟找最小時堆方便得多,寫起又簡單。什麼時候出現這種問題呢?比如說,我的女友排起隊的,我每次要選一個最純潔的,就是受那些的影響最小的人。每當我遇見了一個新的美女,我就把她放在這個隊伍裏合適的位置供我以後娛樂。這時,我只關心每次插入、取最小和刪最小。這個隊伍就可以用一個堆來優化。因此,堆還有一個形象的名字叫優先隊列。如果誰問題目要求不找最小找最大怎麼辦,那人肯定是個傻子,把堆變通一下,上大下小不就完了嗎?

    研究堆麻煩的地方就是堆的合併。如何把兩個堆合併成一個堆?這個解決了很有用,至少上面的這些操作跟着全部統一了:插入就是與一個單節點的堆合併,刪除根就是把根不要了,把根的左右兩邊(顯然還是堆)合併起來。一個簡單的辦法就是遞歸地不斷把根大的堆往根小的堆的右邊合併,把新得到的堆替換原來的右兒子。注意遞歸過程中哪個根大哪個根小是不停在改變的。這樣下來的結果就是典型的“右傾錯誤”,而且破壞了完全二叉樹的完美。爲此,我們想要隨時保證堆的最右邊儘量少。於是,乾脆不要完全二叉樹了,不過是多寫幾行代碼嘛。這個不存在像二叉查找樹那樣“某一邊越做越多”的退化問題,因爲對於一個堆來說,反正我只管最頂上的東西,下面平不平衡無所謂,只要不擋我合併的道就行。於是,我們想到人爲下一個能讓堆儘量往左邊斜的規定。這個規定就是,對於左右兩個兒子來說,左邊那個離它下面最近的兩個兒子不全(有可能一個都沒有)的節點的距離比右邊那個的遠。這規定看着麻煩,其實還真有效,最右邊的路徑的長比想像中的變得短得多。這就叫左式堆(左偏樹)。這下合併倒是方便了,但合併着合併着要不了多少次右邊又多了。解決的辦法就是想辦法隨時保持左式堆的性質。辦法很簡單,你合並不是遞歸的嗎?每次遞歸一層後再看看左右兩邊兒子離它下面沒有兩個兒子的節點哪個遠,如果右邊變遠了就把左邊右邊調一下。由於我們已經沒有用數組實現這玩意了,因此鏈表搞起很簡單。這個對調左右的方法給了我們一個啓發:哪裏還要管什麼到沒有兩個兒子的節點的距離嘛,既然我每次都在往右合併,我爲什麼不每次合併之後都把它對調到左邊去呢?這種想法是可行的,事實上它還有一個另外的名字,叫斜堆。

    二項堆更強,它也是堆,也能合併,不過它已經超越了堆的境界了:它不是一個堆,而是滿屋子的堆。也就是說,找最小值不能再一下子找到了,而是要把二項堆中的每個堆的頂部都看一下。二項堆的合併也很強,直接把根大的堆放在根小的堆的下面。這意味着二項堆的每個堆都可能不是二叉樹了。這增加了編程的難度,不過可以用一個叫做“左兒子右兄弟”的技巧來解決問題。這個技巧,說穿了就是仍然用二叉樹來表示多叉樹:把樹畫好,然後規定節點的左兒子是下一層的最左邊那個,右兒子就是它右邊那個。就是說,左兒子纔是真正的兒子,右兒子不過是一起生出來的。爲了讓二項堆好看些,讓堆的個數和大小保持在一個能快速操作的數目和比例內,二項堆作出了一個明智的規定:每個堆的大小(總的節點個數)只能是1、2、4、8、16…中的一個,且每種大小的堆只能有一個。若干個互不相同的2的冪足以表示任意一個正整數,因此這個規定可以保證不管多大的二項堆都能表示出來。保持這個性質很簡單,遇到兩個大小相等的堆就合併起來成爲一個大一號的堆。由於總是兩個大小相等的堆在合併,因此二項堆中的每一個堆都有一個奇妙的樣子,看看本文結束後下面附的一個大小爲16的堆的示意圖,再看一下,再看一下,你就能體會到了。圖下面有一個用“左兒子右兄弟”法表示的同樣的樹,其中,往下走的線是左兒子,往右走的線是右兒子。

    最後簡單說一下Fibonacci堆。保持一個跟着變的數組記錄現在某個節點在堆中的位置,我們還是可以對堆裏的數據進行一些操作的,至少像刪除、改變數值等操作是完全可以的。但這個也需要耗費一些時間。Fibonacci堆相當開放,比二項堆更開放,它可以不花任何時間減少(只能是減少)某個節點的值。它是這樣想的:你二項堆都可以養一屋子的堆,我爲什麼不行呢?於是,它懶得把減小了的節點一點一點地浮上去,而是直接就把它作爲根拿出來當成一個新的堆。每次我要查最小值時我就再像二項堆一樣(但不要求堆的大小了)一個個合併起來還原成一個堆。當然,這樣的做法是有適用範圍的,就是前面說的數值只能是減少。在什麼時候需要一個數值只減少不增加的堆結構呢?莫過於Dijkstra一類的圖論算法了。所以說,這些圖論算法用Fibonacci堆優化可以進一步提速。



Matrix67原創
做人要厚道 轉帖請註明出處 
發佈了2 篇原創文章 · 獲贊 13 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章