排序算法性能比较

排序做为算法最基础的一部分,可是仍是有部分程序员连手写冒泡排序都比较困难,包括我 :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…

相关文章
相关标签/搜索