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

概述

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


版权说明

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


目录


算法简介

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


算法原理分析

在上一篇《排序算法系列:插入排序算法》一文中,我们说到了一种复杂度为 O(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(n3/2) O(n2) O(n) O(n) 不稳定 较复杂

问题解答

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

Ref

  • 《大话数据结构》

Github源码下载

https://github.com/William-Hai/ArraySortAlgorithm/blob/master/src/org/algorithm/array/sort/impl/ShellSort.java

原文地址:https://www.cnblogs.com/fengju/p/6335997.html