插入排序:希尔排序

    我们知道当一个序列基本有序时,直接插入会变得很高效。因为此时只需少量的移动元素,操作集中在元素的比较上。基于这种想法,我们就试图把一个序列在进行直接插入前调整得尽量有序。这就是希尔排序(Shell Sort)的核心思路。(Shell只是算法发明者的名字,无特殊含义)

那到底该怎么做呢?

    希尔排序一反以前的做法,插入为何只在相邻的元素之间进行,不相邻的同样可以进行。于是,希尔排序也被形象地称为”跳着插“。

那应该隔几个元素进行插入呢?

    这就说到希尔排序的另一个名字:缩小增量排序(Diminishing Increment Sort)。实际上这个增量序列或者说步长序列是可以提前指定的。不同的序列可以极大地影响到算法效率,至于最好的步长序列貌似还在研究。不过可以肯定的是最后的一个步长一定是1。因为最后一次是直接插入。

先来看一种最简单、也最常用的步长序列:n/2、n/2/2、... 1 (n是待排序的元素个数)。也是就说,初始步长是n/2,以后每次减半,直到步长为1。

利用这种步长序列,举一个例子:开始序列中加下划线的字体表示每一趟待排序的数字。

原序列: 21  12  4   9   9   5   78  1    (n=8)

下标:   0   1   2   3   4   5   6   7

第一趟:步长step=4,0、4号元素直接插入排序

开始    21  12  4   9   9   5   78  1

结束    9   12  4   9   21  5   78  1

第二趟:步长step=2, 0、2、4、6号元素直接插入排序

开始    9   12  4   9   21  5   78  1

结束    4   12  9   9   21  5   78  1

第三趟:步长step=1,0、1、2、3、4、5、6、7、8号元素直接插入排序(显然这是整体直接插入排序)

开始    4   12  9  9   21  5   78  1

结束    1    4   5  9   9   12  21  78 

如何理解每一趟排序:

  1. 步长序列的长度决定了排序的趟数。
  2. 每一趟排序,并不是所有元素都参与。参与排序的只能是下标为 0、step、2*step...的元素。
  3. 插入排序时,采用何种策略。如直接插入、交换插入、折半插入表插入表折半插入,这可任意选择。
  4. 最后一趟排序的步长一定是1。由第二点可知,最后一趟,全体元素都参与排序。
排序结果中红色9出现在了黑色9的前面,表明希尔排序是不稳定的。

仔细观察上述过程,一定可以理解上述四点。理解之后,就不难写出代码了。

下面给出这种排序的代码:

void ShellSort1(int a[], int n)   //希尔排序 
{
	if(a && n>1)
	{
		int i,j,step;
		for(step = n/2; step; step >>= 2)
		{
			for(i = step; i < n; i += step)
			for(j = i; j; j -= step)
			if(a[j] < a[j - step])
			swap(a[j], a[j - step]);
		}
	}
}
若是,看过插入排序:交换排序,相信很容易看懂上述代码

若是指定步长,代码是这样的:

void ShellSort2(int a[], int n, int step[], int nn)
{
	if(a && n>1 && step && nn>1)
	{
		int i,j,k;
		for(k = 0; k < nn; k++)
		{
			for(i = step[k]; i < n; i += step[k])
			for(j = i; j; j -= step[k])
			if(a[j] < a[j - step[k]])
			swap(a[j], a[j - step[k]]);
		}
	}
}

其中step数组代表步长序列,这里的代码与上一个相比,只是用step[i]替换了step,其它并无变化。


测试走起……

插入排序小结:

目前关于插入排序的文章写了5篇,具体实现涉及到了7种方法。于是,特地来总结一下:

一、什么是插入排序?

把一个元素插入到一个已经有序的序列中去,就叫插入排序。

二、插入排序的复杂度(时间和空间)?

这个得看具体的插入策略。比如最简单的直接插入。

空间复杂度 O(1)(只需一个临时变量)

时间复杂度

在最差的情况下,此时元素处于逆序。对于下标为i的元素,把它保存在临时变量中需移动一次,回填移动一次,由于逆序,前面的i个元素都得往后移动一次,共i次,显然也需比较i次。

故共需移动 求和i+2(i=1...n-1)=(n-1)(n+4)/2

共需比较 求和i(i=1...n-1)=n(n-1)/2

以上两者相加得 (n-1)(n+2)=O(n^2)

在最好的情况下,此时元素处于顺序。对于下标为i的元素,把它保存在临时变量中需移动一次,回填移动一次,由于正序,前面的i个元素不需移动,且只需比较一次。

故共需移动 2(n-1)

共需比较 n-1

以上两者相加得 3(n-1)=O(n)

若是计算平均时间复杂度,数学推理比较复杂,结论是 O(n^n)。

其它几种插入策略,其时间复杂度更加难以推理,不过总体上都是O(n^n)的。快一点的如二路插入、希尔排序(n(logn)^2),量级上不会改变。

提高插入排序的效率,可以从两个方面入手:减少比较次数、减少移动次数。

其中 二路插入是减少了移动次数,折半插入是减少了比较次数,表插入是用指针变化代替元素移动(因为指针变化代价比较低)。


转载请注明出处,本文地址:http://blog.csdn.net/zhangxiangdavaid/article/details/29222095

若是对你有所帮助,顶一个哦!


专栏目录看这里:



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值