排序算法系列:Shell 排序算法

Posted Q-WHai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法系列:Shell 排序算法相关的知识,希望对你有一定的参考价值。

概述

希尔排序(Shell Sort)是 D.L.Shell 于 1959 年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O( n 2 n^2 n2) 的,希尔排序算法是突破这个时间复杂度的第一批算法之一。希尔排序是一种插入排序算法。


版权说明

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
本文作者:Q-WHai
发表日期: 2016年4月12日
本文链接:https://qwhai.blog.csdn.net/article/details/51127533
来源:CSDN
更多内容:分类 >> 算法与数学


目录

文章目录

算法简介

希尔排序(Shell Sort)是 D.L.Shell 于 1959 年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O( n 2 n^2 n2) 的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
                                              – 《大话数据结构》


算法原理分析

在上一篇《排序算法系列:插入排序算法》一文中,我们说到了一种复杂度为 O( n 2 n^2 n2) 的排序算法。而对于插入排序而言,如果我们的排序序列基本有序,或是数据量比较小,那么它的排序性能还是不错的。为什么?可能你会这样问。数据量小,这个不用多说,对于多数排序算法这一点都适用;如果一个序列已经基本有序(序列中小数普遍位于大数的前面),那么我们在插入排序的时候,就可以在较少的比较操作之后使整体有序。如果,这一点你还不是很明白,可以先看看《排序算法系列:插入排序算法》一文中的解释。
基于上面基本有序和小数据量的提示,这里就有了希尔排序的实现。希尔所做的就是分组,在每个分组中进行插入排序。如下就是希尔排序中分组的示意图:

在上图中,我们将一个长度为 10 的序列,分组成 3 组。除最后一组以外的分组都包含了 4 个元素。可是,我们排序的时候并不是在这些分组内部进行的。而是我们要按照上面圆形的标号来进行插入排序,也就是排序中的 [4, 9, 7]。你可以先想一想这是为什么,在文章的最后,我再说明这个问题。

上面从详细地说明了希尔排序的由来及希尔排序的分组精髓。那么现在就要说希尔排序中的插入排序,是的没有错。毕竟希尔排序的核心就是分组+插入排序嘛。在这个示意图中,我们可以很明显地看出它想表达的东西。这里只是将[3, 9, 7]看成了一个整体,对于其它的元素,暂时与[3, 9, 7]无关。

通过上面的两步操作,我们可以得到一个序列 T1 = [4, 0, 6, 1, 7, 2, 8, 5, 9, 3]。咦,这个序列还是无序的呀,怎么回事?我们说希尔排序的核心是分组和获得一个基本有序的数组。这里说的是基本有序,并未做出承诺说这个序列已经有序。那么,现在要做的就是继续分组+插入排序。当然,对应的 step 是要进行修改的。详细过程,参见下面的算法步骤。


算法步骤

通过上面的算法原理分析,这里可以总结出算法实现的步骤。如下:

  1. 获得一个无序的序列 T0 = [4, 3, 6, 5, 9, 0, 8, 1, 7, 2],并计算出当前序列状态下的步长 step = length / 3 + 1;
  2. 对序列 T0 按照步长周期分组;
  3. 对每个子序列进行插入排序操作;
  4. 判断此时的步长 step 是否大于 1?如果比 1 小或是等于 1,停止分组和对子序列的插入排序,否则,就继续;
  5. 修改此时的步长 step = step / 3 + 1,并继续第 2 到第 4 步;
  6. 排序算法结束,序列已是一个整体有序的序列。

逻辑实现

这是Shell 排序的核心模块,也是分组的关键步骤

private void core(int[] array) 
        int arrayLength = array.length;
        int step = arrayLength;
        do 
            step = step / 3 + 1;
            for (int i = 0; i < step; i++) 
                insert(array, i, step);
            
            System.err.println(Arrays.toString(array));
         while (step > 1);
    

希尔排序的直接插入排序,注意这里不同的是它只是针对某一个分组子序列进行插入排序

private void insert(int[] array, int offset, int step) 
        int arrayLength = array.length;
        int groupCount = arrayLength / step + (arrayLength % step > offset ? 1 : 0);
        for (int i = 1; i < groupCount; i++) 
            int nextIndex = offset + step * i;
            int waitInsert = array[nextIndex];
            
            while(nextIndex - step >= 0 && waitInsert < array[nextIndex - step]) 
                array[nextIndex] = array[nextIndex - step];
                nextIndex -= step;
            
            
            array[nextIndex] = waitInsert;
        
    

更多详细逻辑实现,请参见文章结尾的源码链接。


复杂度分析

排序方法时间复杂度空间复杂度稳定性复杂性
平均情况最坏情况最好情况
Shell 排序O($n^3/2$)O($n^2$)O(n)O(n)不稳定较复杂

问题解答

  1. 为什么我们排序的时候并不是在这些分组内部进行的?
    答:这里我们分组的目的在于要完成的是对整个序列的基本序列处理,那么我们肯定想要让这些分组的数据尽量地分散开。如果要针对每个分组内部进行插入排序,那么之后的每次操作,都会是在内部进行的,这样的结果就是每个分组内部已经有序,只是整体仍然是无序的。
  2. 分组步长的计算公式为什么是 step = step / 3 + 1?
    答:这里的步长计算很关键,可是究竟应该选取什么样的增量才是最好的,目前还尚未解决。不过大量的研究表明,当增量序列为 dlta[k] = 2 t − k + 1 2^t-k+1 2tk+1 - 1 (0 <= k <= t <= [ l o g 2 log_2 log2(n+1)])时,可以获得不错的效果,其时间复杂度为 O( n 3 / 2 n^3/2 n3/2)。

Ref

  • 《大话数据结构》

Github源码下载

  • https://github.com/qwhai/algorithms-sort

征集

如果你也需要使用ProcessOn这款在线绘图工具,可以使用如下邀请链接进行注册:
https://www.processon.com/i/56205c2ee4b0f6ed10838a6d

以上是关于排序算法系列:Shell 排序算法的主要内容,如果未能解决你的问题,请参考以下文章

排序算法系列:奇偶排序算法

排序算法:Shell Sort 希尔排序

排序算法系列:快速排序算法

Python太难?原来是没搞懂这一点!

排序算法系列:快速排序算法

Python|冒泡算法快速入门