在上一篇博文中我们提到:要令排序算法的时间复杂度低于O(n2),必须令算法执行“远距离的元素交换”,使得平均每次交换减少不止1逆序数。

  而希尔排序就是“简单地”将这个道理应用到了插入排序中,将插入排序小小的升级了一下。那么,希尔排序是怎么将这个道理应用于插入排序的呢?我们先来回顾一下插入排序的代码:

void InsertionSort(int *a, unsigned int size)
{//StartPos表示执行插入操作的元素开始插入时的下标
//令StartPos从1递增至size-1,对于每个a[StartPos],我们执行向前插入的操作
for (int StartPos = ;StartPos < size;++StartPos)
for (int CurPos = StartPos;CurPos != ;--CurPos)
if(a[CurPos - ] > a[CurPos])
swap(&a[CurPos],&a[CurPos-]); //令当前元素与前一元素交换
}

  不难看出,在插入排序中,对于每一个元素,我们都令其执行“向前插入”操作,直至到达顺序位置。但是,在“向前插入”这个操作中,每一次“当前元素”都是与前一元素进行比较,而这也是插入排序时间复杂度没能低于O(n2)的原因。

  所以,希尔排序与插入排序之间的区别就是:希尔排序在“向前插入”时,“当前元素”总是与前k元素(若当前元素下标为n,则前k元素即下标为n-k的元素)进行比较,并且第一个开始“插队”的元素不再是[1],而是[k]。从代码角度来说,便是将插入排序的循环改为:

   //StartPos表示执行插入操作的元素开始插入时的下标
//令StartPos从k递增至size-1,对于每个a[StartPos],我们执行向前插入的操作
for (int StartPos = k;StartPos < size;++StartPos)
for (int CurPos = StartPos;CurPos >= k;CurPos-=k)
if(a[CurPos - k] > a[CurPos])
swap(&a[CurPos],&a[CurPos-k]); //令当前元素与前k元素交换

  不难看出,插入排序就是k=1的情况。经过上述代码处理后,数据可以保证如下属性:

  a[n],a[n+k],a[n+2k]……a[n+x*k]有序,其中0=<n<size且n+x*k<size,也就是说:所有相隔距离为k的元素组成的数列都有序(当k为1时即全体有序)

  举个实例来看看,假设数组如下,间距为3的元素用同色标注:

  ,,,,,,,,,,,,

  令k=3,进行k=3的“插入排序”后,间距为3的元素互相有序:

  ,,,,,,,,,,,,

  分析上例可以看出,当k>1时,间距为k的k-插入排序的交换可以实现“远距离交换元素”,上例中,3-的插入排序交换了5次元素,逆序数减少了9,平均一次交换减少了1.8逆序数。

  同时可以看出,上述属性,只有在k为1时才能保证整个数组有序,也即普通插入排序的情况,而k>1时则不能。也就是说,要想“远距离交换元素”,就要令k>1,而k>1却又不能保证数组最后有序,那该怎么办呢?

  万幸的是,我们有这么一个定理:

  若数组已经进行过间距为k的k-插入排序,即已经确定间距为k的元素互相有序,则对数组进行间距为(k-1)的(k-1)-插入排序后,数组依然保持“间距为k的元素互相有序”

  用大白话来说,就是:虽然k>1的k-插入排序不能保证数组完全有序,但可以保证不增加数组的逆序数。

  于是,希尔排序的发明者唐纳德·希尔想出了这么一个办法,也就是希尔排序:先进行k比较大的“插入排序”,然后逐步减小k的值,直至k=1。这样一来,希尔排序就能保证最后数组有序。

  接下来的问题就是,k的初始值该如何选?k又该如何减小至1?这一点至关重要,其重要性类似于哈希函数对于哈希表的意义。我们称k从初始值kn减小至1的各值:kn,kn-1,kn-2……1组成的序列称为“增量序列”,即“增量”(Increment,意指k的大小)组成的序列。希尔本人推荐的增量序列是初始值为size/2,任一kn-1=kn/2。这样一来,使用希尔增量序列的希尔排序完整算法如下:

void ShellSort(int *a, unsigned int size)
{
unsigned int CurPos, Increment; //CurPos表示执行插入的元素当下所处的下标,Increment即增量k
int temp; //用于暂存当前执行插入操作的元素值,可以减少交换操作 //Increment从size/2开始,按Increment/=Increment的方式递减至1
for (Increment = size / ;Increment > ;Increment /= )
//下方代码与插入排序几乎相同,只是比较对象由[CurPos-1]变为[CurPos-Increment]
for (unsigned int StartPos = Increment;StartPos < size;++StartPos)
{
temp = a[StartPos];
for (CurPos = StartPos;CurPos >= Increment&&a[CurPos - Increment] > temp;CurPos -= Increment)
a[CurPos] = a[CurPos - Increment];
a[CurPos] = temp;
}
}

  接下来,我们以希尔增量序列为例,说明为什么增量序列的设定对于希尔排序性能至关重要:

  设数据为:1,9,2,10,3,11,4,12,则对应增量序列为4,2,1

  4-插入排序后:1,9,2,10,3,11,4,12

  2-插入排序后:1,9,2,10,3,11,4,12

  1-插入排序后:1,2,3,4,9,10,11,12

  不难发现,这个例子中的增量序列很不好,4-排序和2-排序都没有任何的有效操作。这个例子告诉我们两件事:

  1.增量序列对于希尔排序的性能非常重要,差的增量序列会减少需要本可以执行的“远距离交换”

  2.希尔推荐的增量序列编程实现简单,但实际应用中表现并不好,原因在于其增量序列不互素。

  并且可以确定的是,若需排序的数组a大小n为2的幂,任一x为偶数的a[x]均大于x为奇数的a[x],且a[x]>a[x-2],则希尔的增量序列只有在进行1-排序时才有交换操作。

  举例来说:9,1,10,2,11,3,12,4,13,5,14,6,15,7,16,8。

  其增量序列为8,4,2,1,但是8-排序、4-排序与2-排序都没有交换元素。

  此外,若某元素排序前位于下标奇数处,排序后所在位置为i,则进行1-排序前,其位置在2*i+1处(如例中元素4,其下标为奇数,其有序位置应为3,1-排序前位置为7),而将其从位置2*i+1移动至i需要执行i+1次交换,这样的元素(下标奇数)共有n/2个,所以将这些元素移动至正确位置就需要(0+1)+(1+1)+(2+1)+……+(N/2+1)共N2/8-N/4,时间复杂度为O(n2)。可见,使用希尔增量序列希尔排序的最坏情况是O(n2)

  那么,希尔排序的增量序列该如何选择呢?本文给出两个序列,它们都比希尔增量序列要好:

  1.Hibbard序列:{1,3,7……2k-1},k为大于0的自然数,使用Hibbard序列的希尔排序平均运行时间为θ(n5/4),最坏情形为O(n3/2)。

  2.Sedgewick序列:令i为自然数,将9*4-9*2+1的所有结果与4-3*2+1的所有结果进行并集运算,所得数列{1,5,19,41,109……}。使用此序列的希尔排序最坏情形为O(n4/3),平均情形为O(n7/6)

  如何实现这两个序列的希尔排序并不是难事,Hibbard序列可以直接通过计算得出初始值(小于数组大小即可),而后每次令Increment=(Increment-1)/2即可。Sedgewick序列则稍稍麻烦点,需要先将序列计算出足够项(最后一项小于数组大小),而后存于某个数组,再不断从中取出元素作为增量。

  希尔排序的性能(使用Sedgewick序列)在数据量较大时依然是不错的。如果说插入排序是我们的“初级排序”,用于较少数据或趋于有序数据的情况,那么希尔排序就是我们的“中级排序”,用于数据量偏多的情况。当然,当数据量极大时,我们将用上我们的“高级排序”——快速排序。至于怎么样算数据量偏多,这个就需要因情境而异了,数据的存储形式等都是需要考虑的问题,一般来说数据量为万级时我们使用希尔排序,数据量为十万、百万级时使用快速排序,而数据量为百、千级时插入排序和希尔排序都可以考虑。并且需要再次说明的是,数据越趋于有序,则插入排序越快。从这个角度来说,插入排序也不失为一个“高级排序”。

  那么,我们学习堆时提到的用堆进行排序的想法,明明有着很好的时间界O(N*logN),为什么不在考虑之列呢?我们下一篇博文就简单地分析分析。

  

深入浅出数据结构C语言版(17)——希尔排序的更多相关文章

  1. 深入浅出数据结构C语言版(17)——有关排序算法的分析

    这一篇博文我们将讨论一些与排序算法有关的定理,这些定理将解释插入排序博文中提出的疑问(为什么冒泡排序与插入排序总是执行同样数量的交换操作,而选择排序不一定),同时为讲述高级排序算法做铺垫(高级排序为什 ...

  2. 深入浅出数据结构C语言版(19)——堆排序

    在介绍优先队列的博文中,我们提到了数据结构二叉堆,并且说明了二叉堆的一个特殊用途--排序,同时给出了其时间复杂度O(N*logN).这个时间界是目前我们看到最好的(使用Sedgewick序列的希尔排序 ...

  3. 深入浅出数据结构C语言版(22)——排序决策树与桶式排序

    在(17)中我们对排序算法进行了简单的分析,并得出了两个结论: 1.只进行相邻元素交换的排序算法时间复杂度为O(N2) 2.要想时间复杂度低于O(N2),算法必须进行远距离的元素交换 而今天,我们将对 ...

  4. 深入浅出数据结构C语言版(21)——合并排序

    在讲解合并排序之前,我们先来想一想这样一个问题如何解决: 有两个数组A和B,它们都已各自按照从小到大的顺序排好了数据,现在我们要把它们合并为一个数组C,且要求C也是按从小到大的顺序排好,请问该怎么做? ...

  5. 深入浅出数据结构C语言版(5)——链表的操作

    上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游 ...

  6. 深入浅出数据结构C语言版(1)——什么是数据结构及算法

    在很多数据结构相关的书籍,尤其是中文书籍中,常常把数据结构与算法"混合"起来讲,导致很多人初学时对于"数据结构"这个词的意思把握不准,从而降低了学习兴趣和学习信 ...

  7. 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

    在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...

  8. 深入浅出数据结构C语言版(12)——从二分查找到二叉树

    在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不 ...

  9. 深入浅出数据结构C语言版(15)——优先队列(堆)

    在普通队列中,元素出队的顺序是由元素入队时间决定的,也就是谁先入队,谁先出队.但是有时候我们希望有这样的一个队列:谁先入队不重要,重要的是谁的"优先级高",优先级越高越先出队.这样 ...

随机推荐

  1. win7(iis7)localhost可以访问127.0.0.1不可以访问的问题解决办法

    在C:\Windows\System32\drivers\etc文件夹下有hosts文件,用编辑器打开,看看有没有配置127.0.0.1 localhost 把前面的#去掉保存即可!

  2. markdownpad生成目录

    生成目录 document.addEventListener("DOMContentLoaded", function() { // 生成目录列表 var outline = do ...

  3. SQL 创建存储过程,让主键自增

    1.  首先创建存储过程: 2.  然后分别创建序列,生成基金公司编号.基金代码.活期账号.理财账号.基金账户.合同号.要求如下: 基金公司编号,字母K+5位数字. 基金代码,字母V+6位数字. 活期 ...

  4. Win7怎样禁用自带IE浏览器

    Win7怎样禁用自带IE浏览器 ------------ 1.单击开始按钮,从弹出的菜单中,选择控制面板. 2.在控制面板界面,单击程序和功能 3.在程序界面中单击选择“打开或关闭Windows功能” ...

  5. JavaScript面向对象(一)——JS OOP基础与JS 中This指向详解

      前  言 JRedu 学过程序语言的都知道,我们的程序语言进化是从"面向机器".到"面向过程".再到"面向对象"一步步的发展而来.类似于 ...

  6. 【Linux】修改Linux字符集

    一.查看字符集 常见的几种方法: (1) [root@devhxyw03 ~]# echo $LANG zh_CN.GBK (2) [root@devhxyw03 ~]# env | grep LAN ...

  7. java web开发时的绝对路径与相对路径

    相对路径 不以/开头的路径为相对路径,使用相对路径时的路径为当前访问的文件的父目录,即如果现在访问文件的路径为http://localhost:8080/项目名/目录/文件,那么使用相对路径时路径前缀 ...

  8. 一次FCK拿bc全过程

    和大家简单的弄下fckeditor 漏洞在红客我看到好多人对fck 这个漏洞很干兴趣 其实这个漏洞这的很老了 也非常好利用  我也扫了一点fck的漏洞网址  下面我们就来打开一个我们看看这个一号站平台 ...

  9. 如何通过jmeter使用beanshell进行关联

    关联,大多数都是通过响应的信息抓取部分信息,例如session或者hidden等 在jmeter中要使用关联,分为以下2步: Step 1. 在Sampler请求下添加正则表达式,获得信息,添加 &g ...

  10. 2017最新的Python教程分享

    Python在数据科学盛行的今天,其易于阅读和编写的特点,越来越受编程者追捧.在IEEE发布的2017年编程语言排行榜中,Python也高居首位.如果你有学Python的计划,快来看看小编分享的Pyt ...