排序之希尔排序

希尔排序

我们都能理解,优秀排序算法的首要条件就是速度。于是人们想了许许多多的办法,目的就是为了提高排序的速度。而在很长的时间里,众人发现尽管各种排序算法花样繁多(选择、冒泡、直插法),但时间复杂度都是 O(n2),似乎没法超越了。此时,计算机学术界充斥着“排序算法不可能突破 O(n2)”的声音,“不可能成为了主流”。

终于有一天,当一位科学家发布超越了 O(n2) 新排序算法后,紧接着就出现了好几种可以超越 O(n2)的排序算法,并把内排序算法的时间复杂度提升到了 O(nlogn)。“不可能超越 O(n2) ”彻底成为了历史。

1.希尔排序原理

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

我们之前讨论过直接插入排序,应该说,它的效率在某些时候是很高的,比如,我们的记录本身就是基本有序的,我们只需要少量的插入操作,就可以完成整个记录集的排序工作,此时直接插入很高效。还有就是记录数比较少时,直接插入的优势也比较明显。可问题在于,两个条件本身就过于苛刻,现实中记录少或者基本有序都属于特殊情况。

有条件当然是好,条件不存在,我们创造条件也是可以去做的。于是科学家希尔研究出了一种排序方法,对直接插入排序改进后可以增加效率。如何让待排序的记录个数较少呢?很容易想到的就是将原本有大量记录数的记录进行分组。分割成若干个子序列,此时每个子序列待排序的记录个数就比较少了,然后在这些子序列内分别进行直接插入排序,当整个序列都基本有序时,注意只是基本有序时,再对全体记录进行一次直接插入排序。

比如我们现在有序列是 {9,1,5,8,3,7,4,6,2},现在将它分成三组,{9,1,5},{8,3,7},{4,6,2},哪怕将它们各自排序排好了,变成 {1,5,9},{3,7,8},{2,4,6},再合并它们成 {1,5,9,3,7,8,2,4,6},此时,这个序列还是杂乱无序,谈不上基本有序,要排序还是重来一遍直接插入有序,这样做有用吗?需要强调一下,所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,像{2,1,3,6,4,7,5,8,9} 这样可以称为基本有序了。但像 {1,5,9,3,7,8,2,4,6} 这样的 9 在第三位,2 在倒数第三位就谈不上基本有序。

问题其实也就在这里,我们分割待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展。而如上面这样分完组后就各自排序的方法达不到我们的要求。因此,我们需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。

2.希尔排序算法

先上代码,我们通过代码来分析讨论:

/* 对顺序表 L 作希尔排序 */
void ShellSort(SqList *L) {
	int i, j;
	int increment = L->length;
	do {
		/* 增量序列 */
		increment = increment / 3 + 1;
		for (i = increment + 1; i <= L->length; i++) {
			if (L->r[i] < L->r[i - increment]) {
				/* 需将 L->r[i] 插入有序增量子表 */
				/* 暂存在 L->r[0] */
				L->r[0] = L->r[i];
				for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
				/* 记录后移,查找插入位置 */
				L->r[j + increment] = L->r[j];
				/* 插入 */
				L->r[j + increment] = L->r[0];
			}	
		}	/* 外层 for 循环结束 */
	}	while (increment > 1);
}

注意:注释也计入代码的行数

  1. 程序开始运行,此时我们传入的 SqList 参数值为 length = 9,r[10] = {0,9,1,5,8,3,7,4,6,2} (把0位置作为哨兵,不计入待排记录长度)。这是我们需要等待排序的序列,如图:

在这里插入图片描述

2.第 4 行,变量 increment 就是那个“增量”,我们初始值让它等于待排序的记录数。
3.第 5~20 行是一个do循环,它提终止条件是 increment 不大于 1 时,其实也就是增量为 1 时就停止循环了。
4.第 7 行,这一句很关键,但也是难以理解的地方,我们后面还要谈到它,先放一放。这里执行完成后,increment=9/3+1=4。
5.第 8~19 行是一个 for 循环,i 从 4+1=5 开始到 9 结束。
6.第 9 行,判断 L.r[i] 与 L.r[i-incre-ment] 大小,L.r[5]=3 小于 L.r[i-incre-ment]=L.r[1]=9,满足条件,第12行,将L.r[5]=3暂存入L.r[0]。第13~15行的循环只是为了将 L.r[1]=9 的值赋给 L.r[5],由于循环的增量是 j-=increment,其实它就循环了一次,此时 j=-3。第16行,再将 L.r[0]=3 赋值给 L.r[j+increment]=L.r[-3+4]=L.r[1]=3。如图所示,事实上,这一段代码就干了一件事,就是将第 5 位的3 和第 1 位的 9 交换了位置。

在这里插入图片描述

7.循环继续,i=6,L.r[6]=7>L.r[i-incre-ment]=L.r[2]=1,因此不交换两者数据。如图:

在这里插入图片描述

8.循环继续,i=7,L.r[7]=4<L.r[i-increment]=L.r[3]=5,交换两者数据。如图:

在这里插入图片描述

9.循环继续,i=8,L.r[8]=6<L.r[i-increment]=L.r[4]=8,交换两者数据。如图:

在这里插入图片描述

10.循环继续,i=9,L.r[9]=2<L.r[i-incre-ment]=L.r[5]=9,交换两者数据。注意,第 13~15 行是循环,此时还要继续比较 L.r[5] 与 L.r[1] 的大小,因为 2<3,所以还要交换 L.r[5] 与 L.r[1] 的数据,如图:

在这里插入图片描述

最终第一轮循环后,数组的排序结果为图所示。细心的同学会发现,我们的数字1、2等小数字已经在前两位,而 8、9 等大数字已经在后两位,也就是说,通过这样的排序,我们已经让整个序列基本有序了。这其实就是希尔排序的精华所在,它将关键字较小的记录,不是一步一步地往前挪动,而是跳跃式地往前移,从而使得每次完成一轮循环后,整个序列就朝着有序坚实地迈进一步。

在这里插入图片描述

11.我们继续,在完成一轮 do 循环后,此时由于 increment=4>1 因此我们需要继续 do 循环。第 7 行得到 increment=4/3+1=2。第 8~19 行for循环,i 从 2+1=3 开始到 9 结束。当 i=3、4 时,不用交换,当 i=5 时,需要交换数据,如图:

在这里插入图片描述

12.此后,i=6、7、8、9 均不用交换,如图:

在这里插入图片描述

13.再次完成一轮 do 循环,increment=2>1,再次 do 循环,第 7 行得到 increment=2/3+1=1,此时这就是最后一轮 do 循环了。尽管第 8~19 行 for 循环,i 从 1+1=2 开始到 9 结束,但由于当前序列已经基本有序,可交换数据的情况大为减少,效率其实很高。如图所示,图中箭头连线为需要交换的关键字。

在这里插入图片描述

最终完成排序过程,如图:

在这里插入图片描述

3.希尔排序复杂度分析

通过这段代码的剖析,相信大家有些明白,希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。

这里“增量”的选取就非常关键了。我们在代码中第 7 行,是用 increment=increment/3+1; 的方式选取增量的,可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量的研究表明,当增量序列为 dlta[k]=2t-k+1-1(0≤k≤t≤)时,可以获得不错的效率,其时间复杂度为 O(n3/2),要好于直接排序的 O(n2)。需要注意的是,增量序列的最后一个增量值必须等于 1 才行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。

不管怎么说,希尔排序算法的发明,使得我们终于突破了慢速排序的时代(超越了时间复杂度为O(n2)),之后,出现了相应的更为高效的排序算法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值