排序算法總結

排序算法:一種能將一串數據依照特定的排序方式進行排列的一種算法。
排序算法性能:取決於時間和空間複雜度,其次還得考慮穩定性,及其適應的場景。
穩定性:讓本來有相等鍵值的記錄維持相對次序。也就是若一個排序算法是穩定的,當有倆個相等鍵值的記錄R和S,且本來的序列中R在S前,那麼排序後的列表中R應該也在S以前。 git

如下來總結經常使用的排序算法,加深對排序的理解。github

排序算法目錄

冒泡排序

原理

倆倆比較相鄰記錄的排序碼,若發生逆序,則交換;有倆種方式進行冒泡,一種是先把小的冒泡到前邊去,另外一種是把大的元素冒泡到後邊。算法

性能

時間複雜度爲O(N^2),空間複雜度爲O(1)。排序是穩定的,排序比較次數與初始序列無關,但交換次數與初始序列有關。shell

優化

若初始序列就是排序好的,對於冒泡排序仍然還要比較O(N^2)次,但無交換次數。可根據這個進行優化,設置一個flag,當在一趟序列中沒有發生交換,則該序列已排序好,但優化後排序的時間複雜度沒有發生量級的改變。數組

代碼

void bubble_sort(int arr[], int len){
//每次從後往前冒一個最小值,且每次能肯定一個數在序列中的最終位置
    for (int i = 0; i < len-1; i++){         //比較n-1次
        bool exchange = true;               //冒泡的改進,若在一趟中沒有發生逆序,則該序列已有序
        for (int j = len-1; j >i; j--){    // 每次從後邊冒出一個最小值
            if (arr[j] < arr[j - 1]){       //發生逆序,則交換
                swap(arr[j], arr[j - 1]);
                exchange = false;
            }
        }
        if (exchange){
            return;
        }
    }
}

插入排序

原理

依次選擇一個待排序的數據,插入到前邊已排好序的序列中。ide

性能

時間複雜度爲O(N^2),空間複雜度爲O(1)。算法是穩定的,比較次數和交換次數都與初始序列有關。性能

優化

直接插入排序每次往前插入時,是按順序依次往前找,可在這裏進行優化,往前找合適的插入位置時採用二分查找的方式,即折半插入。
折半插入排序相對直接插入排序而言:平均性能更快,時間複雜度降至O(NlogN),排序是穩定的,但排序的比較次數與初始序列無關,老是須要foor(log(i))+1次排序比較。優化

使用場景

當數據基本有序時,採用插入排序能夠明顯減小數據交換和數據移動次數,進而提高排序效率。ui

代碼

void insert_sort(int arr[], int len){
//每次把當前的數往前插入,能夠順序插入,改進的能夠進行二分插入
    for (int i = 1; i < len; i++){
        if (arr[i] < arr[i - 1]){      //發生逆序,往前插入
            int temp = arr[i];
            int j;
            for (j = i - 1;j>=0 && arr[j]>temp; j--){
                arr[j+1] = arr[j];
            }
            arr[j+1] = temp;
        }
    }
}

void insert_binary_sort(int arr[], int len){
    //改進的插入排序,往前插入比較時,進行二分查找
    for (int i = 1; i < len; i++){
        if (arr[i] < arr[i - 1]){
            int temp = arr[i];
            int low = 0, high = i - 1, mid;
            while (low <= high){
                mid = (low + high) / 2;
                if (temp < arr[mid]){
                    high = mid - 1;
                }
                else{
                    low = mid + 1;
                }
            }
            for (int j = i; j >low; j--){
                arr[j] = arr[j - 1];
            }
            arr[low] = temp;
        }
    }
}

希爾排序

原理

插入排序的改進版,是基於插入排序的如下倆點性質而提出的改進方法:spa

  • 插入排序對幾乎已排好序的數據操做時,效率很高,能夠達到線性排序的效率。
  • 但插入排序在每次往前插入時只能將數據移動一位,效率比較低。

因此希爾排序的思想是:

  • 先是取一個合適的gap<n做爲間隔,將所有元素分爲gap個子序列,全部距離爲gap的元素放入同一個子序列,再對每一個子序列進行直接插入排序;
  • 縮小間隔gap,例如去gap=ceil(gap/2),重複上述子序列劃分和排序
  • 直到,最後gap=1時,將全部元素放在同一個序列中進行插入排序爲止。

性能

開始時,gap取值較大,子序列中的元素較少,排序速度快,克服了直接插入排序的缺點;其次,gap值逐漸變小後,雖然子序列的元素逐漸變多,但大多元素已基本有序,因此繼承了直接插入排序的優勢,能以近線性的速度排好序。

代碼

void shell_sort(int arr[], int len){
    //每次選擇一個gap,對相隔gap的數進行插入排序
    for (int gap = len / 2; gap > 0; gap /= 2){
        for (int i = 0; i < len; i = i + gap){
            int temp = arr[i];
            int j;
            for (j = i; j >= gap && temp < arr[j-gap]; j -= gap){
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
}

選擇排序

原理

每次從未排序的序列中找到最小值,記錄並最後存放到已排序序列的末尾

性能

時間複雜度爲O(N^2),空間複雜度爲O(1),排序是不穩定的(把最小值交換到已排序的末尾致使的),每次都能肯定一個元素所在的最終位置,比較次數與初始序列無關。

代碼

void select_sort(int arr[], int len){
    //每次從後邊選擇一個最小值
    for (int i = 0; i < len-1; i++){     //只需選擇n-1次
        int min = i;
        for (int j = i+1; j < len; j++){
            if (arr[min]>arr[j]){
                min = j;
            }
        }
        if (min != i){
            swap(arr[i], arr[min]);
        }
    }
}

快速排序

原理

分而治之思想:

  • Divide:找到基準元素pivot,將數組A[p..r]劃分爲A[p..pivotpos-1]和A[pivotpos+1...q],左邊的元素都比基準小,右邊的元素都比基準大;
  • Conquer:對倆個劃分的數組進行遞歸排序;
  • Combine:由於基準的做用,使得倆個子數組就地有序,無需合併操做。

性能

快排的平均時間複雜度爲O(NlogN),空間複雜度爲O(logN),但最壞狀況下,時間複雜度爲O(N^2),空間複雜度爲O(N);且排序是不穩定的,但每次都能肯定一個元素所在序列中的最終位置,複雜度與初始序列有關。

優化

當初始序列是非遞減序列時,快排性能降低到最壞狀況,主要由於基準每次都是從最左邊取得,這時每次只能排好一個元素。
因此快排的優化思路以下:

  • 優化基準,不每次都從左邊取,能夠進行三路劃分,分別取最左邊,中間和最右邊的中間值,再交換到最左邊進行排序;或者進行隨機取得待排序數組中的某一個元素,再交換到最左邊,進行排序。
  • 在規模較小狀況下,採用直接插入排序

代碼

//快速排序
int partition(int arr[], const int left, const int right){
    //對序列進行劃分,以第一個爲基準
    int pivot = arr[left];
    int pivotpos = left;
    for (int i = left+1; i <= right; i++){
        if (arr[i] < pivot){
            pivotpos++;
            if (pivotpos != i){     //若是交換元素就位於基準後第一個,則不須要交換
                swap(arr[i], arr[pivotpos]);
            }
        }
    }
    arr[left] = arr[pivotpos];
    arr[pivotpos] = pivot;
    return pivotpos;
}
void quick_sort(int arr[],const int left,const int right){
    if (left < right){
        int pivotpos = partition(arr, left, right);
        quick_sort(arr, left, pivotpos - 1);
        quick_sort(arr, pivotpos + 1, right);
    }
}
void quick_sort(int arr[], int len){
    quick_sort(arr, 0, len - 1);
}

int improve_partition(int arr[], int left, int right){
    //基準進行隨機化處理
    int n = right - left + 1;
    srand(time((unsigned)0));
    int gap = rand() % n;
    swap(arr[left], arr[left + gap]);  //把隨機化的基準與左邊進行交換
    //再從左邊開始進行
    return partition(arr,left,right);
}
void quick_improve_sort(int arr[], const int left, const int right){
    //改進的快速排序
    //改進的地方:一、在規模較小時採用插入排序
    //二、基準進行隨機選擇
    int M = 5;
    if (right - left < M){
        insert_sort(arr, right-left+2);
    }
    if (left>=right){
        return;
    }
    int pivotpos = improve_partition(arr, left, right);
    quick_improve_sort(arr, left, pivotpos - 1);
    quick_improve_sort(arr, pivotpos + 1, right);
}
void quick_improve_sort(int arr[], int len){
    quick_improve_sort(arr, 0, len - 1);
}

歸併排序

原理

分而治之思想:

  • Divide:將n個元素平均劃分爲各含n/2個元素的子序列;
  • Conquer:遞歸的解決倆個規模爲n/2的子問題;
  • Combine:合併倆個已排序的子序列。

性能

時間複雜度老是爲O(NlogN),空間複雜度也總爲爲O(N),算法與初始序列無關,排序是穩定的。

優化

優化思路:

  • 在規模較小時,合併排序可採用直接插入;
  • 在寫法上,能夠在生成輔助數組時,倆頭小,中間大,這時不須要再在後邊加倆個while循環進行判斷,只需一次比完。

代碼

//歸併排序
void merge(int arr[],int temp_arr[],int left,int mid, int right){
    //簡單歸併:先複製到temp_arr,再進行歸併
    for (int i = left; i <= right; i++){
        temp_arr[i] = arr[i];
    }
    int pa = left, pb = mid + 1;
    int index = left;
    while (pa <= mid && pb <= right){
        if (temp_arr[pa] <= temp_arr[pb]){
            arr[index++] = temp_arr[pa++];
        }
        else{
            arr[index++] = temp_arr[pb++];
        }
    }
    while(pa <= mid){
        arr[index++] = temp_arr[pa++];
    }
    while (pb <= right){
        arr[index++] = temp_arr[pb++];
    }
}
void merge_improve(int arr[], int temp_arr[], int left, int mid, int right){
    //優化歸併:複製時,倆頭小,中間大,一次比較完
    for (int i = left; i <= mid; i++){
        temp_arr[i] = arr[i];
    }
    for (int i = mid + 1; i <= right; i++){
        temp_arr[i] = arr[right + mid + 1 - i];
    }
    int pa = left, pb = right, p = left;
    while (p <= right){
        if (temp_arr[pa] <= temp_arr[pb]){
            arr[p++] = temp_arr[pa++];
        }else{
            arr[p++] = temp_arr[pb--];
        }
    }
}
void merge_sort(int arr[],int temp_arr[], int left, int right){
    if (left < right){
        int mid = (left + right) / 2;
        merge_sort(arr,temp_arr,0, mid);
        merge_sort(arr, temp_arr,mid + 1, right);
        merge(arr,temp_arr,left,mid,right);
    }
}

void merge_sort(int arr[], int len){
    int *temp_arr = (int*)malloc(sizeof(int)*len);
    merge_sort(arr,temp_arr, 0, len - 1);
}

堆排序

原理

堆的性質:

  • 是一棵徹底二叉樹
  • 每一個節點的值都大於或等於其子節點的值,爲最大堆;反之爲最小堆。

堆排序思想:

  • 將待排序的序列構形成一個最大堆,此時序列的最大值爲根節點
  • 依次將根節點與待排序序列的最後一個元素交換
  • 再維護從根節點到該元素的前一個節點爲最大堆,如此往復,最終獲得一個遞增序列

性能

時間複雜度爲O(NlogN),空間複雜度爲O(1),由於利用的排序空間仍然是初始的序列,並未開闢新空間。算法是不穩定的,與初始序列無關。

使用場景

想知道最大值或最小值時,好比優先級隊列,做業調度等場景。

代碼

void shiftDown(int arr[], int start, int end){  
    //從start出發到end,調整爲最大堆
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end){
        //先選取子節點中較大的
        if (son + 1 <= end && arr[son] < arr[son + 1]){
            son++;
        }
        //若子節點比父節點大,則交換,繼續往子節點尋找;不然退出
        if (arr[dad] < arr[son]){
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
        else{
            break;
        }
    }
}
void heap_sort(int arr[], int len){
    //先調整爲最大堆,再依次與第一個交換,進行調整,最後構成最小堆
    for (int i = (len - 2) / 2; i >= 0; i--){   //len爲總長度,最後一個爲len-1,因此父節點爲    (len-1-1)/2
        shiftDown(arr,i,len-1);
    }
    for (int i = len - 1; i >= 0; i--){
        swap(arr[i], arr[0]);
        shiftDown(arr, 0,i-1);
    }
}

計數排序

原理

先把每一個元素的出現次數算出來,而後算出該元素所在最終排好序列中的絕對位置(最終位置),再依次把初始序列中的元素,根據該元素所在最終的絕對位置移到排序數組中。

性能

時間複雜度爲O(N+K),空間複雜度爲O(N+K),算法是穩定的,與初始序列無關,不須要進行比較就能排好序的算法。

使用場景

算法只能使用在已知序列中的元素在0-k之間,且要求排序的複雜度在線性效率上。

代碼

//計數排序
void count_sort(int arr[],int sorted_arr[],int len,int k){
    //數組中的元素大小爲0-k,
    //先統計每一個數的相對位置,再算出該數所在序列中排序後的絕對位置
    int *count_arr = (int*)malloc(sizeof(int)*(k+1));
    for (int i = 0; i <= k; i++){
        count_arr[i] = 0;
    }
    for (int i = 0; i < len; i++){       //每一個元素的相對位置
        count_arr[arr[i]]++;
    }
    for (int i = 1; i <= k; i++){       //每一個元素的絕對位置,位置爲第1個到n個
        count_arr[i] += count_arr[i - 1];
    }
    for (int i = len-1; i >=0; i--){     //從後往前,可以使排序穩定,相等的倆個數的位置不會發    生逆序
        count_arr[arr[i]]--;             //把在排序後序列中絕對位置爲1-n的數依次放入到0-    (n-1)中
        sorted_arr[count_arr[arr[i]]] = arr[i];
    }
    free(count_arr);
}

桶排序

原理

  • 根據待排序列元素的大小範圍,均勻獨立的劃分M個桶
  • 將N個輸入元素分佈到各個桶中去
  • 再對各個桶中的元素進行排序
  • 此時再按次序把各桶中的元素列出來便是已排序好的。

性能

時間複雜度爲O(N+C),O(C)=O(M(N/M)log(N/M))=O(NlogN-NlogM),空間複雜度爲O(N+M),算法是穩定的,且與初始序列無關。

使用場景

算法思想和散列中的開散列法差很少,當衝突時放入同一個桶中;可應用於數據量分佈比較均勻,或比較側重於區間數量時。

基數排序

原理

對於有d個關鍵字時,能夠分別按關鍵字進行排序。有倆種方法:

  • MSD:先從高位開始進行排序,在每一個關鍵字上,可採用計數排序
  • LSD:先從低位開始進行排序,在每一個關鍵字上,可採用桶排序

性能

時間複雜度爲O(d*(N+K)),空間複雜度爲O(N+K)。

總結

以上排序算法的時間、空間與穩定性的總結以下:

Algorithm Average Best Worst extra space stable
冒泡排序 O(N^2) O(N) O(N^2) O(1) 穩定
直接插入排序 O(N^2) O(N) O(N^2) O(1) 穩定
折半插入排序 O(NlogN) O(NlogN) O(N^2) O(1) 穩定
簡單選擇排序 O(N^2) O(N^2) O(N^2) O(1) 不穩定
快速排序 O(NlogN) O(NlogN) O(N^2) O(logN)~O(N^2) 不穩定
歸併排序 O(NlogN) O(NlogN) O(NlogN) O(N) 穩定
堆排序 O(NlogN) O(NlogN) O(NlogN) O(1) 不穩定
計數排序 O(d*(N+K)) O(d*(N+K)) O(d*(N+K)) O(N+K) 穩定

本文發表於我的博客:http://lavnfan.github.io/,歡迎指教。

相關文章
相關標籤/搜索