堆——神奇的優先隊列(下)

        接着上一Pa說。就是如何創建這個堆呢。能夠從空的堆開始,而後依次往堆中插入每個元素,直到全部數都被插入(轉移到堆中爲止)。由於插入第i個元素的所用的時間是O(log i),因此插入全部元素的總體時間複雜度是O(NlogN),代碼以下。
複製代碼
n=0; for(i=1;i<=m;i++) { n++; h[ n]=a[ i]; //或者寫成scanf("%d",&h[ n]);  siftup(); }
複製代碼
 
        其實咱們還有更快得方法來創建堆。它是這樣的。
 
        直接把99536722174612219252819214個數放入一個徹底二叉樹中(這裏咱們仍是用一個一維數組來存儲徹底二叉樹)。
        在這個棵徹底二叉樹中,咱們從最後一個結點開始依次判斷以這個結點爲根的子樹是否符合最小堆的特性。若是全部的子樹都符合最小堆的特性,那麼整棵樹就是最小堆了。若是這句話沒有理解不要着急,繼續往下看。
 
        首先咱們從葉結點開始。由於葉結點沒有兒子,因此全部以葉結點爲根結點的子樹(其實這個子樹只有一個結點)都符合最小堆的特性(即父結點的值比子結點的值小)。這些葉結點壓根就沒有子節點,固然符合這個特性。所以全部葉結點都不須要處理,直接跳過。從第n/2個結點(n爲徹底二叉樹的結點總數,這裏即7號結點)開始處理這棵徹底二叉樹。注意徹底二叉樹有一個性質:最後一個非葉結點是第n/2個結點。
 
        以7號結點爲根的子樹不符合最小堆的特性,所以要向下調整。
        同理以6號、5號和4結點爲根的子樹也不符合最小對的特性,都須要往下調整。
        下面是已經對7號、6號、5號和4結點爲根結點的子樹調整完畢以後的狀態。
        固然目前這棵樹仍然不符合最小堆的特性,咱們須要繼續調整以3號結點爲根的子樹,即將3號結點向下調整。

 

        同理繼續調整以2號結點爲根的子樹,最後調整以1號結點爲根的子樹。調整完畢以後,整棵樹就符合最小堆的特性啦。
        小結一下這個建立堆的算法。把n個元素創建一個堆,首先我能夠將這n個結點以自頂向下、從左到右的方式從1n編碼。這樣就能夠把這n個結點轉換成爲一棵徹底二叉樹。緊接着從最後一個非葉結點(結點編號爲n/2)開始到根結點(結點編號爲1),逐個掃描全部的結點,根據須要將當前結點向下調整,直到以當前結點爲根結點的子樹符合堆的特性。雖然講起來起來很複雜,可是實現起來卻很簡單,只有兩行代碼以下:
for(i=n/2;i>=1;i--) siftdown(i);
        用這種方法來創建一個堆的時間複雜度是O(N),若是你感興趣能夠嘗試本身證實一下,嘿嘿。
        堆還有一個做用就是堆排序,與快速排序同樣堆排序的時間複雜度也是O(NlogN)。堆排序的實現很簡單,好比咱們如今要進行從小到大排序,能夠先創建最小堆,而後每次刪除頂部元素並將頂部元素輸出或者放入一個新的數組中,直到堆爲空爲止。最終輸出的或者存放在新數組中數就已是排序好的了。
複製代碼
//刪除最大的元素 int deletemax() { int t; t=h[ 1];//用一個臨時變量記錄堆頂點的值 h[ 1]=h[ n];//將堆得最後一個點賦值到堆頂 n--;//堆的元素減小1 siftdown(1);//向下調整 return t;//返回以前記錄的堆得頂點的最大值 }
複製代碼

 

        建堆以及堆排序的完整代碼以下:
複製代碼
#include <stdio.h>
int h[ 101];//用來存放堆的數組 int n;//用來存儲堆中元素的個數,也就是堆的大小 //交換函數,用來交換堆中的兩個元素的值 void swap(int x,int y) { int t; t=h[ x]; h[ x]=h[ y]; h[ y]=t; } //向下調整函數 void siftdown(int i) //傳入一個須要向下調整的結點編號i,這裏傳入1,即從堆的頂點開始向下調整 { int t,flag=0;//flag用來標記是否須要繼續向下調整 //當i結點有兒子的時候(實際上是至少有左兒子的狀況下)而且有須要繼續調整的時候循環窒執行 while( i*2<=n && flag==0 ) { //首先判斷他和他左兒子的關係,並用t記錄值較小的結點編號 if( h[ i] > h[ i*2] ) t=i*2; else t=i; //若是他有右兒子的狀況下,再對右兒子進行討論 if(i*2+1 <= n) { //若是右兒子的值更小,更新較小的結點編號 if(h[ t] > h[ i*2+1]) t=i*2+1; } //若是發現最小的結點編號不是本身,說明子結點中有比父結點更小的 if(t!=i) { swap(t,i);//交換它們,注意swap函數須要本身來寫 i=t;//更新i爲剛纔與它交換的兒子結點的編號,便於接下來繼續向下調整  } else flag=1;//則否說明當前的父結點已經比兩個子結點都要小了,不須要在進行調整了  } } //創建堆的函數 void creat() { int i; //從最後一個非葉結點到第1個結點依次進行向上調整 for(i=n/2;i>=1;i--) { siftdown(i); } } //刪除最大的元素 int deletemax() { int t; t=h[ 1];//用一個臨時變量記錄堆頂點的值 h[ 1]=h[ n];//將堆得最後一個點賦值到堆頂 n--;//堆的元素減小1 siftdown(1);//向下調整 return t;//返回以前記錄的堆得頂點的最大值 } int main() { int i,num; //讀入數的個數 scanf("%d",&num); for(i=1;i<=num;i++) scanf("%d",&h[ i]); n=num; //建堆  creat(); //刪除頂部元素,連續刪除n次,其實夜就是從大到小把數輸出來 for(i=1;i<=num;i++) printf("%d ",deletemax()); getchar(); getchar(); return 0; }
複製代碼

 

        能夠輸入如下數據進行驗證
        14
        99 5 36 7 22 17 46 12 2 19 25 28 1 92
        運行結果是
        1 2 5 7 12 17 19 22 25 28 36 46 92 99

 

        固然堆排序還有一種更好的方法。從小到大排序的時候不創建最小堆而創建最大堆。最大堆創建好後,最大的元素在h[ 1]。由於咱們的需求是從小到大排序,但願最大的放在最後。所以咱們將h[ 1]h[ n]交換,此時h[ n]就是數組中的最大的元素。請注意,交換後還需將h[ 1]向下調整以保持堆的特性。OK如今最大的元素已經歸位,須要將堆的大小減1n--,而後再將h[ 1]h[ n]交換,並將h[ 1]向下調整。如此反覆,直到堆的大小變成1爲止。此時數組h中的數就已是排序好的了。代碼以下:
複製代碼
//堆排序 void heapsort() { while(n>1) { swap(1,n); n--; siftdown(1); } }
複製代碼

 

完整的堆排序的代碼以下,注意使用這種方法來進行從小到大排序須要創建最大堆。
複製代碼
#include <stdio.h>
int h[ 101];//用來存放堆的數組 int n;//用來存儲堆中元素的個數,也就是堆的大小 //交換函數,用來交換堆中的兩個元素的值 void swap(int x,int y) { int t; t=h[ x]; h[ x]=h[ y]; h[ y]=t; } //向下調整函數 void siftdown(int i) //傳入一個須要向下調整的結點編號i,這裏傳入1,即從堆的頂點開始向下調整 { int t,flag=0;//flag用來標記是否須要繼續向下調整 //當i結點有兒子的時候(實際上是至少有左兒子的狀況下)而且有須要繼續調整的時候循環窒執行 while( i*2<=n && flag==0 ) { //首先判斷他和他左兒子的關係,並用t記錄值較大的結點編號 if( h[ i] < h[ i*2] ) t=i*2; else t=i; //若是他有右兒子的狀況下,再對右兒子進行討論 if(i*2+1 <= n) { //若是右兒子的值更大,更新較小的結點編號 if(h[ t] < h[ i*2+1]) t=i*2+1; } //若是發現最大的結點編號不是本身,說明子結點中有比父結點更大的 if(t!=i) { swap(t,i);//交換它們,注意swap函數須要本身來寫 i=t;//更新i爲剛纔與它交換的兒子結點的編號,便於接下來繼續向下調整  } else flag=1;//則否說明當前的父結點已經比兩個子結點都要大了,不須要在進行調整了  } } //創建堆的函數 void creat() { int i; //從最後一個非葉結點到第1個結點依次進行向上調整 for(i=n/2;i>=1;i--) { siftdown(i); } } //堆排序 void heapsort() { while(n>1) { swap(1,n); n--; siftdown(1); } } int main() { int i,num; //讀入n個數 scanf("%d",&num); for(i=1;i<=num;i++) scanf("%d",&h[ i]); n=num; //建堆  creat(); //堆排序  heapsort(); //輸出 for(i=1;i<=num;i++) printf("%d ",h[ i]); getchar(); getchar(); return 0; }
複製代碼

 

        能夠輸入如下數據進行驗證
        14
        99 5 36 7 22 17 46 12 2 19 25 28 1 92
        運行結果是
        1 2 5 7 12 17 19 22 25 28 36 46 92 99

 

        OK,最後仍是要總結一下。像這樣支持插入元素和尋找最大(小)值元素的數據結構稱之爲優先隊列。若是使用普通隊列來實現這個兩個功能,那麼尋找最大元素須要枚舉整個隊列,這樣的時間複雜度比較高。若是已排序好的數組,那麼插入一個元素則須要移動不少元素,時間複雜度依舊很高。而堆就是一種優先隊列的實現,能夠很好的解決這兩種操做。
 
        另外Dijkstra算法中每次找離源點最近的一個頂點也能夠用堆來優化,使算法的時間複雜度降到O((M+N)logN)。堆還常常被用來求一個數列中第K大的數。只須要創建一個大小爲K的最小堆,堆頂就是第K大的數。若是求一個數列中第K小的數,只最須要創建一個大小爲K的最大堆,堆頂就是第K小的數,這種方法的時間複雜度是O(NlogK)。固然你也能夠用堆來求前K大的數和前K小的數。你還能想出更快的算法嗎?有興趣的同窗能夠去閱讀《編程之美》第二章第五節。
 
        堆排序算法是由J.W.J. Williams1964年發明,他同時描述瞭如何使用堆來實現一個優先隊列。同年,由Robert WFloyd提出了創建堆的線性時間算法。
相關文章
相關標籤/搜索