到目前為止,我所知道的堆有兩種,一是記憶體的一種,常見的用途就是動態記憶體分配了(在c/c++中就是這樣),另一種是這裡所要論述的一種資料結構。
一、堆
資料結構中的堆又叫二元堆積,顧名思義,我們可以把它看成一顆完全二叉樹,每個元素最多有2個孩子,分別看做左孩子和右孩子。根據元素和它的孩子的關係,堆又可以分為最大堆和最小堆。最大堆的性質是父母的值不小於左右(如果都有)的值,相應地,最小堆就是父母的值不大於孩子的值。
和二叉樹的標記法類似,堆既可以順序儲存,也可以鏈式儲存。這裡我使用順序儲存。鏈式儲存要涉及到指標操作,相對來說會稍微麻煩一點。既然用數組儲存,那麼我們就要瞭解順序儲存下二叉樹的父節點和孩子節點的下標關係。
Parent(i) = i/2; left(i) = 2i+1; right(i) = 2i+2
注意i是從0開始的,當i是從1開始計數時,關係略有不同。
堆的兩個很常見的應用是堆排序和優先順序隊列,優先順序隊列在一些演算法(如最小產生樹演算法)中會用到。在對堆進行操作的時候,比如插入元素或刪除元素,我們都要對堆進行調整,以確保更改後堆的性質沒有被破壞,即這個資料結構還是一個堆。如何保持堆的性質呢?
以最大堆為例,假設堆儲存在數組array中,待調整的元素的下標為i,且其左右子樹都是最大堆。那麼我們進行如下操作:
1.找出array[i]和array[left(i)]、array[right(i)](如果都存在)中的最大值,將其下標記為largest,如果array[i]為最大值,那麼以它為根的子樹已經是最大堆,程式結束。否則,array[i]的某個子節點為最大值,那麼將array[i]和這個孩子交換,然後進入步驟2
2.下標為largest的子節點在交換後的值為array[i],以它為根的子樹可能違背堆的性質,所以對該子樹執行步驟1.
在下面的函數裡面,我使用Heap_Type表示堆是最大堆還是最小堆,並據此執行不同的操作。
template<typename T> void heapify( T *array, const int length, int i, Heap_Type type ){if( i < length ){int extre_ele_index;extre_ele_index = i;if( i*2+1 < length )//左孩子{if( type == MAX_HEAP ){if( array[extre_ele_index] < array[ i*2+1 ] )extre_ele_index = i*2+1;}else{if( array[extre_ele_index ] > array[ i*2+1 ] )extre_ele_index = i*2+1;}}if( i*2+2 < length )//右孩子{if( type == MAX_HEAP ){if( array[extre_ele_index] < array[ i*2+2 ] )extre_ele_index = i*2+2;}else{if( array[extre_ele_index ] > array[ i*2+2 ] )extre_ele_index = i*2+2;}}if( extre_ele_index != i ){T extre_ele; extre_ele = array[i];array[i] = array[extre_ele_index];array[extre_ele_index] = extre_ele;heapify( array, length, extre_ele_index, type );}}}
有了上面的子程式,我們就可以進行建堆了。
將堆array當成一個完全二叉樹,我們需要從這個完全二叉樹的第一個非葉子節點開始,依次對array中的元素進行調整,直至array的第一個元素,也就是這個完全二叉樹的根。從第一個非葉子節點開始調整的原因在於葉子節點沒有孩子,那麼顯然它也就滿足堆的性質,也就沒有調整的必要了。
template<typename T> void build_heap( T *array, const int length, Heap_Type type ){int i;for( i = length/2; i >= 0; --i ){heapify( array, length, i, type );}//注意第一個非葉子節點的序號}//注意數組中第i(從0開始)個元素的左孩子和右孩子的序號。
這樣我們就建成一個堆了。
二、堆排序
堆排序,顧名思義,就是基於堆的排序。因為堆的性質(父母不大於或不小於孩子的值),所以堆的第一個元素(或者說完全二叉樹的根)總是堆中的最(大、小)值。堆排序就是利用堆的這個特點,過程可以描述如下:
1.將第一個元素與堆的最後一個元素交換。
2.將堆的大小減一
3.被交換到第一個位置的新值可能違反堆的性質,所以調用heapify以保持堆。
4.重複1、2、3,直至堆只剩下1個元素。
因為步驟一將第一個元素放入排序後它應該放入的位置,所以它每執行一次,數組中就有一個元素被排好序,堆的大小自然減一。
//Sort_By表示升序排列還是降序排列.
template<typename T> void heap_sort( T *array, const int length, Sort_By type ){int i;T extre_ele;if( type == ASCEND )build_heap( array, length, MAX_HEAP );elsebuild_heap( array, length, MIN_HEAP );//首先建堆,保證第一個元素為極值。for( i = length-1; i >= 1; --i )//i表示尚未排好序的子數組的最後元素的下標。{extre_ele= array[0];array[0] = array[i];array[i] = extre_ele;if( type == ASCEND )heapify( array, i, 0, MAX_HEAP );elseheapify( array, i, 0, MIN_HEAP );}}
三、優先順序隊列
優先順序隊列分為最大優先順序隊列和最小優先順序隊列,分別基於最大堆和最小堆。作為隊列的一種,優先順序隊列首先要有出隊、入隊、取隊首、判空等基本操作,所不同的是對於改變隊列的操作(出隊、入隊),我們要進行上面所說的調整來保證隊列仍然是一個堆。
入隊操作和heapify操作正好相反,heapify是從上至下調整,入隊操作卻是從下向上進行調整,將新入隊的元素放在合適的位置上。
1.因為入隊前,隊列已經是一個堆了,所以入隊的時候,只需要將它與父母比較,如果不合堆的性質,則將它與父母對換,然後在新的位置上與父母比較,直至它和父母的關係滿足堆的性質。
template <typename T>void Priority_Queue<T>::enqueue( T *ele ){if( heap_size+1 > max_size )//堆已滿,需重新分配{T *old_heap = heap;heap = new T[ max_size*2 ];for( int i = 0; i < heap_size; ++i )heap[i] = old_heap[i];delete old_heap;old_heap = NULL;}heap[heap_size] = *ele;int cur_index = heap_size;int par_index = parent( heap_size ); ++heap_size;if( type == Minimum_Priority_Queue )while( par_index >= 0 && heap[cur_index] < heap[par_index] ){T tmp = heap[cur_index];heap[cur_index] = heap[par_index];heap[par_index] = tmp;cur_index = par_index;par_index = parent( cur_index );}else//最大優先順序隊列.{while( par_index >= 0 && heap[cur_index] > heap[par_index] ){T tmp = heap[cur_index];heap[cur_index] = heap[par_index];heap[par_index] = tmp;cur_index = par_index;par_index = parent( cur_index );}}}
出隊操作和堆排序進行的操作幾乎完全相同。堆排序每次迭代都是將第一個元素與最後一個元素對調,然後將堆大小減一,最後調用heapify保持堆的性質。而出隊操作是將最後一個元素賦值給第一個元素,堆大小減一,調用heapify保持堆的性質。哈哈,看到沒,僅有的一點不同就是一個是對調,一個是賦值。
template <typename T>void Priority_Queue<T>::dequeue( T *ele ){heap[0] = heap[heap_size-1];--heap_size;if( type == Minimum_Priority_Queue )heapify( heap, heap_size, 0, MIN_HEAP );elseheapify( heap, heap_size, 0, MAX_HEAP );}
優先順序隊列主要的操作就是上面兩個了,其他就簡單一些了。
程式檔案在我的資源裡面,地址連結:點擊開啟連結