1. 程式人生 > >資料結構和演算法分析之排序演算法--選擇排序(堆排序)

資料結構和演算法分析之排序演算法--選擇排序(堆排序)

選擇排序–堆排序

堆排序是一種樹形選擇的排序,是對直接選擇排序的有效改進。
(直接選擇排序:第一次選擇最小值,與第一位數交換,再從後面選擇最小的,和第二位數交換……直至排序結束,共n-1次)

基本思想:
堆的定義如下:具有n個元素的序列(k1,k2,…,kn),當且僅當滿足:
這裡寫圖片描述
時稱之為堆。由堆的定義可以看出,堆頂元素(第一個元素)必須為最小項(或最大項)。
若一一維陣列儲存一個堆,則堆對應一顆完全二叉樹,且所有非葉結點的值均不大於(不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大的)。如下:
(a)大頂堆序列:(96, 83,27,38,11,09)
(b)小頂堆序列:(12,36,24,85,47,30,53,91)
這裡寫圖片描述

初始時把要排序的n個數的序列看作是一顆順序儲存的二叉樹,調整他們的儲存序列,使之成為一個堆,將堆頂元素輸出,得到n個元素中最小(或者最大)的元素,這時堆的根結點的數最小(或者最大)。然後對後面(n-1)個元素重新調整使之成為堆,再次輸出堆頂元素。依次類推,直到只有兩個結點的堆,並對它們做比較,最後輸出n個結點的有序序列。上述過程稱之為堆排序。
因此,實現堆排序需要解決兩個問題:
1、如何將n個待排序的數建成堆;
2、輸出堆頂元素後,怎麼調整剩餘的n-1個元素,使之成為新堆;

下面就針對這兩個問題進行討論:
首先討論第二個問題:輸出堆頂元素後,對剩餘n-1元素重新建成堆的調整過程。


1)設有m個元素的堆,輸出堆頂元素後,剩下m-1個元素。將堆底元素送入堆頂(最後一個元素與堆頂進行交換),此時堆被破壞(根結點不滿足堆的性質)。
2)將根結點與左右子樹中較小的元素進行交換。
3)若與左子樹交換:如果左子樹堆被破壞,即左子樹的堆被破壞,即左子樹的根結點不滿足堆的性質,重複方法(2);
4)若與右子樹交換,如果右子樹堆被破壞,即右子樹的根結單不滿足堆的性質。重複方法(2);
5)繼續對不滿足堆性質的子樹進行上述交換操作,直至堆被建成。
稱這個自根結點到葉子結點的調整過程為篩選,如下圖:
這裡寫圖片描述

再討論對n個元素初始建堆的過程。
建堆方法:堆初始序列建堆的過程,就是一個反覆篩選的過程。
1)n個結點的完全二叉樹,則最後一個結點是第[n/2]個結點的子樹。
2)篩選從第[n/2]個結點為根的子樹開始,該子樹成為堆。
3)之後向前依次對各節點為根的子樹進行篩選,使之成為堆,直到根結點。

如圖建堆初始過程:無序序列:(49,38,65,97,76,13,27,49)
這裡寫圖片描述

演算法的實現:
從演算法描述來看,堆排序需要兩個過程,一是建立堆,而是堆頂與堆的最後一個元素交換位置的篩選過程。所以堆排序兩個函式組成。
1)建堆的滲透函式;
2)反覆呼叫滲透函式實現排序的函式;

程式碼如下:

#include<iostream>
using namespace std;

void print(int a[], int n){  
    for(int j= 0; j<n; j++){  
        cout<<a[j] <<"  ";  
    }  
    cout<<endl;  
}  



/** 
 * 已知H[s…m]除了H[s] 外均滿足堆的定義 
 * 調整H[s],使其成為大頂堆.即將對第s個結點為根的子樹篩選,  
 * 
 * @param H是待調整的堆陣列 
 * @param s是待調整的陣列元素的位置 
 * @param length是陣列的長度 
 * 
 */  
//篩選的過程就是把序列變成堆序列--大頂堆或小頂堆
void HeapAdjust(int H[],int s, int length)  
{  
    int tmp  = H[s];  
    int child = 2*s+1; //左孩子結點的位置。(i+1 為當前調整結點的右孩子結點的位置)  
    while (child < length) {  
        if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大於左孩子(找到比當前待調整結點大的孩子結點)  
            ++child ;  //比較左右子孩子哪個較大,取大的值。
        }  
        if(H[s]<H[child]) {  // 如果較大的子結點大於父結點  --結點取較大的值
            H[s] = H[child]; // 那麼把較大的子結點往上移動,替換它的父結點  
            s = child;       // 重新設定s ,即待調整的下一個結點的位置  
            child = 2*s+1;  
        }  else {            // 如果當前待調整結點大於它的左右孩子,則不需要調整,直接退出  
             break;  
        }  
        H[s] = tmp;         // 當前待調整的結點放到比其大的孩子結點位置上  
    }  
    print(H,length);  
}  


/** 
 * 初始堆進行調整 
 * 將H[0..length-1]建成堆 
 * 調整完之後第一個元素是序列的最小的元素 
 */  
void BuildingHeap(int H[], int length)  
{   
    //最後一個有孩子的節點的位置 i=  (length -1) / 2  
    //從最後一個結點開始篩選,篩選個數是樹的一半
    for (int i = (length -1) / 2 ; i >= 0; --i)  
        HeapAdjust(H,i,length);  
}  
/** 
 * 堆排序演算法 
 互換元素
 */  
void HeapSort(int H[],int length)  
{  
    //初始堆  
    BuildingHeap(H, length);  
    //從最後一個元素開始對序列進行調整  
    for (int i = length - 1; i > 0; --i)  
    {  
        //交換堆頂元素H[0]和堆中最後一個元素  
        int temp = H[i]; H[i] = H[0]; H[0] = temp;  
        //每次交換堆頂元素和堆中最後一個元素之後,都要對堆進行調整  
        HeapAdjust(H,0,i);  
  }  
}   
int main(){  
    int H[10] = {3,1,5,7,2,4,9,6,10,8};  
    cout<<"初始值:";  
    print(H,10);  
    HeapSort(H,10);  
    //selectSort(a, 8);  
    cout<<"結果:";  
    print(H,10);  

    system("pause");
    return 0;

}  

效率分析–堆排序

堆排序的時間複雜度,主要在初始化堆過程和每次選取最大數後重新建堆的過程。
1、初始化建堆過程時間:O(n);
推算過程,參考連結:http://blog.csdn.net/yuzhihui_no1/article/details/44258297
2、更改堆元素後重建堆時間:O(nlogn)
推算過程:
重建堆的過程需要迴圈n-1次,每次都是從根節點往下迴圈查詢,所以每一次時間是logn,總時間為:logn(n-1)=nlogn-logn;

綜上所述,時間複雜度為:O(nlogn)。