希尔排序

shellShort


插入排序

希尔排序是插入排序的改进版本,它以插入排序为基础。在了解希尔排序前,我们有必要先简单了解插入排序。

插入排序很简单,它假定数组的前i个元素是有序的,然后将第 i+1 个元素插入到 0-i 中的正确位置上。假设第 i+1 个元素将插入到 j 位置,则 j~i 中的元素需要依次移动到 j+1 ~ i+1 的位置上。如 a = [5 9 0 7 1 10 1 7 3 4],在第一轮排序中,a1 > a[0],元素 a1 已经在正确位置上,在第二轮排序中,a2 = 0,经过不断的比较,元素 0 将被插入到 a[0] 位置,而元素 5 和 9 将后移,此时数组为 a = [0 5 9 7 1 10 1 7 3 4]。以此类推。核心代码如下

public static void sort(Comparable[] a) {
    for (int i = 0; i < a.length-1; i++) {
        Comparable tmp = a[i+1];
        int j = i+1;
        for (; j > 0; j--) {
            if (less(tmp, a[j-1])) a[j] = a[j-1];
            else break;
        }
        a[j] = tmp;
    }
}

上述代码需要注意的几点

  • 数组元素类型我们设为 Comparable,则任何实现该接口的类型都符合,如Integer,String
  • less 函数见文末链接的完整代码
  • 这里我们在判断 a[j] < a[j-1] 时,并不是直接交换二者的顺序,而是直接让 a[j-1] 覆盖 a[j],最后再将 a[j] 的值放入正确的位置,提高效率。

算法效率分析:
- 插入排序的时间复杂度取决于数组的初始状态,最好的情况下,数组是预排序的,则只需要比较 n-1 次。O(N)
- 当数组元素是随机的,则对于外循环,需要执行N次,对于内循环,平均需要执行 N/4 次(内循环从 i 开始,所以只需要 N/2 次,又在随机状态下,平均每次循环执行到数组中间就 break 出来,故总的为 N/4),即实际情况一般为, 所以 O(N^2)

插入排序的特点:

  • 对于小数组十分高效
  • 对于预排序数组十分高效
  • 每次只能比较相邻元素,比如最后一个元素的实际插入位置为0,则需要比较 N 次,效率比较低。

希尔排序

对于插入排序,它的实质是通过不断的比较来删除数组中的 逆序对,因此,删除逆序对的次数即可间接表示算法的效率,我们也知道,插入排序只能通过比较相邻元素,并在一次交换中只能删除一个逆序对,因此效率有限,而希尔排序则被设计用来解决这个问题。

希尔排序通过每次比较不相邻的元素并执行交换,可以在一个交换中删除一个或多个逆序对,从而在插入排序的基础上,提高算法效率。

如数组 a = [ 2 10 7 1 3 4 ],我们考虑 a[3] = 1 元素,易知它的正确位置应该为 a[0],在插入排序中,我们需要比较 a[3], a2 然后交换二者,在比较 a2, a1,再比较 a1, a[0] ,通过三个交换删除了三个逆序对使其回到正确的位置上。而在希尔排序中,我们可以直接比较 a[3], a[0],交换二者,此时我们通过一次交换删除了三个逆序对,这便是希尔排序最核心的思想。

再进一步,我们是如何做到比较不相邻元素的?
假设在第一轮排序中,我们使用增量序列为 h = 3;则上述a数组将被划分为几个逻辑数组,分别为 s1=[2, 1], s2 = [10, 3], s3 = [7, 4]。我们可以发现,这几个子数组的元素在原数组中的索引是已h=3为间隔。
接下来,我们分别对这三个数组进行插入排序。为 s1=[1, 2], s2 = [3, 10], s3 = [4, 7],则a数组为 a = [1, 2, 4, 2, 10, 7]
接下来,我们修改增量序列为 h = 2,重复上述过程。
最后我们只要保证 h 最终的值能等于1,即可保证最后一次排序是一次标准的插入排序,则数组一定有序。

通过上述过程,我们可以发现,希尔排序在排序前半段可以保证当前排序数组比较小(如s1),在排序后半段可以保证数组是部分有序的,所以其充分利用了插入排序的两个性质。因此十分高效。
核心代码如下:

public static void sort(Comparable[] a) {
    int n = a.length;
    int h = n / 2;
    while (h >= 1) {
        for (int i = h; i < n; i++) {
            Comparable tmp = a[i];
            int j = i;
            for (; j >= h; j -= h) {
                if (less(tmp, a[j-h])) a[j] = a[j-h];
                else break;
            }
            a[j] = tmp;
        }
        h /= 2;
    }
}

对于上述实现,有下列几点说明

  • 增量序列的选取不是唯一的,并且不同的增量序列对算法的影响也是很大的。但最终我们都要保证 h 最后能等于 1,才可以保证最终数组是有序的。
  • 同样的,我们不采取直接交换元素的方式。
  • 把 h 当作 1,并去掉最外层的 while循环,其实就是一个插入排序

对于中等规模的数据,希尔排序是比较不错的选择,一方面它的效率是完全可接受的,另一方面,它的编码非常简短且易于理解。
下面是几种排序的比较(对400个6000长度的随机序列排序的时间和)

Selection: 37.014
Insertion: 36.58
Shell: 6.443
Merge: 5.903
Quick: 3.889

可以发现,希尔排序相对插入排序有很明显的效率提升

插入排序完整代码参考:Insertion.java
希尔排序完整代码参考:Shell.java

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值