排序算法性能比較

排序做爲算法最基礎的一部分,可是仍是有部分程序員連手寫冒泡排序都比較困難,包括我 :joy:,看來在咱們有空的時候仍是頗有必要複習一下排序算法喲, 要理解各大排序算法,必定要本身動手畫一畫,這樣才能更好的幫助本身捋清整個排序思路html

可是到底哪一種排序算法更快呢,請往下面看,固然,你也能夠直接看最下面的結果java

因爲本人水平有限,有疏漏或不正確的地方,還請指正git

暴力排序

嗯,這是最簡單的排序了,不須要任何解釋,你也能理解,時間複雜度爲O(n^2),空間複雜度O(1)程序員

[8 4 5 7 1 3 6 2]
1 [8 4 7 5 3 6 2]
1 2 [8 7 5 4 6 3]
1 2 3 [8 7 5 6 4]
1 2 3 4 [8 7 6 5]
1 2 3 4 5 [8 7 6]
1 2 3 4 5 6 [8 7]
1 2 3 4 5 6 7 [8]
複製代碼
// 暴力排序
for (int i = 0; i < data.length - 1; i++) {
    for (int j = i + 1; j < data.length; j++) {
        // 比較並進行交換
        if (data[i] > data[j]) {
            ArrayUtil.swap(data, i, j);
        }
    }
}
複製代碼

冒泡排序

這也是最簡單的排序算法之一了,其思想是經過與相鄰元素的比較將較小(大)值交換到最後面,時間複雜度O(n^2),空間複雜度O(1)github

數組: 8 4 5 7 1 3 6 2
第一輪:[4 5 7 1 3 6 2] 8
第二輪:[4 5 1 3 6 2] 7 8
第三輪:[4 1 3 5 2] 6 7 8
第四輪:[1 3 4 2] 5 6 7 8
第五輪:[1 3 2] 4 5 6 7 8
第六輪:[1 2] 3 4 5 6 7 8
第七輪:[1] 2 3 4 5 6 7 8
複製代碼
// 須要n-1趟遍歷
for (int i = 1; i < data.length; i++) {
    // 將最值依次日後挪
    for (int j = 0; j < data.length - i; j++) {
        if (data[j] > data[j + 1]) {
            ArrayUtil.swap(data, j, j + 1);
        }
    }
}
複製代碼

插入排序

從索引位置爲1的元素開始,對前2個元素進行排序,索引變爲2,對前3個元素進行排序,以此類推,直至排序完成,時間複雜度O(n^2),空間複雜度O(1)算法

數組: 8 4 5 7 1 3 6 2
從i=1開始:[4 8] 5 7 1 3 6 2
第二輪i=2:[4 5 8] 7 1 3 6 2
第三輪i=3:[4 5 7 8] 1 3 6 2
第四輪i=4:[1 4 5 7 8] 3 6 2
第五輪i=5:[1 3 4 5 7 8] 6 2
第六輪i=6:[1 3 4 5 6 7 8] 2
第七輪i=7:[1 2 3 4 5 6 7 8]
複製代碼

參考:bubkoo.com/2014/01/14/…數組

// 依次對前i+1個元素進行排序
for (int i = 1; i < data.length; i++) {
    int curr = data[i];
    int j = i - 1;
    // 將第i個元素插入到正確的位置
    while (j >= 0 && data[j] > curr) {
        data[j + 1] = data[j--];
    }
    data[j + 1] = curr;
}
複製代碼

思考一下,還能夠優化嗎?固然,咱們還能夠實現一個基於二分查找的插入排序數據結構

快速排序

咱們須要一個基準數(就是一個參考數,能夠從數組中隨便選一個)做爲參考,將比基準數大的放到基準數的右側,比基準數小的放到基準數的左側, 爲了完成這項工做,咱們還須要兩個哨兵ij,來對數組進行探測,哨兵jlength-1的位置最早出發,直到找到一個小於基準數的元素中止, 同理,哨兵i從位置0出發,直到遇到大於基準的元素中止,而後對ij處的元素進行交換,j又率先出發,繼續探測,直到i>=j,並將i處的元素與基準數進行交換, 並終止這一輪的探測post

下一輪將分別對基準數的左側和右側進行一次快速排序,重複上述過程,直至排序完成,時間複雜度O(nlog2n),空間複雜度O(nlog2n)性能

數組:5 4 6 7 3 1 2 8
以5爲基準數排序以後的結果:3 4 2 1 [5] 7 6 8
5的左側以3爲基準數,右側以7做爲基準數排序以後的結果:2 1 [3] 4 [5] 6 [7] 8
繼續以新的基準數排序,直到沒法派生新的基準數:1 [2] [3] [4] [6] [7] [8]
複製代碼

參考:wiki.jikexueyuan.com/project/eas…

private void quickSort() {
    quickSortHelper(data, 0, data.length - 1);
}

private void quickSortHelper(int[] data, int start, int end) {
    if (start < end) {
        // 從左邊開始還探測的哨兵
        int i = start;
        // 從右邊開始探測的哨兵
        int j = end;
        // 基準數
        int base = data[i];
        while (i < j) {
            // 找到小於基準數的索引
            while (j > i && data[j] >= base) {
                j--;
            }
            // 找到大於基準數的索引
            while (j > i && data[i] <= base) {
                i++;
            }
            if (i < j) {
                // 交換兩個哨兵處的元素
                ArrayUtil.swap(data, i, j);
            } else if (i == j) {
                // 交換基準數與哨兵處的元素(兩個哨兵必定會相遇)
                ArrayUtil.swap(data, start, i);
            }
        }
        // 對基準數左側的序列進行快速排序
        quickSortHelper(data, start, j - 1);
        // 對基準數右側的序列進行快速排序
        quickSortHelper(data, i + 1, end);
    }
}
複製代碼

選擇排序

這是一種很是直觀的排序算法,其工做原理是在整個未排序序列中找到最小(大)值,並與這個未排序序列的第一元素進行交換,這樣第一個元素就已經排序了, 接下來對索引位置1開始的未排序序列進行排序,以此類推

其主要優勢是數據移動次數較少,時間複雜度O(n^2),空間複雜度O(1)

有數組:[5 4 6 7 3 1 2 8]

排序過程以下:
1 [4 6 7 3 5 2 8]
1 2 [6 7 3 5 4 8]
1 2 3 [7 6 5 4 8]
1 2 3 4 [6 5 7 8]
1 2 3 4 5 [6 7 8]
1 2 3 4 5 6 [7 8]
1 2 3 4 5 6 7 [8]
複製代碼

參考:bubkoo.com/2014/01/13/…

// 從未排序序列中找到最值並交換到序列中的最前面
for (int i = 0; i < data.length - 1; i++) {
    // 未排序序列的起始索引
    int lowIdx = i;
    // 在當前序列中找到最小值索引
    for (int j = i + 1; j < data.length; j++) {
        if (data[j] < data[lowIdx]) {
            lowIdx = j;
        }
    }
    if (lowIdx != i) {
        // 將最小值交換當前序列的最前面
        ArrayUtil.swap(data, i, lowIdx);
    }
}
複製代碼

希爾排序

希爾排序是一個名叫希爾的人發明的一種排序算法,其實質就是一個分組的插入排序,是插入排序的高效率實現,其思想是按數組下標的必定增量gap進行分組, 對每組進行插入排序,隨着增量的減小,直到增量等於零,整個排序完成,又稱縮小增量排序,時間複雜度O(n^1.3),空間複雜度O(1)

數組:5 4 6 7 3 1 2 8

相同符號的表示一組,對同一組進行插入排序:
gap=4: (5) [4] {6} <7> (3) [1] {2} <8>   ==>   (3) [1] {2} <7> (5) [4] {6} <8>

gap=3: (3) [1] {2} (7) [5] {4} (6) [8]   ==>   (3) [1] {2} (6) [5] {4} (7) [8]

gap=2: {3} [1] {2} [6] {5} [4] {7} [8]   ==>   {2} [1] {3} [4] {5} [6] {7} [8]

gap=1: [2] [1] [3] [4] [5] [6] [7] [8]   ==>   [1] [2] [3] [4] [5] [6] [7] [8]
複製代碼

參考:www.cnblogs.com/chengxiao/p…

// 按數組下標增量分組
for (int gap = data.length / 2; gap > 0; gap /= 2) {
    // 從增量的索引位置開始進行插入排序
    for (int i = gap; i < data.length; i++) {
        int curr = data[i];
        int j = i - gap;
        // 將i處的元素插入到正確的位置
        while (j >= 0 && data[j] > curr) {
            data[j + gap] = data[j];
            j -= gap;
        }
        data[j + gap] = curr;
    }
}
複製代碼

歸併排序

歸併排序採用了經典的分治策略,將大問題拆分紅多個小問題逐個求解,好比這裏的歸併排序,將一個數組拆分兩個序列,再分別將這兩個序列拆分紅兩個序列, 直到序列長度爲1,而後依次向上對這兩個序列進行合併排序,這樣每次咱們合併的都是兩個有序的序列,時間複雜度O(nlog2n), 空間複雜度O(n)

好比數組:[8 4 5 7 1 3 6 2]
拆分:[[8 4 5 7] [1 3 6 2]]
再拆分:[[[8 4] [5 7]] [[1 3] [6 2]]]
再拆分,直到長度等於一:[[[[8] [4]] [[5] [7]]] [[[1] [3]] [[6] [2]]]]

合併排序:[[[4 8] [5 7]] [[1 3] [2 6]]]
再向上合併排序:[[4 5 7 8] [1 2 3 6]]
再向上合併,直到合併後序列長度等於原數組長度:[1 2 3 4 5 6 7 8]
複製代碼

參考:www.cnblogs.com/chengxiao/p…

示例代碼以下:

// 組大小,從1開始,以2的倍數增加
int groupSize;
// 將兩個組合並後的最大大小:groupSize*2
int mergedSize = 1;
while (mergedSize <= data.length) {
    groupSize = mergedSize;
    mergedSize <<= 1;
    // 對mergedSize大小內的兩個分組進行有序合併
    for (int j = 0; j < data.length; j += mergedSize) {
        // 建立一個合法的臨時工做數組
        int diff = data.length - j;
        int[] temp = new int[diff < mergedSize ? diff : mergedSize];
        // 第一個組的起始位置
        int left = j;
        // 第一個組的截止位置
        int maxLeft = j + groupSize;
        // 第二個組的起始位置
        int right = maxLeft;
        // 第二個組的截止位置
        int maxRight = j + temp.length;
        // 有序的合併兩個有序分組
        for (int k = 0; k < temp.length; k++) {
            if (right >= maxRight || (left < maxLeft && data[right] > data[left])) {
                temp[k] = data[left++];
            } else {
                temp[k] = data[right++];
            }
        }
        // 將工做數組拷貝到原數組
        System.arraycopy(temp, 0, data, j, temp.length);
    }
}
複製代碼

堆排序

這種算法稍複雜一些,首先你須要瞭解堆結構,它是一顆近似徹底二叉樹的數據結構,而且須要將它調整成大頂堆或小頂堆,也就是說父節點老是大於(小於)或等於任何一個子節點, 堆化後,將堆頂元素與堆最後一個元素進行交換,堆的最後一個元素將再也不參與下一輪的堆化,重複堆化和交換的過程,直到堆的大小等於1,整個堆排序完成

因此堆排序的重點實際上是如何調整最大(小)堆,若是用數組表示堆的話,父節點爲i的節點,其子節點分別爲2*i+12*i+2,從n/2的父節點開始, 對其子節點進行比較,並調整成最大(小)堆,再對n/2-1的父節點包括其子樹進行調整,最後對0的父節點也就是整顆樹進行調整,整個堆化完成,時間複雜度O(nlog2n), 空間複雜度O(1)

數組:8 4 5 7 1 3 6 2

數組堆化後:
       8
     /   \
    4    [5] (i=2)
   / \   / \
  7   1 3   6
 /
2

從i=4開始調整,發現沒有子節點,i=3時是一顆合法的大頂堆,i=2,調整以下:
       8
     /   \
i=1[4]    6
   / \   / \
  7   1 3   5
 /
2

i=1時調整以下,直到i=0,大頂堆調整完成
       8
     /   \
    7     6
   / \   / \
  4   1 3   5
 /
2

此時數組變成了:[8 7 6 4 1 3 5 2]
將堆頂元素與最後一個元素交換,並將最後一個元素從堆中刪除(不是真的刪除,只是不參與堆化了):[2 7 6 4 1 3 5] 8

堆變成了:

     2                                 7
   /   \                             /   \
  7     6     交換後對新堆進行堆化    4     6
 / \   / \                         / \   / \
4   1 3   5                       2   1 3   5

此時數組變成了:[7 4 6 2 1 3 5] 8
將堆頂與堆最後一個元素交換:[5 4 6 2 1 3] 7 8,交換後堆變成了:

     5                                 6
   /   \                             /   \
  4     6          ===>             4     5
 / \   /                           / \   /
2   1 3                           2   1 3

堆化後數組:[6 4 5 2 1 3] 7 8,交換:[3 4 5 2 1] 6 7 8

     3                                 5
   /   \                             /   \
  4     5          ===>             4     3
 / \                               / \
2   1                             2   1

堆化後數組:[5 4 3 2 1] 6 7 8,交換:[1 4 3 2] 5 6 7 8

    1                               4
   / \                             / \
  4   3          ===>             2   3
 /                               /
2                               1

堆化後數組:[4 2 3 1] 5 6 7 8,交換:[1 2 3] 4 5 6 7 8

  1                           3
 / \                         / \
2   3          ===>         2   1
  
堆化後數組:[3 2 1] 4 5 6 7 8,交換:[1 2] 3 4 5 6 7 8

  1                          2
 /                          /
2             ===>         1

堆化後數組:[2 1] 4 5 6 7 8,交換:[1] 2 3 4 5 6 7 8

當堆中只有一個元素時,排序完成
複製代碼

參考:www.cnblogs.com/chengxiao/p…

private void heapSort() {
    // 將待排序的序列構建成一個大頂堆
    for (int i = data.length / 2; i >= 0; i--) {
        heapSortHelper(data, i, data.length);
    }
    // 逐步將堆頂元素與末尾元素交換,而且再次調整二叉樹,使其成爲大頂堆
    for (int i = data.length - 1; i > 0; i--) {
        // 將堆頂記錄和當前未排序序列的最後一個記錄交換
        ArrayUtil.swap(data, 0, i);
        // 交換以後,須要從新檢查堆是否符合大頂堆,不符合則要調整
        heapSortHelper(data, 0, i);
    }
}

/** * 堆化節點 */
private void heapSortHelper(int[] data, int i, int n) {
    int child;
    int father;
    for (father = data[i]; 2 * i + 1 < n; i = child) {
        child = 2 * i + 1;
        // 若是左子樹小於右子樹,則須要比較右子樹和父節點
        if (child != n - 1 && data[child] < data[child + 1]) {
            // 指向右子樹
            child++;
        }
        // 若是父節點小於孩子結點,則須要交換
        if (father < data[child]) {
            data[i] = data[child];
        } else {
            // 大頂堆結構未被破壞,不須要調整
            break;
        }
    }
    data[i] = father;
}
複製代碼

關於交換

通常來講咱們會使用一個額外的空間來對數組兩個索引位置的元素進行值交換,以下:

private void swap(int[] data, int i, int j) {
    int tmp = data[i];
    data[i] = data[j];
    data[j] = tmp;
}
複製代碼

可是,若是不容許使用額外的空間又如何實現呢?思考一下,不要着急看下面的代碼

private void swap(int[] data, int i, int j) {
    data[i] = data[i] + data[j];
    data[j] = data[i] - data[j];
    data[i] = data[i] - data[j];
}
複製代碼

固然這種方案也有個缺點,當整數足夠大時,它可能會致使整數溢出

知識延伸:爲何說Java只有值傳遞

性能比較

在學習了這些經常使用排序算法以後,下面咱們來看看各個排序算法在不一樣數據量的表現吧

算法名稱 一千數據量 一萬數據量 十萬數據量 百萬數據量
暴力排序 52ms 239ms 37883ms 2639.261s
冒泡排序 11ms 177ms 18665ms 1430.342s
插入排序 5ms 30ms 1430ms 84.078s
快速排序 1ms 4ms 27ms 114ms
選擇排序 11ms 74ms 5080ms 398.866s
希爾排序 1ms 9ms 22ms 191ms
歸併排序 4ms 9ms 41ms 173ms
堆排序 3ms 8ms 16ms 115ms

對上述運行時長進行排序:

一千數據量:
快速排序 > 希爾排序 > 堆排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序 > 暴力排序

一萬數據量:
快速排序 > 堆排序 > 希爾排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序 > 暴力排序

十萬數據量:
堆排序 > 希爾排序 > 快速排序 > 歸併排序 > 插入排序 > 選擇排序 > 冒泡排序 > 暴力排序

百萬數據量:
快速排序 > 堆排序 > 歸併排序 > 希爾排序 > 插入排序 > 選擇排序 > 冒泡排序 > 暴力排序
複製代碼

因爲運行環境和數據的不一樣,運行時長可能會出現較大差別

一般來講,快速排序在數據量較小時,表現得最優秀,而在數據量較大時堆排序表現得更優秀,平均來講希爾排序會比歸併排序快速排序快一點, 固然這些結論都不徹底準確,由於每種算法都存在最優和最壞的狀況,可是後面四種排序算法的排名應該是不會出現變更的,因而可知,這些排序算法之間性能差別仍是很大的

完整源代碼參考

總結

像冒泡這種簡單的排序必定要可以手寫出來,我的以爲除堆排序外,其餘的排序算法,都還好理解,主要是要動手畫一畫整個排序流程,理解整個排序思想, 捋清本身的思路,儘管堆排序比較困難一些,可是最好這些排序算法都可以用本身的代碼實現出來

資料參考:www.cnblogs.com/onepixel/ar…

相關文章
相關標籤/搜索