優先隊列是一種使用比較普遍的數據結構。不一樣於通常的隊列,優先隊列的元素都具備優先級,優先級高的元素會被優先選取。利用這個特色,咱們能夠根據元素值的大小來設置優先級,值最大/最小的擁有最高的優先級。這樣,咱們就能夠快速地獲取隊列中最大/最小的元素。這篇文章我將着重比較三種常見的,構造優先隊列的數據結構 - Binary Heap(二叉堆), Leftist Heap(左傾堆)和Skew Heap(斜堆)。html
這篇文章的完成借鑑了不少網上的資料,其中最主要的是這幾篇:
二叉堆(一)之 圖文解析 和 C語言的實現
Priority Queues (Heaps)
數據結構與算法(五)node
我這裏直接給出維基百科中關於Binary Heap的解釋:算法
二叉堆是一種特殊的堆,二叉堆是徹底二叉樹或者是近似徹底二叉樹。二叉堆知足堆特性:父節點的鍵值老是保持固定的序關係於任何一個子節點的鍵值,且每一個節點的左子樹和右子樹都是一個二叉堆。數組
當父節點的鍵值老是大於或等於任何一個子節點的鍵值時爲最大堆。當父節點的鍵值老是小於或等於任何一個子節點的鍵值時爲最小堆。數據結構
我這裏補充一下徹底二叉樹的概念:徹底二叉樹是指除了樹的最下層外,全部層的節點都達到最大,而且最下層不滿的節點都位於左支。函數
在構造Binary Heap時,咱們通常都使用數組而不是鏈表(網上也不多有用鏈表實現Binary Heap的資料),我這裏也用數組來構造Binary Heap。Binary Heap分爲最大堆和最小堆,在本文我只介紹最小堆,最大堆和最小堆的實現基本同樣。spa
咱們先來看個最小堆的例子:code
咱們能夠看到,全部節點的值都小於等於其子節點的值。這裏須要注意,構造Binary Heap時,咱們能夠用兩種形式的數列1)使用index = 0的元素;2)不使用index = 0的元素htm
上面的例子使用的是第二種形式,下面全部關於最小堆的代碼是基於第一種形式。這兩種形式有一個很小的區別,在1)中:blog
index = x節點的左子節點的index = 2 * x + 1
index = x節點的右子節點的index = 2 * x + 2
index = x節點的父節點的index = floor((x - 1) / 2)
在2)中:
index = x節點的左子節點的index = 2 * x
index = x節點的右子節點的index = 2 * x + 1
index = x節點的父節點的index = floor(x / 2)
Min Heap通常支持插入,刪除,建立和查找函數。咱們這裏詳細講解下插入(建立)和刪除。
插入能夠分爲兩步:
第一步,在數列的末尾添加須要插入的值。
第二步,比較該節點與其父節點的大小,若是比其父節點大,插入結束;若是比其父節點小,交換這兩個節點並重復步驟2直到插入結束或者該節點成爲根節點。
咱們經過下面這個示意圖來看看具體是怎樣將14插入到最小堆的的:
瞭解瞭如何插入後,咱們分析下插入操做的時間複雜度:
在最好的狀況下,插入節點的值大於其父節點,咱們不須要對堆進行調整,插入完成,時間複雜度爲O(1)。
在最壞的狀況下,插入的節點值比根節點還小,那麼咱們須要將該節點一直交換到根節點,所以時間複雜度是O(h),其中h是最小堆的高度。根據徹底二叉樹的性質,有N個節點的徹底二叉樹的高度爲log(N + 1),所以O(h) = O(log(N + 1)) = O(logN)。關於徹底二叉樹高度的證實請參考這篇博文:二叉查找樹(一)之 圖文解析 和 C語言的實現
綜上,最小堆的插入算法平均時間複雜度是O(logN)。
下面是插入操做的代碼:
/**************************************************************************************** * Insert Operation ***************************************************************************************/ void min_heap_up_update(int key) { int p_node_index, new_node_index; /* set inserted node's init index */ new_node_index = heap_size; /* get inserted node's father node's index and key */ p_node_index = (new_node_index - 1 ) / 2; while (new_node_index > 0) { if (min_heap[p_node_index]<= key) { break; } else { /* please note we do not swap key between father node and child * node, we only assign father node's key to its child node's key */ min_heap[new_node_index] = min_heap[p_node_index]; new_node_index = p_node_index; p_node_index = (p_node_index - 1) / 2; } } /* at his point, we assign key to the inserted node */ min_heap[new_node_index] = key; } void min_heap_insert(int key) { if (heap_size == MAX_SIZE) { printf("Min Heap is full...\n"); return; } min_heap[heap_size] = key; min_heap_up_update(key); heap_size++; }
在代碼的實現上,咱們並無不斷的交換符合條件的父節點和子節點,咱們只是在最後肯定了新節點的位置後,咱們纔將這個節點的key設置爲咱們須要的key。在最小堆的代碼中,咱們用 heap_size
這個全局變量表示當前堆的大小,用 min_heap[]
這個全局數組表示最小堆。
這裏的刪除指的是刪除最小值,也就是刪除根節點。刪除的操做和插入的操做相似,只是插入是經過向上更新最小堆,而刪除是經過向下更新最小堆。刪除操做能夠分爲兩步:
第一步,用最小堆的最後一個節點去取代根節點。
第二步,用更新後的第一個節點與其較小的子節點比較,若是該節點比其較小的子節點小,刪除操做結束;不然交換這兩個節點並重復步驟2直到刪除操做結束。
刪除操做的時間複雜度和插入同樣:
在最好的狀況下,刪除的時間複雜度爲O(1) - 好比整個最小堆的節點都有相同的key,咱們只須要比較一次。
在最壞的狀況下,咱們須要將根節點交換到堆的最下一層,所以時間複雜度是O(logN)。
綜上,最小堆的刪除算法平均時間複雜度是O(logN)。
下面是刪除操做的代碼:
/**************************************************************************************** * Delete Operation ***************************************************************************************/ void min_heap_down_update(int position) { int c_node_index, cur_node_index, cur_node_val; cur_node_index = position; cur_node_val = min_heap[cur_node_index]; c_node_index = 2 * cur_node_index + 1; while (c_node_index < heap_size) { /* if node has two children we choose the one with smaller key */ if ((c_node_index < heap_size - 1) && (min_heap[c_node_index] > min_heap[c_node_index + 1])) c_node_index = c_node_index + 1; if (cur_node_val <= min_heap[c_node_index]) { break; } else { min_heap[cur_node_index] = min_heap[c_node_index]; cur_node_index = c_node_index; c_node_index = 2 * c_node_index + 1; } } min_heap[cur_node_index] = cur_node_val; } void min_heap_remove() { if (heap_size == 0) { printf("Min Heap is empty...\n"); return; } min_heap[0] = min_heap[heap_size - 1]; min_heap_down_update(0); heap_size--; }
同插入操做相似,在代碼中咱們並無不斷的交換父子節點的值,只是在刪除結束後,咱們才更新節點的值。
咱們能夠簡單的經過不斷的插入節點來完成最小堆的構造,根據插入操做的複雜度,要構造一個N個節點的最小堆須要的時間複雜度是O(N*log(N))。有沒有更快速的方法來構造最小堆呢?方法是有的,咱們來看看如何使用O(N)的時間來構造一個包含N個節點的最小堆。
插入的方法是自下而上的構造最小堆,咱們這裏的方法是自上而下的構造最小堆。要知足最小堆成立,咱們須要保證全部的節點往下都構成最小堆。所以,咱們能夠將須要添加到最小堆的數按任意順序放入最小堆的數組(此時不是最小堆),而後經過不斷的調整來使其成爲最小堆。這麼作有一個好處,咱們只須要調整前N/2的節點。爲何呢?由於堆中的後N/2的節點是葉節點,它們已是最小堆了,所以咱們只須要調整前N/2的節點便可將該堆調整成最小堆。
咱們來分析下時間複雜度,我這裏直接引用數據結構與算法(五)中的內容:
根據計算,這麼作能夠達到O(N)的時間複雜度。
下面是最小堆建造的代碼:
for (int i = heap_size / 2; i >=0; i--) min_heap_down_update(i);
min_heap_down_update()
是在刪除操做中實現的。