上一篇文章我们一起学习了直接插入排序,它的原理就是把前i个长度的序列变成有序序列,然后循环迭代,直至整个序列都变为有序的.但是说来说去它还是一个时间复杂度为(n^2)的算法,难道就不能再进一步把时间复杂度降低一阶么?可能有很多同学说快速排序,堆排序,我都会,这些简单的插入排序我都不屑于用.确实,以上几种算法相对于之前的O(n^2)级别的算法真的是弱爆了,效率可能还会差上千万倍,但是我们不妨翻看一下历史,你就会感觉每一种算法的出现都是很可贵的.
在1959年D.L.Shell正式提出了我们今天的主角shell算法,这是相当酷的一件事情,为什么这么说呢?因为shell排序时第一个突破了O(n^2)时间复杂度的排序算法,这应该是排序算法历史上比较闪耀的时刻了,因为在1959年之前的相当长的一段时间里,各种各样的排序算法无论是怎么花样繁多,都始终无法突破O(n^2)雷池一步,在当时直接插入排序已经是相当优秀的了,而排序算法不可能突破O(n^2)的声音成为了当时的主流.
看见了这段历史之后你有什么感受呢,我们在课堂上不愿意学的算法确仍然是科学家们多年苦苦思索才发明出来的,是不是觉得他们很不容易呢?其实你也没必要内疚啦,即使你曾经对它不屑一顾过,没有认真的学它,So what?现在开始学也不晚是不是?
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
其实直接插入排序并不是那么逊的,它在待排序数据基本有序并且数量较少的时候威力还是很大的,甚至比一些高级算法还要高效.对于第二点,我只能说这就是我们shell算法的牛逼的地方了,插入排序每次只能移动数据一位,而shell算法成功的解决了这个问题.
shell算法的核心还是分组,但是这个分组就有门道儿了,因为它会实现取一个小于总数据长度的整数值gap作为分组的步长,什么意思呢?假如我们的待排序数组为:
序号 1 2 3 4 5 6 7 8 9 10
49,38,65,97,76,13,27,49,55,04
设置gap的值为长度10的一半也就是5,那么第一个和第六个元素就是一组,第二个和第七个元素就是一组,第三个和第八个元素就是一组,第四个和第九个元素就是一组,第五个和第十个元素就是一组,所以一共分为了gap = 5组,
组 一 二 三 四 五
序号 1 6 2 7 3 8 4 9 5 10
数据 49 13 38 27 65 49 97 55 76 04
交换后 13 49 27 38 49 65 55 97 04 76
然后如上面每一组之间进行再直接插入排序,比较如果前一个元素比较大,则交换两个元素的位置,直至5组全部交换完毕.此时数组的顺序为
13 27 49 55 04 49 38 65 97 26.
然后gap的值再减半为2,重新分组,也就是第一个 第三个 第五个 第七个 第九个 元素为第一组是13 49 4 38 97, 第二个 第四个 第六个 第八个 第十个元素为一组是27 55 49 65 26.
组 一 二
序号 1 3 5 7 9 2 4 6 8 10
数据 13 49 04 38 97 27 55 49 65 26
交换后 04 13 38 49 97 26 27 49 55 65
然后如上面对它们两个组分别进行直接插入排序,得到结果为
4 26 13 27 38 49 49 55 97 65,
之后gap的值再减半为1(要知道gap的值小于1的时候在分组就没意义了,一位你的每一个组至少要有一个元素才能组成一个序列.)这次我们直接对上一次的结果进行一次真正的直接插入排序(为什么说是真正的呢,因为此时步长已经为1)直至得出最终结果:
4 13 26 27 38 49 49 55 65 97.
下面是shell算法的实现代码:
void shell_sort(int array[], int length){
int i;
int j;
int k;
int gap; //gap是分组的步长
int temp; //希尔排序是在直接插入排序的基础上实现的,所以仍然需要哨兵
for(gap=length/2; gap>0; gap=gap/2){
for(i=0; i<gap; i++){
for(j=i+gap; j<length; j=j+gap){ //单独一次的插入排序
if(array[j] < array[j - gap]){
temp = array[j]; //哨兵
k = j - gap;
while(k>=0 && array[k]>temp){
array[k + gap] = array[k];
k = k - gap;
}
array[k + gap] = temp;
}
}
}
}
}
这是完全按照希尔算法的思想写的,并没有做任何更改.但还是有几点要说一下
- 希尔排序的时间复杂度为O(n*logn).
- 我们怎么确定步长才能使算法达到最高效呢?其实这是一个很严谨的数学证明问题,可惜的是我们的科学家们目前为止并没有寻找到一个唯一的答案,但是根据维基百科的介绍,还是有几种比较高效的步长的,大家如果感兴趣的话可以到这个链接看一下:希尔排序-维基百科.
- 希尔排序是不稳定的,可以通过我们上面的两个相同的49就可以看得出来.(其中一个49已经被下划线标记了下来)
我在博客上看到过有些大神为了追求代码的”简洁之美”,尽量使代码保持最短,(强调一下,如果是为了让算法执行的效率更高的话,我们当然要膜拜)但是这样的话可能就不那么好理解了,会给他人造成困扰,因为并不是每一个人都是对这个算法相当熟稔,新手看了你的代码会很气馁,对自信心也是一种伤害代码毕竟还是要给人看的,我们要不为难别人,这样即节省别人的时间也会节省自己的时间,何乐而不为呢,毕竟我们中的绝大多数人还要在一个团队里混饭吃.一家之言,可能是我层次还比较低,错了就错了,努力学习总是没错的,谢谢大家.