在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ 數(shù)據(jù)分析&挖掘/ 算法 11:堆——神奇的優(yōu)先隊(duì)列(上)
最快最簡(jiǎn)單的排序——桶排序
算法 7:Dijkstra 最短路算法
算法 2:鄰居好說(shuō)話:冒泡排序
算法 6:只有五行的 Floyd 最短路算法
算法 10:二叉樹
算法 11:堆——神奇的優(yōu)先隊(duì)列(上)
算法 9:開啟“樹”之旅
算法 5:解密回文——棧
算法 4:隊(duì)列——解密 QQ 號(hào)
算法 8:巧妙的鄰接表(數(shù)組實(shí)現(xiàn))
排序總結(jié):小哼買書
算法 3:最常用的排序——快速排序

算法 11:堆——神奇的優(yōu)先隊(duì)列(上)

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

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.1.png" alt="picture11.1" />

有沒(méi)有發(fā)現(xiàn)這棵二叉樹有一個(gè)特點(diǎn),就是所有父結(jié)點(diǎn)都比子結(jié)點(diǎn)要?。ㄗ⒁猓簣A圈里面的數(shù)是值,圓圈上面的數(shù)是這個(gè)結(jié)點(diǎn)的編號(hào),此規(guī)定僅適用于本節(jié))。符合這樣特點(diǎn)的完全二叉樹我們稱為最小堆。反之,如果所有父結(jié)點(diǎn)都比子結(jié)點(diǎn)要大,這樣的完全二叉樹稱為最大堆。那這一特性究竟有什么用呢?

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


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

現(xiàn)在我們需要?jiǎng)h除其中最小的數(shù),并增加一個(gè)新數(shù) 23,再次求這 14 個(gè)數(shù)中最小的一個(gè)數(shù)。請(qǐng)問(wèn)該怎么辦呢?只能重新掃描所有的數(shù),才能找到新的最小的數(shù),這個(gè)時(shí)間復(fù)雜度也是 O(N)。假如現(xiàn)在有 14 次這樣的操作(刪除最小的數(shù)后并添加一個(gè)新數(shù))。那么整個(gè)時(shí)間復(fù)雜度就是 O(142)即 O(N2)。那有沒(méi)有更好的方法呢?堆這個(gè)特殊的結(jié)構(gòu)恰好能夠很好地解決這個(gè)問(wèn)題。

首先我們先把這個(gè) 14 個(gè)數(shù)按照最小堆的要求(就是所有父結(jié)點(diǎn)都比子結(jié)點(diǎn)要?。┓湃胍豢猛耆鏄?,就像下面這棵樹一樣。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.2.png" alt="picture11.2" />

很顯然最小的數(shù)就在堆頂,假設(shè)存儲(chǔ)這個(gè)堆的數(shù)組叫做h的話,最小數(shù)就是 h[ 1]。接下來(lái),我們將堆頂?shù)臄?shù)刪除,并將新增加的數(shù) 23 放到堆頂。顯然加了新數(shù)后已經(jīng)不符合最小堆的特性,我們需要將新增加的數(shù)調(diào)整到合適的位置。那如何調(diào)整呢?

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.3.png" alt="picture11.3" />

向下調(diào)整!我們需要將這個(gè)數(shù)與它的兩個(gè)兒子 2 和 5 比較,并選擇較小一個(gè)與它交換,交換之后如下。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.4.png" alt="picture11.4" />

我們發(fā)現(xiàn)此時(shí)還是不符合最小堆的特性,因此還需要繼續(xù)向下調(diào)整。于是繼續(xù)將 23 與它的兩個(gè)兒子 12 和7比較,并選擇較小一個(gè)交換,交換之后如下。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.5.png" alt="picture11.5" />

到此,還是不符合最小堆的特性,仍需要繼續(xù)向下調(diào)整直到符合最小堆的特性為止。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.6.png" alt="picture11.6" />

我們發(fā)現(xiàn)現(xiàn)在已經(jīng)符合最小堆的特性了。綜上所述,當(dāng)新增加一個(gè)數(shù)被放置到堆頂時(shí),如果此時(shí)不符合最小堆的特性,則將需要將這個(gè)數(shù)向下調(diào)整,直到找到合適的位置為止,使其重新符合最小堆的特性。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.7.png" alt="picture11.7" />

向下調(diào)整的代碼如下。


    void siftdown(int i) //傳入一個(gè)需要向下調(diào)整的結(jié)點(diǎn)編號(hào)i,這里傳入1,即從堆的頂點(diǎn)開始向下調(diào)整 
    {
        int t,flag=0;//flag用來(lái)標(biāo)記是否需要繼續(xù)向下調(diào)整 
        //當(dāng)i結(jié)點(diǎn)有兒子的時(shí)候(其實(shí)是至少有左兒子的情況下)并且有需要繼續(xù)調(diào)整的時(shí)候循環(huán)窒執(zhí)行
        while( i*2<=n && flag==0 )
        {        
            //首先判斷他和他左兒子的關(guān)系,并用t記錄值較小的結(jié)點(diǎn)編號(hào) 
            if( h[ i] > h[ i*2] )
                t=i*2;
            else
                t=i; 
            //如果他有右兒子的情況下,再對(duì)右兒子進(jìn)行討論 
            if(i*2+1 <= n)
            {
                //如果右兒子的值更小,更新較小的結(jié)點(diǎn)編號(hào)  
                if(h[ t] > h[ i*2+1])
                    t=i*2+1;
            }
            //如果發(fā)現(xiàn)最小的結(jié)點(diǎn)編號(hào)不是自己,說(shuō)明子結(jié)點(diǎn)中有比父結(jié)點(diǎn)更小的  
            if(t!=i)
            {
                swap(t,i);//交換它們,注意swap函數(shù)需要自己來(lái)寫
                i=t;//更新i為剛才與它交換的兒子結(jié)點(diǎn)的編號(hào),便于接下來(lái)繼續(xù)向下調(diào)整 
            }
            else
                flag=1;//則否說(shuō)明當(dāng)前的父結(jié)點(diǎn)已經(jīng)比兩個(gè)子結(jié)點(diǎn)都要小了,不需要在進(jìn)行調(diào)整了 
        }
    }

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

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

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.8.png" alt="picture11.8" />

先將 3 與它的父結(jié)點(diǎn) 25 比較,發(fā)現(xiàn)比父結(jié)點(diǎn)小,為了維護(hù)最小堆的特性,需要與父結(jié)點(diǎn)的值進(jìn)行交換。交換之后發(fā)現(xiàn)還是要比它此時(shí)的父結(jié)點(diǎn) 5 小,因此需要再次與父結(jié)點(diǎn)交換。至此又重新滿足了最小堆的特性。向上調(diào)整完畢后如下。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/11.9.png" alt="picture11.9" />

向上調(diào)整的代碼如下。


    void siftup(int i) //傳入一個(gè)需要向上調(diào)整的結(jié)點(diǎn)編號(hào)i
    {
        int flag=0; //用來(lái)標(biāo)記是否需要繼續(xù)向上調(diào)整
        if(i==1)  return; //如果是堆頂,就返回,不需要調(diào)整了    
        //不在堆頂 并且 當(dāng)前結(jié)點(diǎn)i的值比父結(jié)點(diǎn)小的時(shí)候繼續(xù)向上調(diào)整 
        while(i!=1 && flag==0)
        {
            //判斷是否比父結(jié)點(diǎn)的小 
            if(h[ i]<h[ i/2])
                swap(i,i/2);//交換他和他爸爸的位置 
            else
                flag=1;//表示已經(jīng)不需要調(diào)整了,當(dāng)前結(jié)點(diǎn)的值比父結(jié)點(diǎn)的值要大 
            i=i/2; //這句話很重要,更新編號(hào)i為它父結(jié)點(diǎn)的編號(hào),從而便于下一次繼續(xù)向上調(diào)整 
        }
    }