排序演算法(七大經典排序演算法)

龍躍十二發表於2018-03-05

排序演算法是一種在日常生活中應用很廣泛的演算法,所以我們應該很好的掌握他。然而最熟悉的往往是最容易忽略的。“工欲善其事,必先利其器”,將對下面常見演算法逐個介紹。
本文所有排序分析及程式碼實現都是升序情況 ,降序與此相反。
常見排序演算法:
常見排序演算法
1.插入排序演算法
1)直接插入排序
思路分析:
①在長度為N的陣列,將陣列中第i【1~(N-1)】個元素,插入到陣列【0~i】適當的位置上。
②在排序的過程中當前元素之前的陣列元素已經是有序的了。
③在插入的過程中,有序的陣列元素,需要向右移動為更小的元素騰出空間,直到為當前元素找到合適的位置。
過程動態圖展示:
插入排序
程式碼實現:

void InsertSort(DataType *a,size_t n)
{
    size_t i = 0;
    assert(a);
    for (i=0; i<n-1; ++i)
    {
        int end = i;
        DataType tmp = a[end+1];
        while (end>=0 && a[end] > tmp)
        {
            a[end+1] = a[end];
            end--;
        }

        a[end+1] = tmp;
    }
}

總結:時間複雜度為O(n^2),資料量小時使用。並且大部分已經被排序。
2)希爾排序
希爾排序是對插入排序最壞情況的改進。主要改進思路是減少資料移動次數,增加演算法的效率。
思路分析
先比較距離遠的元素,而不是像簡單交換排序演算法那樣先比較相鄰的元素,這樣可以快速減少大量的無序情況,從而減輕後續的工作。被比較的元素之間的距離逐步減少,直到減少為1,這時的排序變成了相鄰元素的互換。
程式碼實現:

void ShellSort(DataType *a,size_t n)
{
    int gap = n;
    size_t i = 0;
    while (gap>1)
    {
        gap = gap/3 + 1;
        for (i=0; i<n-gap; i++)
        {
            int end = i;
            DataType tmp = a[end+gap];
            while (end >= 0 && a[end] > tmp)
            {
                a[end+gap] = a[end];
                end-= gap;
            }

            a[end+gap] = tmp;
        }
    }
}

總結:希爾排序通過將比較的全部元素分為幾個區域來提升插入排序的效能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後演算法再取越來越小的步長進行排序,演算法的最後一步就是普通的插入排序,但是到了這步,需排序的資料幾乎是已排好的了(此時插入排序較快)。
步長的選擇是希爾排序的重要部分。只要最終步長為1任何步長序列都可以工作(且步長要小於陣列長度)。演算法最開始以一定的步長進行排序。然後會繼續以一定步長進行排序,最終演算法以步長為1進行排序。當步長為1時,演算法變為插入排序,這就保證了資料一定會被排序。
2.選擇排序演算法
1)選擇排序
思路分析:第一趟從n個元素的資料序列中選出關鍵字最小/大的元素並放在最前/後位置,下一趟從n-1個元素中選出最小/大的元素並放在最前/後位置。以此類推,經過n-1趟完成排序
過程動態圖
選擇排序
程式碼實現(此處程式碼對直接排序進行了有優化,遍歷一次同時選出最大的和最小的,最大的放在最右邊,最小的放在最左邊,排序範圍縮減)

void SelectSort(DataType *a,size_t n)
{
    size_t left = 0;
    size_t right = n-1;
    assert(a);
    while (left<right)
    {
        DataType min = left;
        DataType max = left;
        size_t i = left;
        for (i=left; i<=right;++i)
        {
            if (a[min] > a[i])
                min = i;
            if (a[max] < a[i])
                max = i;
        }
        Swap(&a[left],&a[min]); 
        if (max == left) //最大值在最左邊
            max = min;
        Swap(&a[right],&a[max]);
        left++;
        right--;
    }
}

總結: 直接選擇排序的最好時間複雜度和最差時間複雜度都是O(n²),因為即使陣列一開始就是正序的,也需要將兩重迴圈進行完,平均時間複雜度也是O(n²)。空間複雜度為O(1),因為不佔用多餘的空間。直接選擇排序是一種原地排序(In-place sort)並且穩定(stable sort)的排序演算法,優點是實現簡單,佔用空間小,缺點是效率低,時間複雜度高,對於大規模的資料耗時長。
2)堆排序
思路分析
①將長度為n的待排序的陣列進行堆有序化構造成一個大頂堆
②將根節點與尾節點交換並輸出此時的尾節點
③將剩餘的n -1個節點重新進行堆有序化
④重複步驟2,步驟3直至構造成一個有序序列
(升序構建小堆,降序構建大堆)
過程動態圖
堆排序

程式碼實現:

void AdjustDown(DataType *a,size_t n,size_t root)  //向下調整演算法
{ 
    size_t parent = root;
    size_t child = parent *2+1;
    while (child<n)
    {
        if (a[child]<a[child+1] && child+1 <n)
            child++;
        if(a[parent] < a[child])
            Swap(&a[parent],&a[child]);

        parent = child;
        child = parent*2+1;
    }
}

void HeadSort(DataType *a,size_t n)
{
    size_t i =0;
    size_t end = n-1;
    for (i=(n-2)>>1; i>0; --i)
        AdjustDown(a,n,i);

    while (end)
    {
        Swap(&a[0],&a[end]);
        AdjustDown(a,end,0);
        end--;
    }
}

總結: 堆排序的最好和最差情況時間複雜度都為O(nlogn),平均時間複雜度也為O(nlogn),空間複雜度為O(1),無需使用多餘的空間幫助排序。優點是佔用空間小,時間複雜度低,達到了基於比較的排序的最低時間複雜度,缺點是實現較為複雜,並且當待排序序列發生改動時,哪怕是小改動,都需要調整整個堆來維護堆的性質,維護開銷大。
3.交換排序
1)氣泡排序
思路分析:
①比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
②對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
③針對所有的元素重複以上的步驟,除了最後一個。
④持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
過程動態圖:
氣泡排序

程式碼實現:

void BubbleSort(DataType *a,size_t n)
{
    size_t i,j,flag = 0;
    assert(a);
    for (i=0; i<n-1; i++)
    {
        flag = 0;
        for (j=0; j<n-i-1; j++)
        {
            if (a[j] > a[j+1])
            {
                Swap(&a[j],&a[j+1]);
                flag = -1;
            }
        }
        if (!flag)
            break;
    }
}

總結:氣泡排序演算法最壞情況和平均複雜度是O(n²),空間複雜度為O(1)。
2)快速排序
本質上快速排序把資料劃分成幾份,所以快速排序通過選取一個關鍵資料,再根據它的大小,把原陣列分成兩個子陣列:第一個陣列裡的數都比這個主後設資料小或等於,而另一個陣列裡的數都比這個主後設資料要大或等於。
快排有三種方法①左右指標法②挖坑法③前後指標法

#快排之所有快,是因為利用了二分的思想

void QSort(DataType *a,int left, int right)
{
    /****************************【快排思路】******************************
     *快排之所以快,是利用了二分的思想
     ************************************************************************/
    assert(a);
    if (left<=right)
    {
        int mid = PartSort3(a,left,right);
        QSort(a,left,mid-1);
        QSort(a,mid+1,right);
    }
}

①左右指標法

int PartSort1(DataType *a,int left,int right)
{                     /* 左右指標法*/
    /************************************************************************
     *選取key(key隨意選擇,選左邊或者右邊方便設計演算法),                  
     *大的值放在key的右邊,小的值放在key的左邊。   
     *最後當bigin和end相交時,交換交點值和key。
     *返回交點的下標,等待下一次遞迴使用
    /************************************************************************/
    int key = a[right];
    int begin = left;
    int end = right;
    while (begin<end)
    {
        while(a[begin] <= key && begin < end)
            begin++;
        while(a[end] >= key && begin < end)
            end--;
        Swap(&a[begin],&a[end]);
    }
    Swap(&a[begin],&a[right]);
    return end;
}

②挖坑法

int PartSort2(DataType *a,int left,int right)
{
    /******************************【挖坑法】********************************
     *選取初始化坑,(選取最左邊或者最右邊的值),大的值放在坑的右邊,小的值
     *放在坑的左邊,當左右相遇時,把原始坑的值放在這裡。此時左邊所有值比坑大
     *右邊所有值比坑小,把陣列分成兩半,依次遞迴。
     ************************************************************************/
    int begin = left;
    int end = right;
    DataType key = a[right];
    while (begin < end)
    {
        while(a[begin] <= key && begin  < end)
            begin++;
        a[right] = a[begin];
        while(a[end] >= key && begin < end)
            end--;
        a[begin] = a[end];
    }
    a[end] = key;
    return end;
}

③前後指標法

int PartSort3(DataType *a,int left,int right) 
{
    /****************************【前後指標法】******************************
     *定義兩個指標,如果前面的a[cur]小於key,prev就跟著走,遇到a[cur]大於key時
     *prev停下來,cur繼續走,當a[cur]等於key時,一趟結束,交換key和a[cur],平分
     *陣列,依次遞迴。
     ************************************************************************/
    int prev = left-1;
    int cur = left;
    while (cur < right)
    {
        if (a[cur] < a[right] && a[cur] != a[++prev])
            Swap(&a[cur],&a[prev]);

        cur++;
    }
    Swap(&a[++prev],&a[right]);
    return prev;
}

3)快排優化
①三數取中法
當我們每次選取key時,如果key恰好是最大或者最小值,此時快排效率會很低,為了避免這種情況,我們對快排選取key值進行優化。
優化思路:依舊選取最右邊的值作為key,但是在選取前,我們把陣列中最左邊,中間,最右邊位置的三個數取出來。找到這三個數中排在中間的一個。把該值與最右邊位置的值進行交換。此時key的值不可能是最大值或者最小值。
②隨機值法。
num = rand()%N 把num位置的數與最右邊的值交換,key依舊去最右邊的值。這種方法也可以,但是太隨機了,特殊場景會導致不可控的結果。
③小區間優化
快排是利用遞迴棧幀完成的,如果遞迴深度太深會影響效率。切割區間時,當區間內元素數量比較少時就不用切割區間了,這時候就直接對這個區間採用直接插入法,可以進一步提高演算法效率。
3)歸併排序
思路分析:
 是利用歸併的思想實現的排序方法,該演算法採用經典的分治策略,分治法將問題分成一些小的問題然後遞迴求解,而治的階段則將分的階段得到的各答案”修補”在一起,即分而治之。

題解思路:
這裡寫圖片描述
思路圖解
程式碼實現:

void Merge(DataType* arr,DataType* ret,size_t left,size_t mid,size_t right) //排序併合並
{
    size_t i = left;
    size_t j = mid+1; 
    size_t k = left;
    while (i!= mid+1 && j!= right+1)
    {
        if (arr[i]>arr[j])
            ret[k++] = arr[j++];
        else
            ret[k++] = arr[i++];
    }
    while (i!= mid+1)
        ret[k++] = arr[i++];

    while (j!=right+1)
        ret[k++] = arr[j++];

    for (size_t i = left; i<=right; i++)
        arr[i] = ret[i];
}

void MergeSort(DataType* arr,DataType*ret, size_t left,size_t right)  //歸併排序
{
    if(left<right)
    {
        int mid = (right+left)/2;
        MergeSort(arr,ret,mid+1,right); //分
        MergeSort(arr,ret,left,mid);//分
        Merge(arr,ret,left,mid,right);
    }
}

歸併排序是穩定排序,它也是一種十分高效的排序。
排序演算法效能比較

名稱 時間複雜度 空間複雜度 穩定性
直接插入排序 O(N^2) O(1) 穩定
希爾排序 O(N)~O(N^2)之間 O(1) 不穩定
選擇排序 O(N^2) O(1) 不穩定
堆排序 O(NlogN) O(1) 不穩定
氣泡排序 O(NlogN) O(1) 穩定
快排 O(NlogN) O(N) 不穩定
歸併排序 O(Nlog(N)) O(N) 穩定

相關文章