常用資料結構與演算法:二叉堆(binary heap)
一:什麼是二叉堆
二:二叉堆的實現
三:使用二叉堆的幾個例子
一:什麼是二叉堆
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:二叉堆的使用範圍
二:二叉堆的實現
2.1:插入
當我們要在二叉堆中插入一個元素時我們通常要做的就是有三步 1.把要插入的節點放在二叉堆的最末端 2.把這個元素和它的父節點進行比較,如果符合條件或者該節點已是頭結點插入操作就算完成了 3.如果不符合條件的話就交換該節點和父節點位置。並跳到第二步。 假設我們有一個如下的最大二叉堆,圓圈內數字代表的是節點,x代表節點插入位置,我們要插入的值是15,則步驟如下:我們插入的位置為X,X的父節點是8,X與8進行比較,發現X(15)大於8於是8與15互換。
X(15)接著和11比較,發現15比11大於是互換。
15已經是頭結點操作插入操作結束。 插入節點不停向上比較的過程叫做向上整形。
void insert(Data data)
{
if(_last_index==0) //我們的陣列從index 1,我們用第一個插入的數填充index 0.
{
_array.push_back(key);
}
_array.push_back(data); //將key插入陣列最末
swim_up(++_last_index); //對最後一個插入的數字進行向上整形
}
void swim_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已經沒有子節點於是操作結束。 這個不停向下比較的操作我們稱作向下整形。
const T& get_min() //不允許修改值,這樣會造成堆被破壞.
{
return _array[1];
}
void pop_min() //如果沒有資料在佇列中,這個行為是未定義的.
{
_array[1]=_array[_last_index--];
_array.pop_back();
sink_down(1);
}
void sink_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,支援泛型。相關推薦
常用資料結構與演算法:二叉堆(binary heap)
一:什麼是二叉堆 二:二叉堆的實現 三:使用二叉堆的幾個例子 一:什麼是二叉堆 1.1:二叉堆簡介 二叉堆故名思議是一種特殊的堆,二叉堆具有堆的性質(父節點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值),二叉堆又具有二叉樹的性質(二叉堆是完全二叉樹
資料結構與演算法:二叉樹
二叉樹是一種非常常見並且實用的資料結構,它結合了有序陣列與連結串列的優點。在二叉樹中查詢資料與在陣列中查詢資料一樣快,在二叉樹中新增、刪除資料的速度也和在連結串列中一樣高效,所以有關二叉樹的相關技術一直是程式設計師面試筆試中必考的知識點。 基礎知識 二叉樹(Bi
資料結構與演算法:二叉排序樹
二叉排序樹 二叉排序樹(Binary Sort Tree),又稱二叉查詢樹(Binary Search Tree),亦稱二叉搜尋樹。是資料結構中的一類。在一般情況下,查詢效率比連結串列結構要高。 二叉排序樹的定義: 當左子樹不為空時,左子樹上的所有節點值都小於左子樹的根節點值 當右子樹不為空時,右子樹
17_資料結構與演算法_二叉堆_Python實現
#Created By: Chen Da #先實現一個二叉堆,基於完整二叉樹 class Binary_heap(object): def __init__(self): self.heap_list = [0] #一個空的二叉堆以零作為第一個元素,方
python 資料結構與演算法 day05 二叉樹的深度優先遍歷(縱向)
1. 二叉樹深度優先遍歷三種方式 不同於樹的廣度優先遍歷(一層一層的走,同一層從左到右走完開始走下一層的橫向遍歷方式),深度優先遍歷是一條路走到黑,然後再走下一條; 先序遍歷:根節點--左子節點---右子節點(先從根節點開始,走左子樹,對這個左子樹依然按照根節點
14_資料結構與演算法_二叉樹_Python實現
#Created By: Chen Da class BinaryTree(object): def __init__(self,rootObj): self.key = rootObj self.left_child = None sel
資料結構與演算法總結——二叉查詢樹及其相關操作
我實現瞭如下操作 插入,查詢,刪除,最大值 樹的高度,子樹大小 二叉樹的範圍和,範圍搜尋 樹的前序,中序,後序三種遍歷 rank 前驅值 在這一版本的程式碼中,我使用了類模板將介面與實現分
資料結構與演算法學習--二叉樹及二叉搜尋樹
可以看下以前對數的總結https://blog.csdn.net/sjin_1314/article/details/8507490 下面是二叉樹的遍歷,建立及銷燬的函式實現,層次遍歷依賴佇列;佇列實現可以去github上檢視https://github.com/jin13417/al
資料結構與演算法篇 二叉樹(Binary Tree)(二)
今天要講的是二叉查詢樹(Binary Search Tree),是一種最常用的二叉搜尋樹,支援快速查詢,刪除,插入資料。 它是如何實現的呢?,其實它依靠的它的資料結構,在樹中的任意一個節點,其左子樹的每個節點的值都小於這個節點的值,右子樹都大於這個節點的值。 接下來我們來看一下二叉樹是
資料結構與演算法篇 二叉樹(Binary Tree)(一)
好多天沒有寫過資料結構和演算法了,好了今天抽出點時間二叉樹,前面講到的都是線性表,棧,佇列等等。 今天講到的是非線性表結構--樹,首先說一下什麼是樹的概念 樹的這種資料結果挺像我們現實中的樹,這裡的每一個元素我們叫做節點,用線把相鄰的節點連線起來,然後它們就成了父子關係。 A節點是
資料結構與演算法之二叉搜尋樹插入、查詢與刪除
1 二叉搜尋樹(BSTree)的概念 二叉搜尋樹又被稱為二叉排序樹,那麼它本身也是一棵二叉樹,那麼滿足以下性質的二叉樹就是二叉搜尋樹,如圖: 若左子樹不為空,則左子樹上所有節點的值都小於根節點的值; 若它的右子樹不為空,則它的右子樹上所有節點的值都大於
修煉內功---資料結構與演算法29---二叉樹的儲存
樹和二叉樹的定義和特性,樹這種結構不能簡單通過線性表的前後關係來儲存,線上性表中,一個節點只有至多一個前驅節點和至多一個後驅節點,樹則不然,一個節點可能有多個後驅節點,這個時候,我們需要通過更加複雜的結構才能儲存樹。二叉樹是一種特殊的樹,比多叉樹要簡單,因為特定節點至多隻有兩個節點,這就極大簡化了相
資料機構與演算法:二叉查詢樹(Binary Search Tree)Java實現
個人總結,如有錯誤,感謝指正 二叉查詢樹(Binary Search Tree) 一、簡介 二叉樹(Binary Tree):每個節點最多有兩個子節點的樹。 二叉查詢樹(binary srarch tree):具有如下性質的二叉樹稱為二叉查詢樹
資料結構與演算法14-二叉排序樹
二叉排序樹 又稱為二叉查詢樹。它或者是一棵空樹,或者是具有下列性質的二叉樹, 1. 若它的左子樹不空,則左子樹上的所有結點的值均小於它的根結構值 2. 若它的右子樹不空,則右子樹上的所有結點的值均
Javascript之資料結構與演算法的二叉樹和二叉搜尋樹實現
Javascript之資料結構與演算法的二叉樹和二叉搜尋樹實現 簡介 程式碼實現 簡介 二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點,另一個是右側子節點。 二叉搜尋樹( BST)是二叉樹的一種,但是它只允許你在
【資料結構與演算法】二叉樹遞迴與非遞迴遍歷(附完整原始碼)
二叉樹是一種非常重要的資料結構,很多其他資料機構都是基於二叉樹的基礎演變過來的。二叉樹有前、中、後三種遍歷方式,因為樹的本身就是用遞迴定義的,因此採用遞迴的方法實現三種遍歷,不僅程式碼簡潔且容易理解,但其開銷也比較大,而若採用非遞迴方法實現三種遍歷,則要用棧來模擬實現(遞迴也
資料結構和演算法:二叉樹
二叉樹 二叉樹(Binary tree)是樹形結構的一個重要型別。許多實際問題抽象出來的資料結構往往是二叉樹形式,即使是一般的樹也能簡單地轉換為二叉樹,而且二叉樹的儲存結構及其演算法都較為簡單,因此二叉樹顯得特別重要。二叉樹特點是每個節點最多隻能有兩棵子樹,即樹的度最大為2,且有左右之分 。 二叉樹是n個有限
資料結構與演算法:常用排序演算法總結
排序演算法穩定性的簡單形式化定義為:如果Ai = Aj,排序前Ai在Aj之前,排序後Ai還在Aj之前,則稱這種排序演算法是穩定的。通俗地講就是保證排序前後兩個相等的數的相對順序不變。 對於不穩定的排序演算法,只要舉出一個例項,即可說明它的不穩定性;而對於穩定的排序演算法,必須對演算
資料結構開發(23):二叉樹中結點的查詢、插入、刪除與清除操作
0.目錄 1.二叉樹中結點的查詢操作 2.二叉樹中結點的插入操作 3.二叉樹中結點的刪除操作 4.二叉樹中結點的清除操作 5.小結 1.二叉樹中結點的查詢操作 查詢的方式: 基於資料元素值的查詢 BTreeNode<T>* find(const T&
資料結構開發(24):二叉樹中屬性操作、層次遍歷與典型遍歷
0.目錄 1.二叉樹中屬性操作的實現 2.二叉樹結構的層次遍歷 3.二叉樹的典型遍歷方式 4.小結 1.二叉樹中屬性操作的實現 二叉樹的屬性操作: 二叉樹中結點的數目: 定義功能:count(node) 在 node 為根結點的二叉樹中統計結點數目 在