@希尔排序(Shell Sort)是插入排序的一种更高效的改进版本。
对于插入排序算法来说,如果原来的数据就是有序的,那么数据就不需要移动,而插入排序算法的效率主要消耗在数据的移动中。***因此可知:如果数据的本身就是有序的或者本身基本有序,那么效率就会得到提高。
在前面介绍的插入排序,我们知道1.它对有序数列排序的效率是非常高的 2.要排序的数向前移动是一步步进行的导致插入排序效率低。
希尔排序正是利用第一点,改善第二点,达到更理想的效果。
希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
前一节讲的直接插入排序,应该说,它的效率在某些时候是很高的,比如,我们的记录本身就是基本有序的,我们只需要少量的插入操作,就可以完成整个记录集的排序工作,此时直接插入很高效。还有就是记录数比较少时,直接插入的优势也比较明显。可问题在于,两个条件本身就过于苛刻,现实中记录少后者基本有序都与特殊情况。针对插入排序有时候(对于随机数列,效率一般,交换的频率高。)效率不高的问题,科学家希尔研究出一种排序方法,对直接插入排序改进后可以增加效率。
1,希尔排序的基本思想是:将需要排序的序列划分成为若干个较小的子序列(根据增量划分小序列,为什么用增量划分小序列?看下边第一条中的总结),对子序列进行插入排序,通过插入排序能够使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
(1),比如有个序列是{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},此时,这个序列还是杂乱无序,谈不上基本排序,要排序还是重来一遍直接插入排序,这样做有用吗?没用。这里我们对数组划分成3个小序列,是按数组的长度,平均局部划分的,结果排序后,仍然是杂乱无序(只能说是局部有序了。)。这时候怎么办?
用跳跃分割策略(即使用增量划分小序列),将相距某个"增量"的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。(注意是基本有序,而不是局部有序。)
---需要强调一下,所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间,像{2,1,3,6,4,7,5,8,9}这样可以成为基本有序了。
(2),希尔排序的关键并不是随便分组后各自排序,而是将相隔某个"增量"的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。
---这里增量的选取就非常关键了。我们在代码中,用d=d/3+1;的方式选取增量的,可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。
(3),下边看一个希尔排序思想的实例图:
数组元素为:25,19,6,58,34,10,7,98,160,0
--增量5,即索引为0,5,10,15....n,或者是1,6,11,16...n,索引号为这样的都是同一个序列(分割后的序列)。
---增量2,即索引为0,2,4,6,8...n,为分割后的同一序列。
上图是第一次排序的结果(增量为5的时候的排序结果),本次选择增量为 d=2。 本次排序的结果如下图:
当d=1 是进行最后一次排序,本次排序相当于冒泡排序的某一次循环。最终结果如下:
上述是希尔排序的思想。其实希尔排序只是插入排序的一种优化。
2,下边是实现代码:
(1),C#语言实现:
int[] data={25,19,6,58,34,10,7,98,160,0};
//int inc;
//for (inc = 1; inc <= data.Length / 9; inc = 3 * inc + 1) ;
int inc=data.Length/3+1; //1,初始化增量,增量的选择非常重要,这里返回4
for (; inc > 0; inc /= 3) //最外围for循环,如果增量大于0,循环递减增量。
{
//这里第一次增量inc=4
//第二次增量inc=1
//第三次增量inc=0,0>0返回false,跳出循环。
//2,其次,确定增量后(增量为4),开始for循环比较--->(即子序列)增量序列(增量相同元素组成的序列)。
for (int i = inc + 1; i <= data.Length; i += inc)
{ //第一次inc=4,所以i=5
//注意,这里temp就像一个不变的指针,就像一个哨兵,和直接插入排序一样,前边的值,都和这个值比较,如果前边的值都大于这个值,值都后移。(对增量序列排序和直接插入排序是一样的。)
int temp = data[i - 1];//这里temp是第5个元素,索引0是第一个元素,增量4,即索引4(第5个元素) ,这里是将第5个元素,存储在临时变量temp中
int j = i; //这里要有个学习指针时的思想,不要修改原指针,设置个临时变量,修改它,j就是这个作用
//内层while循环比较,作用,值后移
while ((j > inc) && (data[j - inc - 1] > temp)) //这里j必须大于inc,才能比较1次,所以这个条件必须成立,才能循环下去,否则就会超出索引。
{ //data[0]>data[4],表示升序,反之,降序。
data[j - 1] = data[j - inc - 1];//交换数据,将大的数赋值给后边的数。
j -= inc; //这里j就像指针,指向数组中的元素,减一次增量,就往前跳一次,跳到前边一个元素上。
}
data[j - 1] = temp; //结合,插入排序记忆,值后移,然后前边的值设置为temp,就等于交换数据。
}
/*大话数据结构中,上边for循环是放在下边代码中的
do{}
while(inc>1);
*/
/*
@循环比较增量序列(这里升序)
1,元素1和元素0比较(例如9,3。那么排序后,就变成了3,9)
2,元素2和元素1比较(3,9,9),元素2和元素0比较(3,3,9)。(例如3,9,1,排序后变成1,3,9)
3,元素n和元素n-1比较,元素n和元素n-2比较...
*/
}
foreach(var i in data)
{
Console.WriteLine(i);
}
Console.ReadKey();
(2),Python语言实现:
data=[25, 19, 6, 58, 34, 10, 7, 98, 160, 0]
inc=len(data)/3+1
while(inc>0): #排序循环。注意,Python中for循环不能进行判断,这里只能用while代替。
for i in range(inc+1,len(data)+1,inc):
temp=data[i-1]
j=i
while((j>inc) and (data[j-inc-1]<temp)):
data[j-1]=data[j-inc-1]
j=j-inc
data[j-1]=temp
inc=inc/3
@总结:
1,重点(原理先行思想,不懂原理,光看代码,不行。):了解一个算法,还是要首先了解原理。怎么排序?怎么分割序列?等等。
2,联系直接插入排序一块学习,哨兵思想,以及值后移思想(包括值的还原)。以及指针指和索引关系思想(即数组索引)。
3,数学归纳法进一步学习,直接用数据是最好的实践和证明,也最容易理解。(代码成立以后,可以代入大量数据进行测试,看排序效果。)
4,联系前边排序算法一块记忆,值的交换方法。
浙公网安备 33010602011771号