初级排序算法

本文深入讲解选择排序、插入排序及希尔排序的原理与实现。选择排序通过不断选择最小元素完成排序,适合小型数据集。插入排序则通过将元素插入已排序部分的正确位置,效率受初始顺序影响。希尔排序是对插入排序的优化,通过分组排序提升效率,适用于大型数组。

选择排序

定义

首先, 找到数组中最小的那个元素。 其次, 将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换) 。 再次, 在剩下的元素中找到最小的元素, 将它与数组的第二个元素交换位置。 如此往复, 直到将整个数组排序。 这种方法叫做选择排序, 因为它在不断地选择剩余元素之中的最小者。

选择排序的内循环只是在比较当前元素与目前已知的最小元素(以及将当前索引加 1 和检查是否代码越界) , 这已经简单到了极点。 交换元素的代码写在内循环之外, 每次交换都能排定一个元素, 因此交换的总次数是 N。 所以算法的时间效率取决于比较的次数。

特点

  • 运行时间和输入无关
    为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息,也是它的缺点。
  • 数据移动是最少的
    每次交换都会改变两个数组元素的值, 因此选择排序用了 N 次交换——交换次数和数组的大小是线性关系
package com.lyh.seckill.test;

/**
 * 排序
 */
public class SortUtil {
     public static void selectSort(int[] a) {
        int N = a.length; // 数组长度
        for (int i = 0; i < N; i++) {
            int min = i;
            for (int j = i + 1; j < N; j++) {
                if (a[j] < a[min]) {
                    min = j;
                }
            }
            //交换
            exch(a, i, min);
        }
    }

    public static void main(String[] args) {
        int[] a = new int[]{23, 12, 1, 2, 78, 4, 32, 23, 5, 100, 4};
        show(a);
        selectSort(a);
        show(a);
    }
    private static void exch(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
    private static void show(int[] a) { // 在单行中打印数组
        for (int i = 0; i < a.length; i++)
            System.out.print(a[i] + " ");
        System.out.println();
    }
}

插入排序

定义

通常人们整理桥牌的方法是一张一张的来, 将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中, 为了给要插入的元素腾出空间, 我们需要将其余所有元素在插入之前都向右移动一位。 这种算法叫做插入排序。

与选择排序一样, 当前索引左边的所有元素都是有序的, 但它们的最终位置还不确定, 为了给更小的元素腾出空间, 它们可能会被移动。 但是当索引到达数组的右端时, 数组排序就完成了。

和选择排序不同的是, 插入排序所需的时间取决于输入中元素的初始顺序。 例如, 对一个很大且其中的元素已经有序(或接近有序) 的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。

时间复杂度

对于随机排列的长度为 N 且主键不重复的数组,
平均情况下插入排序需要~ N2/4 次比较 以及~ N2/4 次交换。
最坏情况下需要~ N2/2 次比较和~ N2/2 次交换,
最好情况下需要 N-1次比较和 0 次交换。

插入排序对于实际应用中常见的某些类型的非随机数组很有效。 例如, 正如刚才所提到的, 想想当你用插入排序对一个有序数组进行排序时会发生什么。 插入排序能够立即发现每个元素都已经在合适的位置之上, 它的运行时间也是线性的(对于这种数组, 选择排序的运行时间是平方级别的)。

代码

public static void insertSort(int[] a){
        int N = a.length;
        for(int i = 1; i<N; i++){
            for(int j = i; j > 0 && a[j] < a[j-1]; j--){
                exch(a, j, j-1);
            }
        }
    }

部分有序

如果数组中倒置的数量小于数组大小的某个倍数, 那么我们说这个数组是部分有序的
典型的部分有序数组:

  • 数组中每个元素距离它的最终位置都不远;
  • 一个有序的大数组接一个小数组;
  • 数组中只有几个元素的位置不正确。

插入排序需要的交换操作和数组中倒置的数量相同, 需要的比较次数大于等于倒置的
数量, 小于等于倒置的数量加上数组的大小再减一。

希尔排序

定义

为了展示初级排序算法性质的价值, 接下来我们将学习一种基于插入排序的快速的排序算法。对于大规模乱序数组插入排序很慢, 因为它只会交换相邻的元素, 因此元素只能一点一点地从数组的一端移动到另一端。 例如, 如果主键最小的元素正好在数组的尽头, 要将它挪到正确的位置就需要 N-1 次移动。 希尔排序为了加快速度简单地改进了插入排序, 交换不相邻的元素以对数组的局部进行排序, 并最终用插入排序将局部有序的数组排序。

思想

希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。 这样的数组被称为 h 有序数组。 换句话说, 一个 h 有序数组就是 h 个互相独立的有序数组编织在一起组成的一个数组

思路

实现希尔排序的一种方法是对于每个 h, 用插入排序将 h 个子数组独立地排序。 但因为子数组是相互独立的, 一个更简单的方法是在 h- 子数组中将每个元素交换到比它大的元素之前去(将比它大的元素向右移动一格)。 只需要在插入排序的代码中将移动元素的距离由 1 改为 h 即可。 这样, 希尔排序的实现就转化为了一个类似于插入排序但使用不同增量的过程。
希尔排序更高效的原因是它权衡了子数组的规模和有序性。 排序之初, 各个子数组都很短, 排序之后子数组都是部分有序的, 这两种情况都很适合插入排序

代码

public static void shellSort(int[] a){
        int N = a.length;
        int h = 1;
        while (h < N/3) {
            h = 3 * h + 1;
        }
        while (h >= 1)
        {
            for (int i = h; i < N; i++)
            {
                for (int j = i; j >= h && a[j]<a[j-h]; j -= h)
                    exch(a, j, j-h);
            }
            h = h/3;
        }
    }


果我们在插入排序 中加入一个外循环来将 h 按照递增序列递减, 我们就能得到这个简洁的希尔排序。 增幅 h 的初始值是数组长度乘以一个常数因子, 最小为 1。

递增序列

如何选择递增序列呢? 要回答这个问题并不简单。 算法的性能不仅取决于 h, 还取决于 h 之间的数学性质, 比如它们的公因子等。 有很多论文研究了各种不同的递增序列, 但都无法证明某个序列是“ 最好的” 。

和选择排序以及插入排序形成对比的是, 希尔排序也可以用于大型数组。 它对任意排序(不一定是随机的) 的数组表现也很好

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值