轉自:https://blog.csdn.net/u010224394/article/details/8834969
一:什么是二叉堆
1.1:二叉堆簡介
二叉堆故名思議是一種特殊的堆,二叉堆具有堆的性質(父節點的鍵值總是大于或等于(小于或等于)任何一個子節點的鍵值),二叉堆又具有二叉樹的性質(二叉堆是完全二叉樹或者是近似完全二叉樹)。當父節點的鍵值大于或等于(小于或等于)它的每一個子節點的鍵值時我們稱它為最大堆(最小堆)。
? ? ? 二叉堆多數是以數組作為它們底層元素的存儲,根節點在數組中的索引是1,存儲在第n個位置的父節點它的子節點在數組中的存儲位置為2n與2n+1。可以借用網上的一幅圖來標示這種存儲結構。其中數字表明節點在數組中的存儲位置。
? ? ? ? 1? ? ? ? ? ? ?
? ? ? /? \? ? ? ? ?
? ? 2? ? 3? ? ? ? ? ?
? ? / \? / \? ? ?
? 4? 5? 6? 7? ?
? / \ / \? ? ? ?
8? 9 10 11? ? ?
1.2:二叉堆支持的操作
? ? ? ?二叉堆通常支持以下操作:刪除,插入節點,創建二叉堆。這些操作復雜對都是O(log2n)
? ? ? ?二叉堆也可以支持這些操作:查找。O(n)復雜度。
1.3:二叉堆的特點
? ? ? ?二叉堆是專門為取出最大或最小節點而設計點數據結構,這種數據結構在查找一般元素方面性能和一般數組是沒有多大區別的。二叉堆在取出最大或最最小值的性能表現是O(1),取出操作完成之后,二叉堆需要一次整形操作,以便得到下一個最值,這個操作復雜度O(log2n)。這是一個相當理想的操作時間。但是二叉堆也有一個缺點,就是二叉堆對存儲在內存中的數據操作太過分散,這導致了二叉堆在cpu高速緩存的利用與內存擊中率上面表現不是很好,這也是一個二叉堆理想操作時間所需要付出的代價。
1.4:二叉堆的使用范圍
? ? ? ?二叉堆主要的應用擊中在兩個地方一個是排序,一個是基于優先級隊列的算法。比如:
? ? ? ?1:A*尋路
? ? ? ?2:統計數據(維護一個M個最小/最大的數據)
? ? ? ?3:huffman code(數據壓縮)
? ? ? ?4:Dijkstra's algorithm(計算最短路徑)
? ? ? ?5:事件驅動模擬(粒子碰撞。這個比較有意思,從國外的一個網站看到過)
? ? ? ?5:貝葉斯垃圾郵件過濾(這個只是聽過沒怎么了解)
2.1:插入
? ? ? 當我們要在二叉堆中插入一個元素時我們通常要做的就是有三步
? ? ? 1.把要插入的節點放在二叉堆的最末端
? ? ? 2.把這個元素和它的父節點進行比較,如果符合條件或者該節點已是頭結點插入操作就算完成了
? ? ? 3.如果不符合條件的話就交換該節點和父節點位置。并跳到第二步。
假設我們有一個如下的最大二叉堆,圓圈內數字代表的是節點,x代表節點插入位置,我們要插入的值是15,則步驟如下:
我們插入的位置為X,X的父節點是8,X與8進行比較,發現X(15)大于8于是8與15互換。
X(15)接著和11比較,發現15比11大于是互換。
15已經是頭結點操作插入操作結束。
? ? ? ?插入節點不停向上比較的過程叫做向上整形。
voidinsert(Data data)
{
if(_last_index==0)//我們的數組從index 1,我們用第一個插入的數填充index 0.
{
_array.push_back(key);
}
_array.push_back(data);//將key插入數組最末
swim_up(++_last_index);//對最后一個插入的數字進行向上整形
}
voidswim_up(size_type n)//向上整形
{
size_type j;//n 代表向上整形的元素,j代表n的父節點
while( (j = n /2) >0&& compare(_array[n],? _array[j]) )//判斷n父節點是否為空&比較n與j大小
{
exchange(n, j);
n=j;
}
}
2.2:刪除
? ? ? ?二叉堆的刪除操作指的是刪除頭結點的操作,也就是最小或者最大的元素。刪除操作分為三步:
? ? ? 1.首先將頭結點與最后一個節點位置互換(互換之后的最后一個節點就不再視為二叉堆的一部分)。
? ? ? 2.將互換之后的新的頭結點與子節點進行比較,如果符合條件或者該節點沒有子節點了則操作完成。
? ? ? 3.將它和子節點互換,并重復步驟2。(如果它的兩個子節點都比它大/小,那么它與其中較大/小的一個互換位置。最大堆的話與較大的互換,最小堆的話與較小的互換。)
假設我們有如下一個最大堆,圓圈內數字表示節點的值:
現在我們刪除頭結點11,我們將11頭結點與最末一個節點4互換。
互換之后我們剔除了最后一個節點。我們將4與它的子節點進行比較,發現它比它的兩個節點都小,不滿足條件跳到步驟3。
我們將4與它的子節點中較大的一個進行互換(最小堆則和最小的一個互換)。然后繼續進行步驟2,但是我們發現節點4已經沒有子節點于是操作結束。
? ? ? ?這個不停向下比較的操作我們稱作向下整形。
constT&get_min()//不允許修改值,這樣會造成堆被破壞.
? ? ? ? ? ? ? ? {
return_array[1];
? ? ? ? ? ? ? ? }
voidpop_min()//如果沒有數據在隊列中,這個行為是未定義的.
? ? ? ? ? ? ? ? {
_array[1]=_array[_last_index--];
? ? ? ? ? ? ? ? ? ? ? ? _array.pop_back();
sink_down(1);
? ? ? ? ? ? ? ? }
voidsink_down(size_type n)
? ? ? ? ? ? ? ? {
size_type j;//j 是 n的子節點的索引
while( ( j =2* n) <= _last_index )
? ? ? ? ? ? ? ? ? ? ? ? {
if( j +1<= _last_index && _compare(_array[j+1],_array[j]) )//比較兩個子節點,取出其中較小的.
j=j+1;
if( _compare(_array[j],_array[n]) )//較小的子節點與父節點進行比較
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? exchange(n,j);
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? n=j;
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
2.3:構建二叉堆
? ? ? ?構建二叉堆很簡單只要我們把要構建的元素一個一個的進行插入操作插入到二叉堆中即構建了一個二叉堆。
2.4:堆排序
? ? ? ?堆排序其實分為以下幾步:
? ? ? ?1:首先將待排序的n個元素構建一個二叉堆array[n]
? ? ? ?2:執行刪除操作,只是這里我們并不是刪除頭結點,而是將頭結點換到二叉堆末尾,并形成一個出去隊列末尾的新二叉堆。
? ? ? ?3:重復步驟2,直到刪除了最后一個元素。
這個過程其實就是先構建一個最大/最小二叉堆,然后不停的取出最大/最小元素(頭結點),插入到新的隊列中,以此達到排序的目的。觀察下面這個從wiki上面截取的gif圖。
這幅圖描述的是一個最大堆,柱子的高度代表了元素的大小,可以看出不停的把頭結點換到新形成的二叉堆的最末,然后就形成了一個有序隊列。
A*尋路:
? ? ? ? ? ? 這里只是舉一個相對于來說比較簡單的例子,用A*尋路來解決8-PUZZLE(8格數字拼圖),當然更為經典的一種是15-puzzle,它們道理都是一樣的。下面來看看這個問題的描述。
? ? ? ? ? ? 在一個九宮格里面,有1-8 8個數字和一個空格,我們可以移動空格上下左右相鄰的數字到空格,然后通過這種移動方式我們最終要求9宮格里面的數字,形成1-8的順訊排列。類似如下
1 3? ? ? 1? 3? ? 1 2 3? ? ? 1 2 3? ? ? 1 2 3
4 2 5 =>? 4 2 5 =>? 4? 5 =>? 4 5? ? =>? 4 5 6
7 8 6? ? 7 8 6? ? 7 8 6? ? ? 7 8 6? ? ? 7 8
初始 第1次移動 第2次移動 第3次移動 第4次移動
? ? ? ? ? ?這個問題在我小時候玩圖片拼板的時候很難,幾乎很久都拼不成功,但是我們只要找到決竅就行了。有兩種訣竅是廣泛使用的一種稱作Hamming priority function,而另外一種就是Manhattan priority function。這里我們使用更為廣泛使用的Manhattan方法作為講解。
? ? ? ? ? ?Manhattan方法:我們用這個9宮格里面每個數字到達自己指定位置的距離加上我們目前總共移動的步數來表示一個度量值M。這里所指的每個數字到達自己指定位置的距離指的是通過橫向移動和縱向移動到達自己規定位置的距離。舉例:
? ? ? ? ?1 ? 3 ??
? ? ? ? ?4 ? 2 ? 5
? ? ? ? ?7 ? 8 ? 6?
在這里圖中數字“1”在位置1上于是距離為0。數字“3”到達自己的指定位置需要右移一步于是距離為1,“4"在位置4上于是距離為0,"2"需要向上移動一步到達自己的制定位置距離為1,”5“需要左移一步距離為1,”7“”8“在指定位置上距離0,6需要向上移動一步距離1,于是這個圖形的總距離為4。 ? ? ??
? ? ? ? ? 我們從上圖的”初始“狀態開始,有兩種移動方法,一種是”3“移動到空格,一種是”5“移動到空格。我們應該選擇哪種移動方法呢,這個時候就需要使用我們剛才所說的度量值了,我們選擇度量值小的一種移動方式。”3“移動到空格的方法距離3,移動步數1,度量值M=4。”5“移動到度量空格的距離5,移動步數為1,度量值M=6。我們選擇”3“移動到空格的這種方式。這里的具體過程是我們把記錄下“3”和“5”移動的這兩種節點的父節點,然后分別計算他們的M值,然后放入到min bianry heap中,取出最小M值節點作為移動節點,并從min
binary heap中刪除這個節點。
1? 3? ? ? ? ? ? ? ? ? ? ? ? ? ? 1 3 5
4 2 5? ? ? ? ? ? ? ? ? ? ? ? ? ? 4 2
7 8 6? ? ? ? ? ? ? ? ? ? ? ? ? ? 7 8 6
"3"移動到空格,M=4? ? ? ? ? ? ? ? “5”移動到空格,M=6
當我們選出了第一次的移動節點之后,我們就要在第一次的移動節點上再決定下一次的移動節點,下一次怎么走一共有3+1種節點,3種是基于上一次移動后我們新加入的移動節點,1種是上一次我們并沒有沿用的移動節點,我們計算3種新節點的M值并記錄他們的父節點然后再把它們加入取出最小的作為下一次的移動節點,直到我們得到距離等于0的節點位置。
當我們找到距離等于0的節點之后,我們遞歸查找該節點父節點直到查找到根節點位置,這個查找的順續的逆序便是我們移動節點到達最終目的地的順序
這里有一個A*尋路中需要注意的地方,我們并不會刪除我們沒有沿用的節點,而是仍然留住它在min binary heap中作為備選節點以防現有路線不是最優解或是不能到達終點。
? 這種數字拼盤程序還有一種非常值得注意的地方,即是這種數字拼盤總是存在著一種無法求解的可能,比如8-puzzle中,這種排序和它的變種都無法解:
1 2 3?
4 5 6?
8 7
面對這種難題,有一種較為合理的解決方法來判斷,我們只需要交換我們初始節點中同一排的相鄰兩個節點位置(兩個都為非空節點)得到另外一種初始化節點,在這兩種方案中只有一種方案能夠解。所以我們只需要同時計算兩種初始節點,只要其中一個得出解了那么另外一個即可以判斷是無解的了。
好奇的你或許會問為什么交換了同一排相鄰的兩個非空節點的位置之后,新得到的節點的可解性與舊節點的可解性相反。這個問題嚴謹的數學解釋需要參考較早的研究論文,并且對于非專業學生也比較晦澀難懂。我能想到的比較容易解釋方式及是“同一排兩個節點交換了位置之后,你永遠也無法通過移動還原到交換前的模樣。”這也即是
1 2 3? ? ? ? 1 2 3
4 5 6 得不到=>4 5 6 的原因。
8 7? ? ? ? ? 7 8
實現:
? ? ? ? ? 下面是這個8-puzzle問題的代碼實現,零零散散寫了2,3百行的code,寫得比較隨意,代碼的泛型沒有做,所以暫時只支持8-puzzle問題的求解,但是只要稍微改動下就能支持n puzzle問題了。因為寫的較快,注釋暫時忽略了.........。代碼的輸出在標準輸出上,運用了上面講到的技術判斷不能求解的情況,二叉堆底層使用的vector,支持泛型。
---------------------本文來自 JoeyMIao 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/u010224394/article/details/8834969?utm_source=copy