1. 程式人生 > >其他排序演算法:快速、歸併、堆排序(top N)

其他排序演算法:快速、歸併、堆排序(top N)

快速排序

快速排序是一種分治排序演算法,採用了遞迴的方法。

原理:
1.先從數列中取出一個數作為基準數。
2.分割槽過程:將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊。
3.對左右區間重複第二步,直到各區間只有一個數。

程式碼實現為:

int partition(Item a[],int l,int r) {
    ...
}

void quick(Item a[],int l,int r) {
    if (l>=r) return;//如果子區間為01個元素,就可以結束遞迴呼叫

    int i = partition(a,l,r);
    quick(a,l,i-1
); quick(a,i+1,r); }

partition函式的實現方式

partition函式是快排的核心部分,它的目的就是將陣列劃分為小於等於pivot和大於pivot兩部分,並返回pivot的正確位置。其實現方法大體有兩種,單向掃描版本和雙向掃描版本。

單向掃描

int partition(int a[], int l, int r) {
    int x = a[l];//選擇a[l]作為選定元素
    int i = l;
    int j = l+1;
    for (; j <= r; j++)
        if (a[j] < x) //如果a[j]小於x,就將它交換到前面去
swap(&a[++i], &a[j]); swap(&a[l], &a[i]);//最終將a[l]交換到正確的位置上 return i; }

雙向掃描

int partition(int a[], int p, int r) {
    int x = a[p]; //選取a[p]作為選定元素
    int i = p + 1;
    int j = r;
    while (true) {
        while (i <= j && a[j] >= x) j--;//從右往左,找到比a[p]小的數
if (i > j) break; while (i <= j && a[i] < x) i++;//從左往右,找到比a[p]大的數 if (i > j) break; swap(&a[i++], &a[j--]); } swap(&a[j], &a[p]); return j; }

比較起來的話,還是單向掃描更簡單和直觀一些。

單鏈表的快排實現

由於單鏈表只能順序遍歷,所以適合使用單向掃描。

struct Node   
{  
    int key;  
    Node* next;  
    Node(int nKey, Node* pNext) : key(nKey),next(pNext) {}  
};  


Node* GetPartion(Node* pBegin, Node* pEnd)  
{  
    int key = pBegin->key;  
    Node* i = pBegin;  
    Node* j = p->next;  

    while(j != pEnd)  
    {  
        if(j->key < key)  
        {  
            i = i->next;  
            swap(i,j);//把i和j的值交換一下
        }  

        j = j->next;  
    }  
    swap(i,pBegin);//把起始節點的值交換到正確的位置上
    return i;  
}  

void QuickSort(Node* pBeign, Node* pEnd)  
{  
    if(pBeign == pEnd) return; 

    Node* partion = GetPartion(pBeign,pEnd);  
    QuickSort(pBeign,partion);  
    QuickSort(partion->next,pEnd);   
}  

快速排序的效能特徵

平均情況下,快速排序使用大約2NlgN次比較。
最壞情況下,快速排序使用大約N^2/2次比較。

歸併排序

原理:基本思路是不斷將兩個有序的序列合併為一個大的有序序列。具體操作時,首先將序列分為兩部分,然後對每一部分進行迴圈遞迴,再逐個將結果進行歸併。歸併排序是一種 穩定排序

歸併排序依賴於歸併操作,歸併操作的具體過程如下:

  1. 申請空間,使其大小為兩個已經排序序列之和,然後將待排序陣列複製到該陣列中
  2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
  3. 比較複製陣列中兩個指標所指向的元素,選擇相對小的元素放入到原始待排序陣列中,並移動指標到下一位置
  4. 重複步驟3直到某一指標達到序列尾
  5. 將另一序列剩下的所有元素直接複製到原始陣列末尾

有了歸併操作,就可以自頂而下地實現歸併排序:將給定序列平分成兩個子序列。若兩個子序列長度大於1,則再遞迴劃分下去,直到兩個子序列都只有1個元素(或者一個有1個元素,另一個沒有元素),這時候他們顯然是各自有序的。然後進行歸併操作,並逐層向上返回。這樣層層歸併,等到第一層遞迴呼叫返回時,再進行最後一次歸併,排序就完成了。

與快速排序的聯絡

快速排序包括一個選擇操作過程(確定a[r]的正確位置),後跟兩個遞迴呼叫。
歸併排序的執行過程和快速排序相反,兩個遞迴呼叫之後是一個歸併過程。

程式碼實現:

Item b[maxN]; //輔助陣列

void partition(Item a[],int l,int m,int r) {
    int i,j,k;
    //將排好序了的兩個子陣列:a[1~m]和a[m+1~r]放入輔助陣列b中
    for (i=l;i<=r;i++) b[i]=a[i];

    //i和j分別指向兩個子陣列的開頭
    i=l;
    j=m+1;
    k=l;
    while(i<=m && j<=r) {
        if (b[i]<b[j])
            a[k++]=b[i++];
        else 
            a[k++]=b[j++];  
    }
    //如果後半個數組已經全部處理完了
    while(i<=m) 
        a[k++]=b[i++];
    //如果前半個陣列已經全部處理完了
    while(j<=r)
        a[k++]=b[j++];
}

void merge(Item a[],int l,int r)  {
        //當子序列只有1個元素或為空時,返回
        if (l>=r) return;

        int m = (l+r)/2;
        merge(a,l,m);
        merge(a,m+1,r);
        partition(a,l,m,r);
    }

對6 5 3 1 8 7 24 進行歸併排序的動畫效果如下:

歸併排序舉例

歸併操作的視覺效果如下所示:

歸併排序原理

歸併排序的效能特徵

歸併排序使用大約N lgN 次比較。
歸併排序使用與N成正比的額外記憶體空間。

堆排序

定義:
優先佇列:是一種資料結構,其資料項中帶有關鍵字,它支援兩種基本操作:向優先佇列中插入一個新的資料項,刪除優先佇列中關鍵字最大的資料項。
堆有序:如果一棵樹中每個節點的關鍵字都大於或等於所有子節點中的關鍵字(如果子節點存在),就稱樹是堆有序的。
堆:本質上是完全二叉樹,用陣列(vector)表示。

首先定義一個數組,用來表示堆。

int N=0;
vector<int> heap(100005,0);

上浮

當插入一個節點時,為了恢復堆的條件,我們向上移動,需要時交換位置k處的節點與其父節點a[k/2],只要a[k/2]<a[k],就繼續這一過程,或到達堆頂。

void Up(int k) {
    while(k>1 && heap[k/2]<heap[k]) {//是否到根節點了,或者父節點比當前節點小
        swap(heap[k/2],heap[k]);
        k=k/2;
    }
}

下沉

在降低一個節點的優先順序時,為了恢復堆的條件,我們向下移動,需要時交換位置k處的節點與其子節點中較大的那個,如果在k處的節點不小於它的任何一個子節點,或到達樹底,則停止這一過程。

void Down(int k) {
    int j;
    while(2*k<=N) {//如果沒到達樹底
        j=2*k;
        if(j<N && heap[j+1]>heap[j]) j++;//找到子節點中較大的那個
        if(heap[j]<heap[k]) break;//如果父節點比子節點都大,退出
        swap(heap[k],heap[j]);
        k=j;
    }
}

基於堆的優先佇列

利用上浮和下沉的堆化操作,可以實現優先佇列的插入和刪除操作。
插入時,在堆尾加入新元素,然後使用Up來恢復堆的條件。

void insert(int weight) {
    N++;
    heap[N]=weight;
    Up(N);
}

刪除頂上元素時,取出該頂上元素(根節點),然後把當前堆的最後一個元素臨時放在根節點上,然後使用Down來恢復堆的條件。

int deleteMax() {
    swap(heap[N],heap[1]);
    N--;
    Down(1);
    return heap[N+1];
}

堆排序

使用堆對陣列a[]進行排序,首先是依次把元素插入到堆中,然後從大到小,依次遞減取出當前最大元素,從而完成排序。

int pq[MAX];
int N=0;
void PQsort(int a[],int l,int r) {
    int k;
    for (k=l;k<=r;k++) insert(a[k]);
    for (k=r;k>=l;k--) a[k]=deleteMax();
}

初次建堆的時間複雜度為O(n),刪除堆頂元素並維護堆的性質需要O(logn),這樣的操作一共進行n次,故最終時間複雜度為O(nlogn)。我們不需要利用額外空間,故空間複雜度O(1)。

topN問題

給定n個元素,要求其中最大/小的N個元素,即為topN問題。
思路:
最大topN問題可以用大小為N的最小堆實現:首先建立起一個大小為N的最小堆;然後從N+1個元素開始直到最後一個元素,插入一個元素,緊接著彈出堆頂元素(最小的那個),最後剩下的堆中元素即為n個元素中最大的N個。

簡單修改下上一節的程式碼:

int pq[MAX];
int N=0;

void PQsort(int a[],int l,int r) {
    int k;
    for (k=l;k<l+N;k++) //建立大小為N的堆
        insert(a[k]);
    for(k=l+N;k<r;k++) { //每插入一個新元素,刪去頂上元素
        insert(a[k]);
        deleteMax();
    }
    for(int i=1;i<=N;i++) //輸出這個堆,即topN的解
        cout<<pq[i];
}

最小topN問題則類似。