基礎排序算法詳解與優化

文章圖片存儲在GitHub,網速不佳的朋友,請看《基礎排序算法詳解與優化》 或者 來個人技術小站 godbmw.comhtml

1. 談談基礎排序

常見的基礎排序有選擇排序、冒泡排序和插入排序。衆所周知,他們的時間複雜度是 O(n*n)。ios

可是,如今要從新認識一下基礎排序算法,尤爲是「插入排序」:在近乎有序的狀況下,插入排序的時間複雜度能夠下降到 O(n)的程度。算法

所以,在處理系統日誌的任務中,由於日誌記錄是按照時間排序,但偶爾會有幾條是亂序,此時使用插入排序再好不過。而對於高級排序算法,一個常見的優化就是利用插入排序作局部數據排序優化。編程

2. 算法實現

排序算法被封裝在了SortBase.h中的SortBase命名空間中,以實現模板化和防止命名衝突。以下圖所示:segmentfault

2.1 選擇排序

假設從小到大排序,那麼,剛開始指針指向第一個數據,選擇從當前指針所指向數據到最後一個數據間最小的數據,將它放在指針位置。dom

指針後移一位,重複上述步驟,直到指針移動到最後一個數據。函數

這種重複保證了每次,指針前面的數據都是從小到大排好順序的數據。因此,從頭至尾掃描一遍,天然排好序了。性能

代碼以下:學習

template <typename T>
void selectionSort(T arr[], int n) {
  int minIndex = -1;
  for(int i = 0; i < n; i++) {
    minIndex = i;
    for(int j = i+1; j < n; ++j) {
      if(arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    swap(arr[i], arr[minIndex]);
  }
}

2.2 冒泡排序

假設排序是從小到大排序。測試

我一直感受冒泡排序是和選擇排序反過來了(若是說錯請指正)。由於選擇排序是每次選擇最小的數據,放到當前指針位置;而冒泡排序是把不停交換相鄰數據,直到把最大的數據「冒泡」到應該到的位置。

優化的地方是:記錄每次交換的最後位置,在此以後的元素在下一輪掃描中均不考慮。由於交換的最後位置以後的元素已是從小到大排序好了的。

在實現過程當中,由於須要不停交換相鄰兩個數據,所以,消耗了不少額外時間。

template <typename T>
void bubbleSort(T arr[], int n) {
  int newn;
  do {
    newn = 0;
    for(int i = 1; i < n; i++) {
      if(arr[i-1] > arr[i]) {
        swap(arr[i-1], arr[i]);
        // 優化
        newn = i;
      }
    }
    n = newn; // 再也不考慮 newn 後的數據
  } while (newn > 0);
}

2.3 插入排序

插入排序容易和上面兩個算法搞混。能夠類比打撲克牌時候的對撲克牌進行排序:咱們會先排序前 1 張、而後是前 2 張、前 3 張 ... 一直到前 n 張。算法實現顯然是雙重循環,以下所示:

template <typename T>
void insertionSort(T arr[], int n) {
  for(int i = 1; i < n; i++) {
    for(int j = i ; j > 0; j--) {
      if(arr[j - 1] > arr[j]) {
        swap(arr[j], arr[j - 1]);
      } else {
        break; // 優化:已經保證以前都是正常排序,直接跳出便可
      }
    }
  }
}

顯然,插入排序也能在局部排好序的狀況下跳出循環(代碼中的優化),以減小算法消耗時間。

然而上述算法其實跑分並比不上選擇排序,由於swap(arr[j], arr[j - 1]);這行代碼交換了一次,至關於賦值 3 次,在大數據量狀況下,比較消耗時間。

優化: 內層循環,每次保存arr[i], 在檢測到當前數據大於arr[i]的時候,後移一位當前元素arr[j] = arr[j-1];。當跳出內層循環時,直接將保存的arr[i]賦值給arr[j]便可。

template <typename T>
void insertionSort(T arr[], int n) {
  for(int i = 1; i < n; i++) {
    T e = arr[i];
    int j = i ;
    for(; j > 0 && arr[j-1] > e; j--) {
      arr[j] = arr[j-1];
    }
    arr[j] = e;
  }
}

3. 性能測試

首先利用 SortTestHelper::generateRandomArray函數生成大量無序隨機數據,而後進行排序和時間測定。代碼以下:

#include <iostream>
#include "SortHelper.h"
#include "SortBase.h"
#include "SortAdvance.h"

using namespace std;

int main() {
  int n = 50000, left = 0, right = n;

  int *arr = SortTestHelper::generateRandomArray<int>(n, left, right);
  int *brr = SortTestHelper::copyArray<int>(arr, n);
  int *crr = SortTestHelper::copyArray<int>(arr, n);
  SortTestHelper::testSort<int>(arr, n, SortBase::selectionSort<int>, "selection sort");
  SortTestHelper::testSort<int>(brr, n, SortBase::insertionSort<int>, "insertion sort");
  SortTestHelper::testSort<int>(crr, n, SortBase::bubbleSort<int>, "bubble sort");
  delete[] brr;
  delete[] arr;
  delete[] crr;

  return 0;
}

運行結果以下圖所示:

除了大量無序隨機數據,相似於系統日誌的數據就是基本有序的大量數據。此時,測試代碼以下:

#include <iostream>
#include "SortHelper.h"
#include "SortBase.h"
#include "SortAdvance.h"

using namespace std;

int main() {

  int n = 50000, left = 0, right = n;
  int *arr = SortTestHelper::generateNearlyOrderedArray<int>(n, 10);
  int *brr = SortTestHelper::copyArray<int>(arr, n);
  int *crr = SortTestHelper::copyArray<int>(arr, n);
  SortTestHelper::testSort<int>(arr, n, SortBase::selectionSort<int>, "selection sort");
  SortTestHelper::testSort<int>(brr, n, SortBase::insertionSort<int>, "insertion sort");
  SortTestHelper::testSort<int>(crr, n, SortBase::bubbleSort<int>, "bubble sort");
  delete[] brr;
  delete[] arr;
  delete[] crr;

  return 0;
}

如圖所示,插入排序的只用了 0.002 秒。在這種數據狀況下,插入排序的時間複雜度近似 O(N),絕對快於高級排序的 O(NlogN)。除此以外,還保證了穩定性。

4. 感謝

本篇博客是總結於慕課網的《學習算法思想 修煉編程內功》的筆記,liuyubobobo 老師人和講課都很 nice,歡迎去買他的課程。

5. 更多內容

相關文章
相關標籤/搜索