1. 程式人生 > >【排序】快速排序及其非遞迴實現,歸併排序詳解

【排序】快速排序及其非遞迴實現,歸併排序詳解

快速排序

  快速排序(Quicksort)是對氣泡排序的一種改進。
  我們知道快速排序用的是分治的基本思想:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞迴的解決這些子問題,然後將這些子問題的解組合為原問題的解。
  快速排序的基本思想是:

  通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

快速排序的基本思想用分治法可描述為:

  • 分解:將一個序列以一個基準值分解為兩個部分,一個部分所有值都比基準值小,另一個部分所有值都比基準值大
  • 求解:通過遞迴呼叫快速排序單趟排序演算法對左右子區間進行單趟排序
  • 組合:當求解步驟中的兩個遞迴呼叫結束時,其左右兩個子區間已有序。

  快速排序的過程如下圖:
  這裡寫圖片描述

  • 首先先找到最右邊元素key = a[right],將key作為基準值,單趟排序後所有key左邊的數全部小於key,右邊的數全部大於key。
  • 從begin=left,end=right-1兩個方向開始找,begin索引找的是比key大的數字,因為要找到比key大的數字然後與後面比key小的數字交換。
  • 在begin小於end的前提條件下,只要begin找到比key大的數字,end找到比key小的數字,就交換兩個值,然後繼續尋找。begin++,end–
  • 當begin小於end不成立,即begin==end時,判斷a[begin]與key兩個值,如果a[begin]>key,那麼交換兩個值。

    上面判斷a[begin]>key的目的是:當一個數組只有兩個數字的時候,如果不進行判斷,會多一次交換。

      上面的過程有些抽象,現在放上程式碼:
      


int PartSort(int* a, int left, int right)
{
    int key = a[right];

    int begin = left;
    int end = right-1;

    while (begin < end
) { // 找大 while (begin < end && a[begin] <= key) ++begin;
// 找小 while (begin < end && a[end] >= key) --end; if (begin < end) swap(a[begin], a[end]); } if (a[begin] > a[right]) { swap(a[begin], a[right]); return begin; } else { return right; } }

現在來分析一下只有兩個值的時候:

int a[2]={4,3};

  此時left = 0,right = 1;key = a[right] ;
  begin = 0;end = 0;
  所以一開始begin < end條件就不成立,直接跳出迴圈,判斷a[begin]與key的大小,得a[begin]>key,swap即可。
  排序成功。

那麼現在分析一下有序序列:

int a[2]={3,4};

  此時left = 0,right = 1;key = a[right] ;key==4
  begin = 0;end = 0;
  所以一開始begin < end條件就不成立,直接跳出迴圈,判斷a[begin]與key的大小,得a[begin] < key,不進行swap直接return。
  排序成功。

  上面只是單趟排序的演算法。
整體排序演算法:


void QuickSort(int* a, int left, int right)
{
    assert(a);

    if (left >= right)
        return;

    if(right - left < 50)
    {
        InsertSort(a+left, right-left+1);
        return;
    }

    int div = PartSort(a, left, right);
    QuickSort(a, left, div-1);
    QuickSort(a, div+1, right);
}


void TestQuickSort()
{
    //int a[] = {5,5,4,2,3,6,8,5,1,5};
    //int a[] = {9,0,4,2,3,6,8,7,1,5};
    int a[] = {0,1,2,3,4,5,6,7,8,9};

    QuickSort(a, 0, sizeof(a)/sizeof(a[0])-1);
    PrintArray(a, sizeof(a)/sizeof(a[0]));
}

  上面的快速排序對其進行了優化,在數量小於50的情況下,其實用直接插入排序效果更好一些。

單趟排序方法2——挖坑法

  挖坑法是快速排序單趟排序第二種方法:
  我們設定end下標是一個坑,當begin找到比key值大的數的時候,將該值放入坑內,接著下一個坑就是begin下標所在位置,再從end開始往前找,找比key值小的數字,當找到後,再將值放入begin坑內,此時將坑變為end所在下標,直到begin小於end條件不成立為止。
  這裡寫圖片描述
  上圖中,藍色方框代表坑,綠色圓圈代表改變的資料。
  程式碼為:
  

// 挖坑法
int PartSort(int* a, int left, int right)
{
    int key = a[right];

    int begin = left;
    int end = right;

    while (begin < end)
    {
        while(begin < end && a[begin] <= key)
            ++begin;

        if (begin < end)
            a[end] = a[begin];

        while (begin < end && a[end] >= key)
            --end;

        if (begin < end)
            a[begin] = a[end];
    }

    a[begin] = key;

    return begin;
}

快速排序非遞迴實現

利用棧進行非遞迴存放資料。
程式碼如下:


#include <stack>

void QuickSort_NonR(int* a, int left, int right)
{
    stack<int> s;
    s.push(right);
    s.push(left);

    while (!s.empty())
    {
        int begin = s.top();
        s.pop();
        int end = s.top();
        s.pop();

        int div = PartSort2(a, begin, end);

        // [begin div-1] [div+1, end] 
        if (begin < div-1)
        {
            s.push(div-1);
            s.push(begin);
        }

        if (div+1 < end)
        {
            s.push(end);
            s.push(div+1);
        }
    }
}

歸併排序

  歸併排序是利用歸併技術來進行排序。
  歸併是指將若干個已排序的子檔案合併成一個有序的檔案。
  兩路歸併演算法的基本思路:

歸併過程為:比較a[i]和a[j]的大小,若a[i]≤a[j],則將第一個有序表中的元素a[i]複製到r[k]中,並令i和k分別加上1;否則將第二個有序表中的元素a[j]複製到r[k]中,並令j和k分別加上1,如此迴圈下去,直到其中一個有序表取完,然後再將另一個有序表中剩餘的元素複製到r中從下標k到下標t的單元。歸併排序的演算法我們通常用遞迴實現,先把待排序區間[s,t]以中點二分,接著把左邊子區間排序,再把右邊子區間排序,最後把左區間和右區間用一次歸併操作合併成有序的區間[s,t]。

  歸併排序有兩種實現方法:自底向上和自頂向下

自底向上

基本思想:

  • 第一趟歸併排序時,將待排序的檔案A[1…n]看做n個長度為1的有序子檔案,將這些子檔案兩兩歸併。若n為偶數,則得到n/2個長度為2的有序子檔案,若n為奇數,則最後一個子檔案不參與歸併。
  • 將前面的子檔案兩兩歸併,如此反覆。

自頂向下

  • 分解:將當前區間一分為二,即求分裂點
  • 求解:遞迴的對兩個子區間進行歸併排序
  • 組合:將已排序的兩個子區間歸併為一個有序的區間
  • 遞迴終結條件:子區間長度為1

實現:

程式碼如下:


void _Merge(int* a, int* tmp
            ,int begin1, int end1
            ,int begin2, int end2)
{
    int index = begin1;
    while (begin1 <= end1 && begin2 <= end2)
    {
        if (a[begin1] < a[begin2])
            tmp[index++] = a[begin1++];
        else
            tmp[index++] = a[begin2++];
    }

    while(begin1 <= end1)
        tmp[index++] = a[begin1++];

    while (begin2 <= end2)
        tmp[index++] = a[begin2++];
}

void _MergeSort(int* a, int* tmp, int left, int right)
{
    if (left < right)
    {
        int mid = left+(right-left)/2;

        // [left, mid] [mid, right]
        _MergeSort(a, tmp, left, mid);
        _MergeSort(a, tmp, mid+1, right);

        _Merge(a, tmp, left, mid, mid+1, right);
        memcpy(a+left, tmp+left, sizeof(int)*(right-left+1));
    }
}

void MergeSort(int* a, size_t n)
{
    assert(a);

    int* tmp = new int[n];

    _MergeSort(a, tmp, 0, n-1);

    delete [] tmp;
}

void TestMergeSort()
{
    //int a[] = {5,5,4,2,3,6,8,5,1,5};
    int a[] = {9,0,4,2,3,6,8,7,1,5};
    //int a[] = {0,1,2,3,4,5,6,7,8,9};

    MergeSort(a, sizeof(a)/sizeof(a[0]));
    PrintArray(a, sizeof(a)/sizeof(a[0]));
}