1. 程式人生 > >總結:排序演算法

總結:排序演算法

一、直接插入排序

基本思想:

     把n個待排序的元素看成為一個有序表和一個無序表,開始時有序表中只包含1個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,將它插入到有序表中的適當位置,使之成為新的有序表,重複n-1次可完成排序過程.

圖解:

這裡寫圖片描述

程式碼實現:

void InsertSort(int *a, int size)
{
    assert(a);
    for
(int i = 0; i < size - 1; i++) { int end = i; //有序表最後一個下標 int key = a[end + 1]; //待插入元素 while (end >= 0 && key < a[end]) //查詢合適位置 { a[end + 1] = a[end]; end--; } a[end + 1] = key; //插入 } }
分析:

時間複雜度:

  • 最好情況:
    如果待排序的元素本身有序,那麼在進行插入排序時,每一個元素直接在前面有序表末尾處進行插入,整個過程下來,時間複雜度為O(N)
  • 最壞情況:
    如果待排序的元素無序,那麼在進行插入排序時,每一個元素都需要在前面的有序表中找到其合適的插入位置,整個過程下來,時間複雜度為O(N^2)
  • 平均情況:O(N^2)

空間複雜度:O(1)
穩定性:穩定
說明:設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面,在排序之後,a[i]仍然在a[j]前面,則這個排序演算法是穩定的。

二、希爾排序

基本思想:

(1)預排序:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成),分別進行直接插入排序,然後依次縮減增量再進行排序,使整個序列接近有序
(2)當整個序列中的元素基本有序時,再對全體元素進行一次直接插入排序
     因為直接插入排序在元素基本有序的情況下效率是很高的,因此希爾排序在時間效率上相對於直接插入排序有較大提高。

圖解:

這裡寫圖片描述

程式碼實現:

void ShellSort(int *a, int size)
{
    assert(a);
    int gap = size;
    while (gap > 1)
    {
        gap = gap / 3 + 1; //增量(步長)
        for (int i = 0; i < size - gap; i++)
        {
            int end = i; //有序表最後一個下標
            int tmp = a[end + gap]; //待插入元素
            while (end >= 0 && tmp < a[end]) //查詢合適位置
            {
                a[end + gap] = a[end];
                end -= gap;
            }
            a[end + gap] = tmp; //插入
        }
    }
}

分析:

時間複雜度:

  • 最好情況:O(N)
  • 最壞情況:O(N^2)
  • 平均情況:O(N^1.3)

空間複雜度:O(1)
穩定性:不穩定
     例如:待排序列3 2 2* 4,當gap為2時進行希爾排序,經過排序後變為2* 2 3 4,此時2和2*之間的相對位置發生變化。

三、直接選擇排序

基本思想:

     在元素序列a[i]~a[n-1]中選擇關鍵碼最大(最小)的資料元素,將它與這組元素中的最後一個(第一個)元素進行交換,接著在剩餘的元素序列a[i]~a[n-2](a[i+1]~-a[n-1])中重複上述步驟,直到剩餘1個元素時完成排序。

圖解:

這裡寫圖片描述

程式碼實現:
void SelectSort(int *a, int size)
{
    assert(a);
    int left = 0;
    int right = size - 1;
    while (left < right)
    {
        int min = left;
        int max = left;
        for (int i = left; i <= right; i++)
        {
            if (a[i] < a[min]) //選出最小
            {
                min = i;
            }
            if (a[i] > a[max]) //選出最大
            {
                max = i;
            }
        }
        Swap(&a[left], &a[min]);
        if (max == left) //若max和left相等,則經過上一步交換,導致原max處為最小值,而min處為最大值
        {
            max = min; //更新max,讓其位置為最大值
        }
        Swap(&a[right], &a[max]);
        left++;
        right--;
    }
}
分析:

時間複雜度:O(N^2)
空間複雜度:O(1)
穩定性:不穩定
     例如上述圖解,排序前25在25*之前,而在排序後25在25*後。

四、堆排序

基本思想:

升序建大堆,降序建小堆
     以升序為例,先將整個序列的元素建造成一個大堆,接著把堆頂元素和當前堆的最後一個元素進行交換,然後堆元素個數減1,接著從根節點通過向下調整使得當前堆恢復到大堆,重複上述過程,直到當前堆的元素個數為1時完成排序。

圖解:

這裡寫圖片描述
這裡寫圖片描述

程式碼實現:
//自頂向下調整
void AdjustDown(int *a, int size, int root)
{
    int parent = root;
    int child = parent * 2 + 1;
    while (child < size)
    {
        int flag = 0;
        if (child + 1 < size)
        {
            if (a[child + 1] > a[child])
            {
                child++;
            }
        }
        if (a[child]>a[parent])
        {
            flag = 1;
            Swap(&a[child], &a[parent]);
        }
        if (flag == 0) //優化
        {
            break;
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

void HeapSort(int *a, int size)
{
    assert(a);
    int i = (size - 2) / 2;
    for (; i >= 0; i--) //先建大堆
    {
        AdjustDown(a, size, i);
    }

    for (i = size - 1; i > 0; i--) //排序
    {
        Swap(&a[0], &a[i]);
        AdjustDown(a, i, 0);
    }   
}
分析:

時間複雜度:O(N*lgN)
空間複雜度:O(1)
穩定性:不穩定

五、氣泡排序

基本思想:

     從元素序列第一個位置開始,進行兩兩比較,根據大小交換位置,直到最後將最大(最小)的資料元素交換到了當前序列的最後一個位置,成為有序序列的一部分,然後元素個數減1,重複上述過程,直到所有資料元素都排好序。

圖解:

這裡寫圖片描述

程式碼實現:
void BubbleSort(int *a, int size)
{
    assert(a);
    for (int i = 0; i < size - 1; i++)
    {
        int flag = 0;
        for (int j = 0; j < size - i - 1; j++) //一趟排序
        {
            if (a[j]>a[j + 1])
            {
                flag = 1;
                Swap(&a[j], &a[j + 1]);
            }
        }
        if (flag == 0) //如果一趟排序後發現沒有一次交換,則說明已經有序
        {
            break;
        }
    }
}
分析:

時間複雜度:

  • 最好情況:O(N)
  • 最壞情況:O(N^2)
  • 平均情況:O(N^2)

空間複雜度:O(1)
穩定性:穩定

六、快速排序

基本思想:

     任取待排列元素序列中的一個元素作為基準值,通過一趟排序將要排序的序列分割成獨立的兩子序列,其中左子序列的所有元素都比基準值小,右子序列的所有元素都比基準值大,然後左右子序列重複此過程,直到所有元素都排列在相應的位置上為止。

圖解:

這裡寫圖片描述

將區間按照基準值劃分成左右兩部分的方法有:
(1)左右指標法:
     定義兩個指標begin和end,將基準值放在最右邊,begin從頭開始找比基準值大的值(begin++),end從尾開始找比基準值小的值(end–),若都找到且begin小於end,則兩者值交換,重複上述過程,直到begin>=end時,將begin所對應的值和最右邊的基準值交換,此時整個序列被基準值劃分成左右兩個子序列。
這裡寫圖片描述

int PartSort1(int *a, int left, int right)
{
    int index = GetMid(a, left, right); //三數取中,選取基準值
    Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
    int key = a[right];
    int begin = left;
    int end = right;
    while (begin < end)
    {
        //begin選大
        while (begin < end && a[begin] <= key) //"="可省略
        {
            begin++;
        }
        //end選小
        while (begin < end && a[end] >= key) //"="不可省略
        {
            end--;
        }
        if (begin < end)
        {
            Swap(&a[begin], &a[end]);
        }
    }
    Swap(&a[begin], &a[right]);
    return begin;
}

(2)挖坑法:
     定義兩個指標begin和end,將基準值放在最右邊並儲存該值,此時該位置可視為一個坑。begin從頭開始找比基準值大的值,找到後將begin所對應的值填入到剛才的坑中,此時begin這個位置成為新的坑;begin不動,接著end從尾開始找比基準值小的值,找到後將end所對應的值填入到剛才的新坑中,此時end這個位置成為新的坑;end不動,begin從上次的位置接著往後找,重複上述過程,直到begin>=end時,將儲存的基準值填入到begin所對應的坑中,此時整個序列被基準值劃分成左右兩個子序列。
這裡寫圖片描述

int PartSort2(int *a, int left, int right)
{
    int index = GetMid(a, left, right); //三數取中,選取基準值
    Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
    int key = a[right]; //儲存基準值
    int begin = left;
    int end = right;
    while (begin < end)
    {
        //begin選大
        while (begin < end && a[begin] <= key)
        {
            begin++;
        }
        a[end] = a[begin];
        //end選小
        while (begin < end && a[end] >= key)
        {
            end--;
        }
        a[begin] = a[end];
    }
    a[begin] = key;
    return begin;
}

(3)前後指標法:
     定義兩個指標prev和cur,將基準值放在最右邊,prev初始位置在left-1處,cur初始位置在left處。cur從頭開始找比基準值小的值,找到後若此時++prev的位置和cur的位置不在同一處(說明++prev對應的值一定比基準值大),則交換這兩處的值,cur接著剛才的位置往後找,重複上述過程,直到cur>=right時,將最右邊的基準值和++prev所對應的值進行交換,此時整個序列被基準值劃分成左右兩個子序列。
這裡寫圖片描述

int PartSort3(int *a, int left, int right) 
{
    int index = GetMid(a, left, right); //三數取中,選取基準值
    Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
    int prev = left - 1;
    int cur = left;
    while (cur < right)
    {
        if (a[cur] < a[right] && ++prev != cur)
        {
            Swap(&a[cur], &a[prev]);
        }
        cur++;
    }
    Swap(&a[++prev], &a[right]);
    return prev;
}

三數取中法:

     在選擇基準值時,為了提高排序效率,我們常常利用三數取中法來選擇基準值,所謂的三數指的是:序列最左端的值、中間位置的值和最右端的值,計算它們的中位數來作為基準值。

int GetMid(int *a, int left, int right)
{
    int mid = (left + right) >> 1;
    if (a[left] < a[right])
    {
        if (a[mid] < a[left])
        {
            return left;
        }
        else if (a[mid] > a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
    else
    {
        if (a[mid] > a[left])
        {
            return left;
        }
        else if (a[mid] < a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
}

程式碼實現:

(1)遞迴法:

void QuickSortR(int *a, int left, int right)
{
    assert(a);
    if (left >= right)
    {
        return;
    }

    if (right - left < 10) //優化:在區間較小時,插入排序的效率高
    {
        InsertSort(a, right - left + 1);
    }
    else
    {
        int div = PartSort1(a, left, right);
        QuickSortR(a, left, div - 1);
        QuickSortR(a, div + 1, right);
    }
}

(2)非遞迴法:
     借用棧的結構來模仿遞迴(相關棧的函式定義請檢視順序棧

void QuickSort(int *a, int left, int right)
{
    assert(a);
    Stack s;
    StackInit(&s);
    StackPush(&s, left);
    StackPush(&s, right);
    while (!StackEmpty(&s))
    {
        int end = StackTop(&s);
        StackPop(&s);
        int begin = StackTop(&s);
        StackPop(&s);
        int div = PartSort1(a, begin, end);
        if (begin < div - 1)
        {
            StackPush(&s, begin);
            StackPush(&s, div - 1);
        }
        if (div + 1 < end)
        {
            StackPush(&s, div + 1);
            StackPush(&s, end);
        }
    }
}

分析:

時間複雜度:

  • 最好情況:O(N*lgN)
  • 最壞情況:O(N^2)
  • 平均情況:O(N*lgN)

空間複雜度:O(lgN)——遞迴深度
穩定性:不穩定

七、歸併排序

基本思想:

     將待排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後再將它們合併成一個有序序列。

圖解:

這裡寫圖片描述

合併圖解:

這裡寫圖片描述

程式碼實現:

//歸併區域性遞迴
void _MergeSort(int *a, int left, int right, int *tmp)
{
    if (left >= right)
    {
        return;
    }

    if (right - left < 10) //優化:在區間較小時,插入排序的效率高
    {
        InsertSort(a, right - left + 1);
        return;
    }

    int mid = left + ((right - left) >> 1);
    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid + 1, right, tmp);

    int p = left;
    int q = mid + 1;
    int index = 0;
    while (p <= mid&&q <= right)
    {
        if (a[p] <= a[q]) //加上"="可保持穩定性
        {
            tmp[index++] = a[p++];
        }
        else
        {
            tmp[index++] = a[q++];
        }
    }
    while (p <= mid)
    {
        tmp[index++] = a[p++];
    }
    while (q <= mid)
    {
        tmp[index++] = a[q++];
    }

    int j = 0;
    for (int i = left; i <= right; i++)
    {
        a[i] = tmp[j++];
    }
}

void MergeSort(int *a, int left, int right)
{
    assert(a);
    int *tmp = (int*)malloc((right - left + 1)*sizeof(int));
    memset(tmp, 0, (right - left + 1)*sizeof(int));
    //或者:int *tmp = (int *)calloc(right - left + 1,sizeof(int));
    _MergeSort(a, left, right, tmp);
    free(tmp);
}

分析:

時間複雜度:O(N*lgN)
空間複雜度:O(N)——臨時陣列
穩定性:穩定

八、計數排序

基本思想:

     統計待排序序列中每個元素出現的次數,再根據統計的結果重新對元素進行回收。

圖解:

這裡寫圖片描述

程式碼實現:

void CountSort(int *a, int size)
{
    assert(a);
    //(1)找出最值,確定範圍,以便開闢空間
    int max = a[0];
    int min = a[0];
    int index = 0;
    for (index = 1; index < size; index++)
    {
        if (a[index]>max)
        {
            max = a[index];
        }
        if (a[index] < min)
        {
            min = a[index];
        }
    }
    int range= max - min + 1;
    //(2)統計出現次數
    int *tmp = (int*)calloc(range, sizeof(int));
    for (index = 0; index < size; index++)
    {
        tmp[a[index] - min]++;
    }
    //(3)回收
    int i = 0;
    for (index = 0; index < range; index++)
    {
        while (tmp[index])
        {
            a[i++] = index + min;
            tmp[index]--;
        }
    }
    free(tmp);
    tmp = NULL;
}

分析:

時間複雜度:O(N+range)
空間複雜度:O(range)
穩定性:穩定

九、總結:

這裡寫圖片描述