堆——神奇的優先隊列(上) 【經典】


 堆是什么?是一種特殊的完全二叉樹,就像下面這棵樹一樣。

        有沒有發現這棵二叉樹有一個特點,就是所有父結點都比子結點要小(注意:圓圈里面的數是值,圓圈上面的數是這個結點的編號,此規定僅適用於本節)。符合這樣特點的完全二叉樹我們稱為最小堆。反之,如果所有父結點都比子結點要大,這樣的完全二叉樹稱為最大堆。那這一特性究竟有什么用呢?

        假如有14個數分別是99、5、36、7、22、17、46、12、2、19、25、28、1和92。請找出這14個數中最小的數,請問怎么辦呢?最簡單的方法就是將這14個數從頭到尾依次掃一遍,用一個循環就可以解決。這種方法的時間復雜度是O(14)也就是O(N)。

1
2
3
4
for (i=1;i<=14;i++)
{
     if (a[ i]<min)    min=a[ i];
}

        現在我們需要刪除其中最小的數,並增加一個新數23,再次求這14個數中最小的一個數。請問該怎么辦呢?只能重新掃描所有的數,才能找到新的最小的數,這個時間復雜度也是O(N)。假如現在有14次這樣的操作(刪除最小的數后並添加一個新數)。那么整個時間復雜度就是O(142)即O(N2)。那有沒有更好的方法呢?堆這個特殊的結構恰好能夠很好地解決這個問題。

        首先我們先把這個14個數按照最小堆的要求(就是所有父結點都比子結點要小)放入一棵完全二叉樹,就像下面這棵樹一樣。

        很顯然最小的數就在堆頂,假設存儲這個堆的數組叫做h的話,最小數就是h[ 1]。接下來,我們將堆頂的數刪除,並將新增加的數23放到堆頂。顯然加了新數后已經不符合最小堆的特性,我們需要將新增加的數調整到合適的位置。那如何調整呢?

        向下調整!我們需要將這個數與它的兩個兒子2和5比較,並選擇較小一個與它交換,交換之后如下。

        我們發現此時還是不符合最小堆的特性,因此還需要繼續向下調整。於是繼續將23與它的兩個兒子12和7比較,並選擇較小一個交換,交換之后如下。

        到此,還是不符合最小堆的特性,仍需要繼續向下調整直到符合最小堆的特性為止。

        我們發現現在已經符合最小堆的特性了。綜上所述,當新增加一個數被放置到堆頂時,如果此時不符合最小堆的特性,則將需要將這個數向下調整,直到找到合適的位置為止,使其重新符合最小堆的特性。

 

 

        向下調整的代碼如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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; //則否說明當前的父結點已經比兩個子結點都要小了,不需要在進行調整了 
     }
}

 

 

        我們剛才在對23進行調整的時候,竟然只進行了3次比較,就重新恢復了最小堆的特性。現在最小的數依然在堆頂為2。之前那種從頭到尾掃描的方法需要14次比較,現在只需要3次就夠了。現在每次刪除最小的數並新增一個數,並求當前最小數的時間復雜度是O(3),這恰好是O(log214)即O(log2N)簡寫為O(logN)。假如現在有1億個數(即N=1億),進行1億次刪除最小數並新增一個數的操作,使用原來掃描的方法計算機需要運行大約1億的平方次,而現在只需要1億*log1億次,即27億次。假設計算機每秒鍾可以運行10億次,那原來則需要一千萬秒大約115天!而現在只要2.7秒。是不是很神奇,再次感受到算法的偉大了吧。

        說到這里,如果只是想新增一個值,而不是刪除最小值又該如何操作呢?即如何在原有的堆上直接插入一個新元素呢?只需要直接將新元素插入到末尾,再根據情況判斷新元素是否需要上移,直到滿足堆的特性為止。如果堆的大小為N(即有N個元素),那么插入一個新元素所需要的時間也是O(logN)。例如我們現在要新增一個數3。

 

 

        先將3與它的父結點25比較,發現比父結點小,為了維護最小堆的特性,需要與父結點的值進行交換。交換之后發現還是要比它此時的父結點5小,因此需要再次與父結點交換。至此又重新滿足了最小堆的特性。向上調整完畢后如下。

        向上調整的代碼如下。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void  siftup( int  i)  //傳入一個需要向上調整的結點編號i
{
     int  flag=0;  //用來標記是否需要繼續向上調整
     if (i==1)   return //如果是堆頂,就返回,不需要調整了    
     //不在堆頂 並且 當前結點i的值比父結點小的時候繼續向上調整 
     while (i!=1 && flag==0)
     {
         //判斷是否比父結點的小 
         if (h[ i]<h[ i/2])
             swap(i,i/2); //交換他和他爸爸的位置 
         else
             flag=1; //表示已經不需要調整了,當前結點的值比父結點的值要大 
         i=i/2;  //這句話很重要,更新編號i為它父結點的編號,從而便於下一次繼續向上調整 
     }
}

 

 

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM