1.5排序——堆排序:二叉堆和排序

我們有意調(diào)整了排序的順序,最后講這個(gè)堆排序。不是因?yàn)樗茈y,而是它涉及到了基本的數(shù)據(jù)結(jié)構(gòu)知識(shí)。

# 以下py版摘自百度百科
def big_endian(arr,start,end):    
    root=start    
    child=root*2+1 #左孩子    
    while child<=end:
    #孩子比最后一個(gè)節(jié)點(diǎn)還大,也就意味著最后一個(gè)葉子節(jié)點(diǎn)了,就得跳出去一次循環(huán),已經(jīng)調(diào)整完畢     
        if child+1<=end and arr[child]<arr[child+1]:
        #為了始終讓其跟子元素的較大值比較,如果右邊大就左換右,左邊大的話就默認(rèn)           
            child+=1            
        if arr[root]<arr[child]:
        #父節(jié)點(diǎn)小于子節(jié)點(diǎn)直接交換位置,同時(shí)坐標(biāo)也得換,這樣下次循環(huán)可以準(zhǔn)確判斷:是否為最底層,
        #是不是調(diào)整完畢                
            arr[root],arr[child]=arr[child],arr[root]                
            root=child                
            child=root*2+1            
        else:               
        break
         
def heap_sort(arr): #無序區(qū)大根堆排序    
    first=len(arr)//2 - 1    
    for start in range(first,-1,-1):
    #從下到上,從左到右對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行調(diào)整,循環(huán)得到非葉子節(jié)點(diǎn)        
        big_endian(arr,start,len(arr)-1) #去調(diào)整所有的節(jié)點(diǎn)    
    for end in range(len(arr)-1,0,-1):        
        arr[0],arr[end]=arr[end],arr[0] #頂部尾部互換位置        
        big_endian(arr,0,end-1) #重新調(diào)整子節(jié)點(diǎn)的順序,從頂開始調(diào)整    
    return arr

堆,又名“優(yōu)先隊(duì)列”,是一個(gè)帶有優(yōu)先級(jí)(就是一定順序)的隊(duì)列,而隊(duì)列則是一種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),它的特點(diǎn)是“先進(jìn)先出”,i.e.,先進(jìn)入隊(duì)列的元素,在做移除操作時(shí)會(huì)被首先移除出隊(duì)列。其實(shí),優(yōu)先隊(duì)列這種隊(duì)列,并沒有說最先進(jìn)入的會(huì)被最先移除,因?yàn)閹в幸欢ǖ捻樞颍紫冗M(jìn)入的元素也會(huì)根據(jù)要求放到合適的位置,而不是最前端,而刪除時(shí),假設(shè)是默認(rèn)刪除,會(huì)刪除最上方的元素。

我們的堆,一般情況下都是二叉堆,就是由完全二叉樹構(gòu)成的對(duì)象。二叉樹是一種樹形結(jié)構(gòu),每個(gè)父節(jié)點(diǎn)只有2個(gè)子節(jié)點(diǎn);而完全二叉樹則是說,在排下一個(gè)子節(jié)點(diǎn)時(shí),必須保證這一層之前所有的位置都有節(jié)點(diǎn)。

這樣的結(jié)構(gòu)就會(huì)有以下幾個(gè)特點(diǎn),

  1. 假設(shè)一個(gè)元素的編號(hào)是 i,一個(gè)元素的2個(gè)子節(jié)點(diǎn)——左孩子和右孩子分別是 2i 和 2i+1。
  2. 其父節(jié)點(diǎn)的編號(hào)是 i/2,是向下取整的。

所以,當(dāng)我們構(gòu)建一個(gè)(最小)二叉堆時(shí),我們首先需要一個(gè)基本數(shù)據(jù)結(jié)構(gòu)——描述堆的struct或者class:

template <typename T>
struct Heap
{
    int Capacity;  //描述堆的能力,i.e.,堆一共能放幾個(gè)數(shù)
    int Size;  //堆目前放了幾個(gè)數(shù)
    T *elements;  //一個(gè)放置元素的數(shù)組
};

而我們?cè)谑褂玫臅r(shí)候,可以這樣定義一下:

typedef struct Heap *PriorityQueue;

就會(huì)有一個(gè)指向此優(yōu)先隊(duì)列的指針。

而初始化這個(gè)優(yōu)先隊(duì)列也很簡單,給指向優(yōu)先隊(duì)列的指針一個(gè)空間,給放置元素的數(shù)組開辟一個(gè)空間,給結(jié)構(gòu)Heap一個(gè)初始值:

template<typename T>
PriorityQueue Initialize( int MaxSize )
{
    PriorityQueue H;
    H = malloc( sizeof( struct Heap ) );  //開辟指針的空間
    if ( H == NULL )
        Error( "out of space" );  //空間開辟失敗
    H->Elements = malloc( ( MaxSize + 1 ) * sizeof( T ) );  //把最前邊的作為哨兵
    if ( H == NULL )
        Error( "out of space" );
    //Heap初始化
    H->Capacity = MaxSize;
    H->Size = 0;
    H->Elements[0] = Min;  //這個(gè)Min是什么大家可以自己定義,比如INT_MIN
    return H;
}

這個(gè)多一位的“哨兵”有2個(gè)好處,一個(gè)是左右孩子可以直接取2i和2i+1(不然的話第0位的2倍沒有意義),第二個(gè)是在執(zhí)行元素的插入操作時(shí)可以作為結(jié)束循環(huán)的判斷。

那咱們就寫一下如何Insert元素吧,因?yàn)槎咽怯许樞虻模圆迦霑r(shí)必須要考慮如何把它放到合適的位置,一般,咱們先在堆的末端添加一個(gè)空位置,然后判斷空位置放此元素是不是合理的,不合理的話就把此位置的父節(jié)點(diǎn)下移,然后嘗試父節(jié)點(diǎn)的位置放置此元素是否合理,直到根節(jié)點(diǎn):

template <typename T>
void insert( T x, PriorityQueue H )
{
    int i;
    if( Queue已經(jīng)滿了 )
    {
        Error;
        return;
    }
    for( i = ++H->Size; H->Elements[i/2] > x; i /= 2)  //i從一個(gè)新位置(size+1)開始
    //這樣在父節(jié)點(diǎn)的移動(dòng)時(shí)就可以避免繁瑣的swap例程,只用依次放入;而循環(huán)條件也很簡單,
    //只要父元素大于它,就把父元素下移,繼續(xù)尋找父元素
        H->Elements[i] = H->Elements[i/2];
    H->Elements[i] = x;
}

我覺得我已經(jīng)在code里把操作敘述的很清楚了,這里就不再多說,不過只說一點(diǎn)(還是要說啊),這個(gè)循環(huán)條件語句可以用簡單的大小判斷,就是因?yàn)槲覀儼?位置設(shè)為了哨兵(一個(gè)很小的值),這樣即使我們需要插入的值是最小值,當(dāng)它插入到根節(jié)點(diǎn)的位置后,就必然大于0位置的值(1/2=0),從而結(jié)束循環(huán)。

有插入就有刪除,我們實(shí)現(xiàn)一種刪除,就是刪除最小的元素:堆頂元素。其他的刪除可以參考它:

template<typename T>
T DeleteTop( PriorityQueue H )
{
    int i, int child;
    T minElement, lastElement;
    if( empty( H ) )
    {
        Error;  //空
        return H->Elements[0];  //返回第0個(gè)元素
    }
    minElement = H->Elements[1];
    lastElement = H->Elements[H->Size--];  //賦給最后一個(gè)元素,同時(shí)size減1,因?yàn)橐獎(jiǎng)h除一個(gè)元素
    for( i = 1; i * 2 <= H->Size; i = child)  
    //條件是(左)兒子在范圍內(nèi),因?yàn)槿绻蠛⒆硬辉谀敲从液⒆颖厝徊辉?    {
        child = i * 2;
        if( child != H->size && H->elements[child + 1] < H->elements[child])
        //這里我們沒有明顯檢測(cè)右孩子是否存在,但是其實(shí)用了child != H->size來控制,這里的!=
        //其實(shí)就是<,只有它不是最后一位,那么就保證了child+1位置的存在
            child++;
        if( lastElements>H->elements[child] )
            H->elements[i] = H->elements[child];
        else
            break;
    }
    H->elements[i] = lastElements;
    return minElements;
}

有了這個(gè)刪除堆頂(最小)元素的例程,我們就可以做刪除任意元素的操作:把想要?jiǎng)h除的元素賦值為最小元素,即給它一個(gè)小于堆頂元素的值,然后Insert到堆頂,最后執(zhí)行DeleteTop就好了。

我們寫了很多關(guān)于二叉堆的操作,其實(shí)和馬上要寫的堆排序代碼并不太一致,至少不需要掌握復(fù)雜的插入刪除,但它是我們理解堆排序的必要知識(shí)。

現(xiàn)在我們來看堆排序就很簡單了,它的主要操作就是:

  1. 拿數(shù)據(jù)建堆。
  2. 按照規(guī)則刪堆(解散堆),因?yàn)橹粍h除堆頂?shù)模首詈蟮玫降氖且粋€(gè)有序序列。

假設(shè)我們還是需求一個(gè)非減序列,那么相應(yīng)的,我們最好建一個(gè)最大堆,因?yàn)樽畲蠖训膖op delete之后可以放到堆尾,這樣的排序不需要額外的空間,可以說是相當(dāng)成功了。

作為一個(gè)可以用的排序,我覺得應(yīng)該從數(shù)組的第0位開始(廢話,就這么一個(gè)數(shù)組做原地排序,你不從第0位開始從哪開始啊?),也因?yàn)槭菑牡?位開始的,左孩子就不能是2i了(上邊講過),得是2i+1。那么,我們寫寫?

#define LeftChild(i) (2*(i)+1)
template<typename T>
void heap( T a[], int i, int n )
{
    int child;
    T tmp;
    for( tmp = a[i]; LeftChild(i) < n; i = child )
    {
        child = LeftChild(i);
        if( child != n - 1 && a[child+1] > a[child] )
            child++;
        if( tmp < a[child] )  //建立最大堆,那么就是parent小于child時(shí),child上移
            a[i] = a[child];
        else
            break;  //找到了合適的位置,停止循環(huán)
    }
    a[i] = tmp;
}

這個(gè)建堆的基礎(chǔ)操作,被我們拿來既用作建堆,也用做刪堆,當(dāng)然在這里更好的稱呼是排序:建堆就不用講了,就是從最后一位元素開始逐漸建立小堆,然后合并成大堆;而排序的過程就是把top元素放入堆尾,對(duì)其他元素做重建堆:

template<typename T>
void HeapSort( T a[], int n )
{
    int i;
    for( i = n/2; i >= 0; i-- )
        heap( a, i, n );  //建堆
    for( i = n - 1; i > 0; i--)  //一個(gè)微小的點(diǎn):i>0,不需要=,因?yàn)樽詈笠豁?xiàng)不用排序
    {
        swap( a[0], a[i] );  //交換第0項(xiàng)和最后一項(xiàng)后,排除最后一項(xiàng)建堆
        heap( a, 0, i );
    }
}

以上2段代碼只有2個(gè)需要注意的地方,一是HeapSort()建堆的過程中,從i=n/2;開始,而不是很多代碼的i=n-1;,因?yàn)樽詈笠粚硬]有什么可建的,完全是浪費(fèi)時(shí)間;二是在HeapSort()執(zhí)行heap()時(shí),最后一個(gè)參數(shù)是數(shù)組的數(shù)量,也就是數(shù)組的最后一項(xiàng)加1,因?yàn)樵?code>heap()中的循環(huán)條件決定了我們是拿n來比較的,這和其他排序的例程中使用n-1稍微有所區(qū)別。

目前,我們已經(jīng)把所有經(jīng)典的排序都過了一遍,順便還講了遞歸式的證明,以及二叉堆,在一般的算法課上,這大概需要不到一個(gè)月的時(shí)間,希望大家喜歡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371

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