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

  堆是什麼?是一種特殊的徹底二叉樹,就像下面這棵樹同樣。html

        有沒有發現這棵二叉樹有一個特色,就是全部父結點都比子結點要小(注意:圓圈裏面的數是值,圓圈上面的數是這個結點的編號,此規定僅適用於本節)。 符合這樣特色的徹底二叉樹咱們稱爲最小堆。反之,若是全部父結點都比子結點要大,這樣的徹底二叉樹稱爲最大堆。那這一特性究竟有什麼用呢?
        假若有14個數分別是995367221746122192528192。請找出這14個數中最小的數,請問怎麼辦呢?最簡單的方法就是將這14個數從頭至尾依次掃一遍,用一個循環就能夠解決。這種方法的時間複雜度是O(14)也就是O(N)
 
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放到堆頂。顯然加了新數後已經不符合最小堆的特性,咱們須要將新增長的數調整到合適的位置。那如何調整呢?
        向下調整!咱們須要將這個數與它的兩個兒子25比較,並選擇較小一個與它交換,交換以後以下。
        咱們發現此時仍是不符合最小堆的特性,所以還須要繼續向下調整。因而繼續將23與它的兩個兒子127比較,並選擇較小一個交換,交換以後以下。
        到此,仍是不符合最小堆的特性,仍須要繼續向下調整直到符合最小堆的特性爲止。
        咱們發現如今已經符合最小堆的特性了。綜上所述,當新增長一個數被放置到堆頂時,若是此時不符合最小堆的特性,則將須要將這個數向下調整,直到找到合適的位置爲止,使其從新符合最小堆的特性。
 

 

        向下調整的代碼以下。
複製代碼
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小,所以須要再次與父結點交換。至此又從新知足了最小堆的特性。向上調整完畢後以下。
        向上調整的代碼以下。
 
複製代碼
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爲它父結點的編號,從而便於下一次繼續向上調整  } }
複製代碼

        說了半天,咱們忽略一個很重要的問題!就是如何創建這個堆。咱們週一接着說。
相關文章
相關標籤/搜索