排序算法系列: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 是要进行修改的。详细过程,参见下面的算法步骤。
算法步骤
通过上面的算法原理分析,这里可以总结出算法实现的步骤。如下:
- 获得一个无序的序列 T0 = [4, 3, 6, 5, 9, 0, 8, 1, 7, 2],并计算出当前序列状态下的步长 step = length / 3 + 1;
- 对序列 T0 按照步长周期分组;
- 对每个子序列进行插入排序操作;
- 判断此时的步长 step 是否大于 1?如果比 1 小或是等于 1,停止分组和对子序列的插入排序,否则,就继续;
- 修改此时的步长 step = step / 3 + 1,并继续第 2 到第 4 步;
- 排序算法结束,序列已是一个整体有序的序列。
逻辑实现
这是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) | 不稳定 | 较复杂 |
问题解答
- 为什么我们排序的时候并不是在这些分组内部进行的?
答:这里我们分组的目的在于要完成的是对整个序列的基本序列处理,那么我们肯定想要让这些分组的数据尽量地分散开。如果要针对每个分组内部进行插入排序,那么之后的每次操作,都会是在内部进行的,这样的结果就是每个分组内部已经有序,只是整体仍然是无序的。 - 分组步长的计算公式为什么是 step = step / 3 + 1?
答:这里的步长计算很关键,可是究竟应该选取什么样的增量才是最好的,目前还尚未解决。不过大量的研究表明,当增量序列为 dlta[k] = 2 t − k + 1 2^t-k+1 2t−k+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 排序算法的主要内容,如果未能解决你的问题,请参考以下文章