八大排序

八大排序 - Shuai-Xie - Github

內部排序和外部排序

  • 內部排序數據少,數據記錄在內存中進行排序,八大排序都是內部排序。
  • 外部排序數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。

當 n 較大,則應采用時間復雜度為 O(nlog2n) 的排序方法:快速排序、堆排序或歸并排序。快速排序是目前基于比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分布時,快速排序的平均時間最短。

各種排序的穩定性,時間復雜度和空間復雜度總結:

八大排序復雜度

1. 直接插入排序

  • 基本思想:將一個記錄插入到已排序好的有序表中,從而得到一個新的記錄數增1的有序表。即:先將序列的第1個記錄看成是一個有序的子序列,然后從第2個記錄逐個進行插入,直至整個序列有序為止。
  • 要點:設立哨兵,作為 臨時存儲判斷數組邊界 之用。
    有哨兵是將要排序的元素保存為 a[0],而不是再:int x
  • 穩定:如果碰見一個和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變(因為從后往前插,相同的元素,后插入的仍然排在后面),所以插入排序是穩定的。
  • 時間復雜度: O(n2
直接插入排序示例
  • 有哨兵
void insertSort( int *a, int len) {

    // 數組后移
    auto *b = new int[len + 1];
    for (int i = 1; i <= len; ++i) {
        b[i] = a[i - 1];
    }

    // 有哨兵插入排序
    for (int i = 2; i <= len; ++i) { // len是b數組中最后一個元素的下標
        if (b[i] < b[i - 1]) { // 要插入的元素 < 已有序數列的最大元素
            b[0] = b[i];
            int j = i - 1; // 已有序序列的最后一個元素
            while (b[0] < b[j]) { // 此處與無哨兵相比少了一個判斷條件
                b[j + 1] = b[j];
                j--;
            } // 跳出時,b[j]<=b[0]
            b[j + 1] = b[0];
        }
    }

    // 排序好后傳給a
    for (int i = 1; i <= len; ++i) {
        a[i - 1] = b[i];
    }
}
  • 無哨兵
void insertSort(int *arr, int len) {
    for (int i = 1; i < len; ++i) { // 從第2個元素開始插入,原始單個元素視為有序
        if (arr[i] < arr[i - 1]) { // 如果相鄰的2個數不是有序 再排序
            int x = arr[i]; // x 是要插入的元素
            int j = i - 1; // 已有序序列的最后一個
            while (arr[j] > x && j >= 0) { // j >=0 防止數組越界
                arr[j + 1] = arr[j]; // 循環內滿足:arr[j] > x 元素后移1位
                j--;
            }  // 跳出循環時,arr[j] <= x,x 插入在 j+1
            arr[j + 1] = x; 
        }
    }
}
帶哨兵的插入排序中的哨兵元素 a[0] 有兩個作用:
  1. 臨時存放待插入元素
  2. 防止數組下標越界
    當待插入的元素 < 已排序的子數組中的最小元素時,j=-1,越界
    而采用哨兵,arr[0] < arr[j],當 j=0 時,就結束循環,不會出現越界
    while 循環只有一次判斷,提高了效率
  • 但是存在一個問題:原始數組如果從 a[0] 開始,則插入第一個元素時,a[0]會被覆蓋,造成最終排完序的數組缺少了原始數組的第一個元素。
  • 消除此問題:在調用此方法之前,將數組做下處理,使其右移一個元素的位置,空出來的第0個元素初始化為0(或不做初始化)
  • 無哨兵的插入排序,無上述問題,但在效率上會稍低,while 循環中有兩個判斷條件。
  • 摘錄 - 直接插入排序(哨兵和越界)

2. Shell排序

希爾排序又叫縮小增量排序,在直排基礎上改進。
增量序列通常為:d = {n/2 ,n/4, n/8 .....1} 依次減小,最后必須為1。

  • 基本思想:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
  • 不穩定:因為有元素交換。
  • 復雜度:取決于增量因子序列d的選取,目前還沒有選取最好的增量因子序列的方法。
  • 操作方法:如下圖示例。

希爾排序的示例:

非遞歸實現

void shellSort(int *arr, int len) {
    for (int dk = len / 2; dk > 0; --dk) { // dk增量
        for (int i = dk; i < len; ++i) { // 1層for
            if (arr[i] < arr[i - dk]) {
                int x = arr[i]; // 哨兵
                int j = i - dk; // 原有序序列最后一個元素
                while (x < arr[j] && j >= 0) {
                    arr[j + dk] = arr[j];
                    j -= dk;
                }
                arr[j + dk] = x;
            }
        }
    }
}

遞歸實現

// dk是shell排序的增量
void shellInsertSort(int *arr, int len, int dk) {
    // 一層for 
    for (int i = dk; i < len; ++i) { // i=dk,其實是以dk為增量的子序列的第2個元素,與基本插入排序一樣從第2個元素開始

        if (arr[i] < arr[i - dk]) {
            int x = arr[i]; // 哨兵
            int j = i - dk;

            while (x < arr[j] && j >= 0) { // 一定要判斷j>0,因為while循環體中j會<0
                arr[j + dk] = arr[j];
                j -= dk;
            }
            // 跳出時,x >= arr[j]
            arr[j + dk] = x;
        }
    }
}

void shellSort(int *arr, int len) {
    int dk = len / 2;
    while (dk >= 1) {
        shellInsertSort(arr, len, dk);
        dk /= 2;
    }
}

注意:最外層for是dk..len-1,相當于從arr[dk]開始,把后面的元素都進行了插入排序,比如上圖一趟排序的結果,增量為3,先插入55,再插入4,而不是先完成 13, 55, 38, 76 這4個數的排序。如果想一次完成,需要寫2個for,其實參加排序的數是一樣的。

void shellInsertSort(int *arr, int len, int dk) {
    // 兩層for
    for (int k = 0; k < dk; ++k) { // 每次完成從arr[k]開始的增量序列
        for (int i = k + dk; i < len; i += dk) { // 第2個元素開始 
            // 內部不變
            if (arr[i] < arr[i - dk]) {
                int x = arr[i]; // 哨兵
                int j = i - dk;
                while (x < arr[j] && j >= 0) { // 一定要判斷j>0,因為while循環體中j會<0
                    arr[j + dk] = arr[j];
                    j -= dk;
                }
                // 跳出時,x >= arr[j]
                arr[j + dk] = x;
            }
        }
    }
}

3. 直接選擇排序

最小值放前思想

// 選擇排序
void selectSort(int *arr, int len) {
    for (int i = 0; i < len - 1; ++i) {
        for (int j = i + 1; j < len; ++j) {
            if (arr[i] > arr[j]) swap(arr[i], arr[j]); // 最小值放前思想
        }
    }
}

4. 堆排序

堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。

  • 基本思想:將序列調整成大頂堆,然后 swap(arr[0], arr[len-1]),最大值放后的思想,然后再將剩下的序列調整成大頂堆,再將次大值放后……
  • 誤區:依次從 0..len-2 到 len-1 建立小頂堆,這是一種思路,但是這樣去掉的是堆頂,直接就破壞了堆結構,而大頂堆去掉的是堆尾,不會破壞堆結構。
大頂 小頂
構建小頂堆
輸出堆頂后調整堆
// 調整大頂堆
void heapAdjust(int *arr, int root, int len) {
    int child = 2 * root + 1;
    while (child < len) { // child可以去最后一個元素:len-1
        if (child + 1 < len && arr[child] < arr[child + 1]) child++; // child指向大孩子
        if (arr[root] < arr[child]) {
            swap(arr[root], arr[child]);
            root = child;
            child = 2 * root + 1;
        } else {
            break; // 基于下面已經滿足大頂堆
        }
    }
}

// 構建大頂堆
void buildHeap(int *arr, int len) {
    for (int i = (len - 1) / 2; i >= 0; --i) { // (length-1)/2 最大的非葉節點
        heapAdjust(arr, i, len); // i遍歷所有的root
    }
}

// 4.堆排序
void heapSort(int *arr, int len) {
    buildHeap(arr, len);
    cout << "調整之后";
    printArr(arr, len);
    while (len > 1) { 
        swap(arr[0], arr[len - 1]); // 首尾元素互換
        cout << "len=" << len;
        printArr(arr, len);
        len--;
        heapAdjust(arr, 0, len);
    }
}
          49   38   65   97   76   13   27   49   55    4
build     97   76   65   55   49   13   27   49   38    4
len=10     4   76   65   55   49   13   27   49   38   97
len=9     38   55   65   49   49   13   27    4   76
len=8      4   55   38   49   49   13   27   65
len=7     27   49   38    4   49   13   55
len=6     13   49   38    4   27   49
len=5     13   27   38    4   49
len=4      4   27   13   38
len=3     13    4   27
len=2      4   13
    4   13   27   38   49   49   55   65   76   97

5. 冒泡排序

最大值放后思想

// 冒泡排序
void bubbleSort(int *arr, int len) {
    for (int i = 0; i < len - 1; ++i) {
        for (int j = 0; j < len - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) swap(arr[j], arr[j + 1]); // 最大值放后思想
        }
    }
}

6. 快速排序

基本思想:

  1. 選擇一個基準元素,通常選擇第一個元素或者最后一個元素,
  2. 通過一趟排序將序列分成兩部分,一部分比基準值小,另一部分比基準值大。
  3. 此時基準元素正好在其排好序后的正確位置(第k小數)
  4. 然后分別對這兩部分記錄用同樣的方法繼續進行排序,直到整個序列有序。

尋找 pivot 的位置很關鍵。

// 分成兩部分
int partition(int *arr, int low, int high) {
    int pivot = arr[low]; // 選第1個值為基準值
    while (low < high) {
        while (low < high && arr[high] >= pivot) high--;
        swap(arr[high], arr[low]); // 大小值更換,注意:更換的不是pivot
        while (low < high && arr[low] <= pivot) low++;
        swap(arr[high], arr[low]); // 大小值更換
    }
    return low;
}

void quickSort(int *arr, int low, int high) {
    if (low < high) {
        int pivotLoc = partition(arr, low, high); // 基準值位置
//        printArr(arr, high + 1);
        quickSort(arr, low, pivotLoc - 1);
        quickSort(arr, pivotLoc + 1, high);
    }
}

7. 歸并排序

  • 基本思想:歸并(Merge)排序法是將兩個(或兩個以上)有序表合并成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然后再把有序子序列合并為整體有序序列。

歸并排序示例:

void merge(int *arr, int low, int mid, int high) {
    int tmp[high - low + 1]; // 暫存數據

    // 3個序列迭代器
    int i = low; // 序列1開始
    int j = mid + 1; // 序列2開始
    int k = 0; // 合并新序列開始

    while (i <= mid && j <= high) { // 都得小于最后一個元素
        tmp[k++] = (arr[i] <= arr[j]) ? arr[i++] : arr[j++];
    }

    while (i <= mid) tmp[k++] = arr[i++];
    while (j <= high) tmp[k++] = arr[j++];

    i = low; // arr序列開始的位置
    k = 0;
    while (i <= high) arr[i++] = tmp[k++];
}

void mergeSort(int *arr, int low, int high) {
    int mid;
    if (low < high) {
        mid = (low + high) / 2;
        mergeSort(arr, low, mid); // 這里和merge中的j值有關
        mergeSort(arr, mid + 1, high); // 先mergeSort成2個有序序列,再將2個序列合并有完整有序
        merge(arr, low, mid, high);
    }
}

8. 基數排序

  • 基本思想:把數據分組,放在一個個的桶中,然后對每個桶里面的在進行排序。

  • 操作方法:例如要對大小為 [1..1000] 范圍內的 n 個整數 A[1..n] 排序:

    1. 把桶設為大小為10的范圍,具體而言,設集合 B[1] 存儲 [1..10] 的整數,集合B[2] 存儲 [11..20] 的整數……總共100個桶
    2. 對 A[1..n] 從頭到尾掃描一遍,把每個 A[i] 放入對應的桶 B[j] 中
    3. 再對這100個桶中每個桶里的數字排序,可用任何排序方法
    4. 依次輸出每個桶里面的數字。
  • 復雜度:
    假設有n個數字,m個桶,如果數字是平均分布的,每個桶里面平均有n/m個數字
    如果對每個桶中的數字采用快速排序,那么整個算法的復雜度是:
    O(n + m × n/m×log(n/m)) = O(n + n(log(n/m)))
    從上式看出,當 m 接近 n 的時候,桶排序復雜度接近O(n)
    當然,以上復雜度的計算是基于輸入的n個數字是平均分布這個假設的。這個假設是很強的 ,實際應用中效果并沒有這么好。如果所有的數字都落在同一個桶中,那就退化成一般的排序了。

  • 特點:非常耗費空間,如果能實現每個桶剛好1個數字,這是數列就已經有序了。

void radixSort(int *arr, int len, int radix) {

    // 先找到待排序元素的上下界
    int max = arr[0];
    int min = arr[0];
    for (int i = 1; i < len; ++i) {
        if (max < arr[i]) max = arr[i];
        if (min > arr[i]) min = arr[i];
    }
//    cout << max << "\t" << min << endl;

    int bucket_num = max / radix - min / radix + 1; // 桶的數量,一定要分開除
    int bucket_arr[bucket_num][len]; // 存儲元素
    int bucket_len[bucket_num]; // 記錄每個桶的元素個數

    for (int i = 0; i < bucket_num; ++i) bucket_len[i] = 0; // 賦初值

    // 元素進入桶
    cout << "元素進桶" << endl;
    for (int i = 0; i < len; ++i) {
        int bucket_id = arr[i] / radix - min / radix; // 桶id轉移到數列下標,一定要分開除
        bucket_arr[bucket_id][bucket_len[bucket_id]] = arr[i];
        bucket_len[bucket_id]++;
    }

    // 打印各桶元素
    for (int i = 0; i < bucket_num; ++i) {
        cout << radix * (min / radix + i) << "," << radix * (min / radix + i + 1) - 1 << ": ";
        printArr(bucket_arr[i], bucket_len[i]);
    }

    cout << "桶內排序" << endl;
    for (int i = 0; i < bucket_num; ++i) {
        if (bucket_len[i] > 1) quickSort(bucket_arr[i], 0, bucket_len[i] - 1);
    }

    // 打印排序后各桶元素
    for (int i = 0; i < bucket_num; ++i) {
        cout << radix * (min / radix + i) << "," << radix * (min / radix + i + 1) - 1 << ": ";
        printArr(bucket_arr[i], bucket_len[i]);
    }

    // 排序后元素拷貝
    int k = 0;
    for (int i = 0; i < bucket_num; ++i) {
        for (int j = 0; j < bucket_len[i]; ++j) {
            arr[k] = bucket_arr[i][j];
            k++;
        }
    }
}
 83   86   77   15   93   35   86   92   49   21   62   27   90   59   63   26   40   26   72   36

元素進桶
10,19:    15
20,29:    21   27   26   26
30,39:    35   36
40,49:    49   40
50,59:    59
60,69:    62   63
70,79:    77   72
80,89:    83   86   86
90,99:    93   92   90

桶內排序
10,19:    15
20,29:    21   26   26   27
30,39:    35   36
40,49:    40   49
50,59:    59
60,69:    62   63
70,79:    72   77
80,89:    83   86   86
90,99:    90   92   93

   15   21   26   26   27   35   36   40   49   59   62   63   72   77   83   86   86   90   92   93

文章參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。