排序(二)堆排序

上次寫到了快排实辑,接著往下講捺氢。O(nlogn)級別的算法除了上次說的快排,歸并剪撬,還有推排序摄乒。今天從先推排序開始寫。堆排序是堆的一個主要應(yīng)用残黑,要說清楚推排序就要先介紹堆馍佑,而且后面打算寫一下計算時間復雜度(我自己算了一遍感覺挺有意思),所以堆排序可能會寫的長一點梨水。
說明:
1.如果你不清楚樹拭荤,建議先了解一下樹再往下看
2.本文是個人學習筆記,參考了算法導論和網(wǎng)上其他文章

一疫诽、二叉堆

1.定義

二叉堆是棵完全二叉樹(后面會用到一些完全二叉樹的性質(zhì))舅世,并且父結(jié)點的鍵值總是大于等于(叫最大堆)或小于等于(最小堆)子節(jié)點的鍵值旦委。并且每個結(jié)點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。
因為二叉堆最常用雏亚,一般簡稱的堆就是指二叉堆缨硝。

最小堆
2.表示

一般是用數(shù)組表示,因為堆是完全二叉樹罢低,用數(shù)組儲存不會浪費空間查辩。上面這個堆可以表示為:


數(shù)組表示堆

左右孩子下標分別為2 * i + 1和2 * i + 2。
用Java寫一個堆應(yīng)該先聲明堆類网持,定義各種成員變量和方法宜岛,寫構(gòu)造函數(shù)等等,由于我們這里主要為了寫堆排序算法功舀,所以下面就只介紹三個重要操作插入萍倡、刪除、建堆辟汰,然后寫三個函數(shù)遣铝。圖解如下。

Paste_Image.png
3.插入節(jié)點

假設(shè)現(xiàn)在已經(jīng)有了一個堆莉擒,插入一個新節(jié)點,步驟如下:

  1. 堆的大小+1
  2. 新節(jié)點放在堆的尾部(數(shù)組最后一個位置)
  3. 從下至上(根)調(diào)整新節(jié)點位置瘫絮,令其再次滿足堆的定義(這個步驟叫堆化)

可以發(fā)現(xiàn)從這個新結(jié)點到它的父節(jié)點一直向上涨冀,到根結(jié)點必然為一個有序的數(shù)列,現(xiàn)在的任務(wù)是將這個新數(shù)據(jù)插入到這個有序數(shù)列的合適中麦萤。是不是和之前寫的插入排序很像鹿鳖?其實代碼寫法也差不多,只是現(xiàn)在不需要依次往前遍歷數(shù)組壮莹,只要一層一層往上遍歷翅帜,所以一次插入的時間復雜度也從O(n)變得更快了。代碼如下:

void insert(int[] A, int count, int data){
        int i,temp;
        i=count++;
        A[i]=data; 
        while(i!=0&&(i-1)/2>=0&&A[(i-1)/2]<data){
            temp=A[i];
            A[i]=A[(i-1)/2];
            A[(i-1)/2]=temp;
            i=(i-1)/2 
        }
    }

時間復雜度為O(logn)命满,因為在最壞情況下涝滴,需要從最后一層出發(fā),一直向上到根節(jié)點胶台,遍歷的次數(shù)就是樹的高度歼疮。那么為什么樹的高度h=logn?
這是因為堆是個完全二叉樹诈唬。除了最底層韩脏,其他層都是滿的,故它的節(jié)點個數(shù)n為2h~2(h+1)-1铸磅,h表示樹高赡矢。這就表明h<=logn<=h+1杭朱,由于h為整數(shù),所以h=logn吹散。

4.刪除節(jié)點

堆只能刪除根節(jié)點(最大或最小節(jié)點)弧械。操作如下:

  1. 根節(jié)點與最后一個節(jié)點交換并刪除它(就是直接把最后一個節(jié)點賦值給根節(jié)點)
  2. 從根節(jié)點開始向下堆化,令其再次滿足堆的定義送浊。

向下堆化和上面的向上堆化差不多梦谜,但是是和子節(jié)點中最大(最小)的比較袭景,交換唁桩。調(diào)整時先在左右兒子結(jié)點中找最大的,如果父結(jié)點比這個最大的子結(jié)點還大說明不需要調(diào)整了耸棒,反之將父結(jié)點和它交換后再考慮后面的結(jié)點荒澡。這個過程也叫"向下滲透"。

    int delete(int[] A, int count){
        if(count==-1)
            return -1;//當堆為空時与殃,報錯
        int data = A[0];
        A[0]=A[count--];
        down(int[] A, int count);
        return data;//返回刪除的值
    }

  //向下堆化
    void down(int[] A,int count){
        int i =0;//從根節(jié)點開始
        int r,l;
        int maxson;
        for(l=2*i+1,r=2*i+2;;){
            if(r<=count){
                if(A[l]>A[r])
                    maxson=l;
                else
                    maxson=r;
            }
            else if(l<=count&&r>count)
                maxson=l;
            else if(l>count)
                break;
            if(A[maxson]>A[i]){
                int temp=A[i];
                A[i]=A[maxson];
                A[maxson]=temp;
                i=maxson;
            }
            else
                break;
        }
    }

復雜度同插入单山,原因也一樣

5.建堆

建堆的一個簡單思路是,將輸入數(shù)據(jù)依次插入空堆中幅疼,這需要n次連續(xù)插入操作米奸,最壞情況時間復雜度O(nlogn)。

不過建堆顯然不用這么麻煩爽篷,看下面這種思路悴晰。葉子節(jié)點沒有子節(jié)點,可以視作已經(jīng)是滿足條件的堆逐工。從最后一非葉子節(jié)點開始向下堆化铡溪,直到根節(jié)點。觀察發(fā)現(xiàn)最后一個非葉子節(jié)點就是最后一個節(jié)點的父節(jié)點泪喊。比如下面這個堆棕硫,按紅色序號的順序分別向下堆化。

void build(int[] A,int count){
        for(int i=(count-1)/2;i>0;i--){
            down(A,count,i);
        }

顯然袒啼,這種方法效率更高哈扮。因為只對幾乎一半的節(jié)點進行了堆化。而且下層節(jié)點數(shù)量遠大于上層節(jié)點(下層節(jié)點數(shù)量是倍增的)瘤泪,向下堆化灶泵,下層節(jié)點交換次數(shù)少,向上堆化对途,下層節(jié)點交換次數(shù)多赦邻。計算后這種方法可以在時間O(n)內(nèi)完成。具體計算方法后面再講实檀。

二惶洲、堆排序

堆排序思想是數(shù)組創(chuàng)建成堆按声,依次刪除根結(jié)點(最大元素)并放入序列直到堆空。需要注意的是堆排序可以是原地的恬吕。具體做法看代碼吧签则,主要就是刪除根時的處理,和向下調(diào)整不要寫成遞歸的铐料。
注意:上面的代碼直接手寫沒測試渐裂,可能有錯。钠惩。柒凉。理解意思就行,下面這個代碼跑過沒問題篓跛。

package fuckingtest;

import java.util.*;

public class HeapSort {
    public int[] heapSort(int[] A, int n) {
        int count=n-1;
        build(A,count);
        for(;count>0;){
            int temp = A[0];
            A[0]=A[count--];
            down(A, count,0);
            A[count+1]=temp;
        }
        return A;
      
            
    }
    
    void build(int[] A,int count){
        for(int i=(count-1)/2;i>=0;i--){
            down(A,count,i);
            System.out.println(i);
        }
      
    }
    
    void down(int[] A,int count,int i){
        int r,l;
        int maxson=-1;
        for(;;){
            l=2*i+1;
            r=2*i+2;
            if(r<=count){
                if(A[l]>A[r])
                    maxson=l;
                else
                    maxson=r;
            }
            else if(l<=count&&r>count)
                maxson=l;
            else if(l>count)
                break;
            if(A[maxson]>A[i]){
                int temp=A[i];
                A[i]=A[maxson];
                A[maxson]=temp;
                i=maxson;
            }
            else
                break;
        }
    }
    
    public static void main(String[] args) {
        int[] r=new int[6];
        int[] a=new int[]{1,5,3,4,2,6};
        HeapSort h = new HeapSort();
        r = h.heapSort(a,6);
        for(int t:r){
            System.out.println(t);
        }
    }
      
}

性能:
時間復雜度最壞平均都是O(nlogn)膝捞。原因是建堆是線性時間,然后需要連續(xù)刪除n次愧沟。額外空間復雜度O(1)(原地的)蔬咬。是不穩(wěn)定算法。雖然都是O(nlogn)級別算法沐寺,但由于快排常量系數(shù)更小林艘,最好情況更快,所以快排效率更高混坞。但堆排序的優(yōu)勢是它可以是原地的北启。

三、復雜度分析

單次插入拔第,單次刪除操作最壞情況時間復雜度O(logn)這個很好理解,那為什么建堆操作的時間復雜度是O(n)场钉?我們接下來就具體計算一下蚊俺。

為方便計算,考慮堆是滿二叉樹的情況(因為計算的是最壞情況逛万,考慮滿二叉樹也沒問題)泳猬。順著剛才講過的每層節(jié)點的交換次數(shù)想,我們計算所有節(jié)點在建堆過程中交換的次數(shù)和宇植。

假設(shè)元素個數(shù)為n得封,樹高h為㏒n。那么第x層的一個節(jié)點指郁,其深度為x忙上,高度為h-x,第x層總共有2^x個節(jié)點闲坎。

最下層非葉節(jié)點的元素疫粥,只需做一次線性運算便可以確定大根茬斧,而這一層具有2(h-1)個元素,我們假定O(1)=1梗逮,那么這一層元素所需時間為2(h-1) × 1项秉。倒數(shù)第二層最多只需要下調(diào)2次,頂點最多需要下調(diào)h次慷彤,而最后一層父節(jié)點共有2(h-1)個,倒數(shù)第二層公有2(h-2),頂點只有1(2^0)個娄蔼,

因此,可以總結(jié)出通項公式,第x層元素的計算量為2^(x) × (h-x)勇边。
又以上通項公式可得知谋币,構(gòu)造樹高為h的二叉堆的精確時間復雜度為:
S = 2^(h-1) × 1 + 2^(h-2) × 2 + …… +1 × (h-1) ①

觀察發(fā)現(xiàn),這不就是高中時候?qū)W的數(shù)列問題嗎唉侄!該求和公式為等差數(shù)列和等比數(shù)列的乘積,因此用錯位相減發(fā)求解野建,給公式左右兩側(cè)同時乘以2属划,可知:
2S = 2^h × 1 + 2^(h-1) × 2+ …… +2 × (h-1) ②

用②減去①可知: S=2h+2(h-1)+......+2^1+1-h ③

③式是個等比數(shù)列,根據(jù)等比數(shù)列公式(別告訴我連這個都忘了候生。同眯。。)求得
S =2^h - h -1 ③

將h = ㏒n 帶入③唯鸭,S = n - ㏒n -1= O(n)

證明成功须蜗!

用這個方法,同樣可以去計算n次連續(xù)插入或n次連續(xù)刪除的時間復雜度目溉。因為n次插入和n次刪除道理是一樣的明肮,交換次數(shù)一樣,可以看成互為逆過程缭付,計算一個就行柿估,我們這里計算n次插入,更好理解些陷猫。

最下層有2^h個節(jié)點秫舌,交換h次,總結(jié)出求和公式:
S=21*1+222+......+2^hh
同樣用錯位相減法绣檬,求得S=2^(h+1)*(h-1)+2=2n(logn-1)+2=O(nlogn)
這樣建堆的時間復雜度和n次刪除的時間復雜度就都清楚了足陨。

再繼續(xù)分析建堆過程,其實如果從上往下看娇未,很像遞歸墨缘,而對這種問題,我們可以用分治法主定理來計算。
建堆算法是從最后一個非葉子結(jié)點開始下溯(向下堆化)飒房,也可以把建堆過程想成先對左子樹建堆(T(n/2))搁凸,再對右子樹建堆(T(n/2)),最后對根下溯(O(lg n))狠毯。
所以遞推式是T(n) = 2*T(n/2) + O(lg n)
根據(jù)主定理可知护糖,它的解是 T(n) = O(n)。

總結(jié)

顯然嚼松,把這個問題看作遞歸形式嫡良,套主定理公式簡單了許多。我們可以自然地想到献酗,主定理公式本質(zhì)上是不是其實就是把上面的數(shù)學推導總結(jié)成了公式寝受,也就是說我們是不是也可以用上面的方法去證明主定理?

之前遇到復雜度分析罕偎,我總是簡單記一下書上的結(jié)論很澄,只是知其然不知其所以然。主定理也只是簡單了解下颜及,沒有深入了解它的原理甩苛。其實許多問題,比如快排樞軸點為什么選擇中間的最好這些都不是很理解俏站。感覺以后不能偷懶讯蒲,還是得真正把問題弄明白。

另外感覺網(wǎng)上大部分中文資料對復雜度的數(shù)學推導不夠肄扎。比如之前搜跳表(skipList)的時候墨林,大部分博客只講了跳表的結(jié)構(gòu),一些基本操作和特性犯祠。對于隨機的跳表節(jié)點高度為什么能證明出O(1)復雜度旭等,幾乎沒有給出數(shù)學推導的。所以最好還是手邊備一本經(jīng)典算法書衡载,并且最好習慣查英文資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辆雾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子月劈,更是在濱河造成了極大的恐慌,老刑警劉巖藤乙,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猜揪,死亡現(xiàn)場離奇詭異,居然都是意外死亡坛梁,警方通過查閱死者的電腦和手機而姐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來划咐,“玉大人拴念,你說我怎么就攤上這事钧萍。” “怎么了政鼠?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵风瘦,是天一觀的道長。 經(jīng)常有香客問我公般,道長万搔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任官帘,我火速辦了婚禮瞬雹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刽虹。我一直安慰自己酗捌,他們只是感情好,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布涌哲。 她就那樣靜靜地躺著胖缤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪膛虫。 梳的紋絲不亂的頭發(fā)上草姻,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音稍刀,去河邊找鬼撩独。 笑死,一個胖子當著我的面吹牛账月,可吹牛的內(nèi)容都是我干的综膀。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼局齿,長吁一口氣:“原來是場噩夢啊……” “哼剧劝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抓歼,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤讥此,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谣妻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萄喳,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年蹋半,在試婚紗的時候發(fā)現(xiàn)自己被綠了他巨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖染突,靈堂內(nèi)的尸體忽然破棺而出捻爷,到底是詐尸還是另有隱情,我是刑警寧澤份企,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布也榄,位于F島的核電站,受9級特大地震影響薪棒,放射性物質(zhì)發(fā)生泄漏手蝎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一俐芯、第九天 我趴在偏房一處隱蔽的房頂上張望棵介。 院中可真熱鬧,春花似錦吧史、人聲如沸邮辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吨述。三九已至,卻和暖如春钞脂,著一層夾襖步出監(jiān)牢的瞬間揣云,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工冰啃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留邓夕,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓阎毅,卻偏偏與公主長得像焚刚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子扇调,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 概述 排序有內(nèi)部排序和外部排序矿咕,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大狼钮,一次不能容納全部...
    蟻前閱讀 5,183評論 0 52
  • 概述:排序有內(nèi)部排序和外部排序碳柱,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進行排序,而外部排序是因排序的數(shù)據(jù)很大熬芜,一次不能容納全部...
    每天刷兩次牙閱讀 3,730評論 0 15
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,253評論 0 2
  • 因為之前就復習完數(shù)據(jù)結(jié)構(gòu)了莲镣,所以為了保持記憶,整理了一份復習綱要猛蔽,復習的時候可以看著綱要想具體內(nèi)容。 樹 樹的基本...
    牛富貴兒閱讀 6,870評論 3 10
  • 兩天復習一門我要開始了,雖然今天起的有點點晚曼库。 新的一年区岗,希望能和一些人走的更近,這些是一看就是心靈相通的人哇毁枯。
    cdccff66b278閱讀 171評論 0 0