堆,就是一陀一陀的東西。頭重腳輕不算堆,要上面小下面大才算乙個堆。堆是一棵二叉樹,滿足下面的始終比上面的大。它和二叉查詢樹比較起來既有好的又有不好的:好的就是要想知道資料裡的最小值時根本就不用找了,直接就是最頂上的那個了;不好的就是堆除了這個以外基本上不能做別的事了。除了最頂上的那個以外,你幾乎沒辦法控制其餘的部分。當然,插入和刪除資料這種基本操作還是可以做的。插入就是把資料暫時先放在最下面的某個位置,然後通過與它上面乙個進行比較、交換不斷往上冒直到已經到了自己的位置不能再向上為止。刪除反起來,通過不斷交換往下沉一直沉到底。因為是往下走,所以要考慮到乙個把左邊的放上來還是把右邊的放上來的問題。當然,為了保證堆上小下大的性質,應該把小的一邊換上來。剛才說過,由於你只能「看」到最頂上的東西,不知道中間部分是什麼樣,我們通常只刪除最小的(最上面的)那個節點。其實堆還有乙個最大的好處:容易寫**。因為我們可以有意讓資料把樹「排得滿滿的」,滿到它是一行一行挨著排下來的。這叫做「完全二叉樹」。我們可以給完全二叉樹編個號,從上到下從左到右挨著數下來。根是1,找左兒子就乘2,找右兒子就乘2加1,找它爸就 div 2。以後叫誰就是誰,很方便。這樣整個樹就可以用乙個陣列實現了。由於堆基本上只用來找最小,因此如果某個問題要求很複雜的話,最好還是用成二叉查詢樹;當然,如果問題只要求插入、刪除和找最小三種操作,你應該毫不猶豫地選擇堆,畢竟找最小時堆方便得多,寫起又簡單。什麼時候出現這種問題呢?比如說,我的女友排起隊的,我每次要選乙個最純潔的,就是受那些的影響最小的人。每當我遇見了乙個新的美女,我就把她放在這個隊伍裡合適的位置供我以後娛樂。這時,我只關心每次插入、取最小和刪最小。這個隊伍就可以用乙個堆來優化。因此,堆還有乙個形象的名字叫優先佇列。如果誰問題目要求不找最小找最大怎麼辦,那人肯定是個傻子,把堆變通一下,上大下小不就完了嗎?
研究堆麻煩的地方就是堆的合併。如何把兩個堆合併成乙個堆?這個解決了很有用,至少上面的這些操作跟著全部統一了:插入就是與乙個單節點的堆合併,刪除根就是把根不要了,把根的左右兩邊(顯然還是堆)合併起來。乙個簡單的辦法就是遞迴地不斷把根大的堆往根小的堆的右邊合併,把新得到的堆替換原來的右兒子。注意遞迴過程中哪個根大哪個根小是不停在改變的。這樣下來的結果就是典型的「右傾錯誤」,而且破壞了完全二叉樹的完美。為此,我們想要隨時保證堆的最右邊盡量少。於是,乾脆不要完全二叉樹了,不過是多寫幾行**嘛。這個不存在像二叉查詢樹那樣「某一邊越做越多」的退化問題,因為對於乙個堆來說,反正我只管最頂上的東西,下面平不平衡無所謂,只要不擋我合併的道就行。於是,我們想到人為下乙個能讓堆盡量往左邊斜的規定。這個規定就是,對於左右兩個兒子來說,左邊那個離它下面最近的兩個兒子不全(有可能乙個都沒有)的節點的距離比右邊那個的遠。這規定看著麻煩,其實還真有效,最右邊的路徑的長比想像中的變得短得多。這就叫左式堆(左偏樹)。這下合併倒是方便了,但合併著合併著要不了多少次右邊又多了。解決的辦法就是想辦法隨時保持左式堆的性質。辦法很簡單,你合併不是遞迴的嗎?每次遞迴一層後再看看左右兩邊兒子離它下面沒有兩個兒子的節點哪個遠,如果右邊變遠了就把左邊右邊調一下。由於我們已經沒有用陣列實現這玩意了,因此鍊錶搞起很簡單。這個對調左右的方法給了我們乙個啟發:**還要管什麼到沒有兩個兒子的節點的距離嘛,既然我每次都在往右合併,我為什麼不每次合併之後都把它對調到左邊去呢?這種想法是可行的,事實上它還有乙個另外的名字,叫斜堆。
二項堆更強,它也是堆,也能合併,不過它已經超越了堆的境界了:它不是乙個堆,而是滿屋子的堆。也就是說,找最小值不能再一下子找到了,而是要把二項堆中的每個堆的頂部都看一下。二項堆的合併也很強,直接把根大的堆放在根小的堆的下面。這意味著二項堆的每個堆都可能不是二叉樹了。這增加了程式設計的難度,不過可以用乙個叫做「左兒子右兄弟」的技巧來解決問題。這個技巧,說穿了就是仍然用二叉樹來表示多叉樹:把樹畫好,然後規定節點的左兒子是下一層的最左邊那個,右兒子就是它右邊那個。就是說,左兒子才是真正的兒子,右兒子不過是一起生出來的。為了讓二項堆好看些,讓堆的個數和大小保持在乙個能快速操作的數目和比例內,二項堆作出了乙個明智的規定:每個堆的大小(總的節點個數)只能是1、2、4、8、16…中的乙個,且每種大小的堆只能有乙個。若干個互不相同的2的冪足以表示任意乙個正整數,因此這個規定可以保證不管多大的二項堆都能表示出來。保持這個性質很簡單,遇到兩個大小相等的堆就合併起來成為乙個大一號的堆。由於總是兩個大小相等的堆在合併,因此二項堆中的每乙個堆都有乙個奇妙的樣子,看看本文結束後下面附的乙個大小為16的堆的示意圖,再看一下,再看一下,你就能體會到了。圖下面有乙個用「左兒子右兄弟」法表示的同樣的樹,其中,往下走的線是左兒子,往右走的線是右兒子。
最後簡單說一下fibonacci堆。保持乙個跟著變的陣列記錄現在某個節點在堆中的位置,我們還是可以對堆裡的資料進行一些操作的,至少像刪除、改變量值等操作是完全可以的。但這個也需要耗費一些時間。fibonacci堆相當開放,比二項堆更開放,它可以不花任何時間減少(只能是減少)某個節點的值。它是這樣想的:你二項堆都可以養一屋子的堆,我為什麼不行呢?於是,它懶得把減小了的節點一點一點地浮上去,而是直接就把它作為根拿出來當成乙個新的堆。每次我要查最小值時我就再像二項堆一樣(但不要求堆的大小了)乙個個合併起來還原成乙個堆。當然,這樣的做法是有適用範圍的,就是前面說的數值只能是減少。在什麼時候需要乙個數值只減少不增加的堆結構呢?莫過於dijkstra一類的圖論演算法了。所以說,這些圖論演算法用fibonacci堆優化可以進一步提速。
matrix67原創
做人要厚道 **請註明出處
《資料結構與演算法分析》5000字縮寫(下)
p x 表示元素x的父親的位置。一開始,p x 都等於x自己,表示自己乙個人是乙個集合。函式find set x 將返回x所在集合 一棵樹 的根。並查集還有些其它的剪枝和一些很複雜的效率分析問題,這裡不多說了。寫到這裡,資料結構與演算法分析 中的幾個大塊內容算是說清楚了。由於本文的敘述調整了原書各章...
資料結構與演算法分析
資料結構與演算法分析可以稱得上是程式設計師必須修煉的內功心法。資料的儲存結構 資料元素在計算機中的儲存方式 資料的操作集合 對一種資料型別的資料所有操作,例如對資料的增刪改查等等!演算法分析主要分析 下面是一些常用資料結構 一 線性表 1.陣列實現 2.鍊錶 二 棧與佇列 三 樹與二叉樹 1.樹2....
資料結構與演算法分析
資料結構 大量資料的組織方法 演算法分析 演算法執行時間的估算。涉及到計算效率。設想,如果能把時間限制從16年減至不到1秒,不很神奇嗎?在很多問題中,乙個重要的觀念是 寫出乙個可以工作的程式並不夠。如果這個程式在巨大的資料集上執行,執行時間就成了重要的 問題。演算法,是為求解乙個問題需要遵循的 被清...