先记住一句话:希尔排序的核心形式——希尔排序是插入排序的改良版
一、插入排序
1、要理解理解希尔排序是如何实现的,先要明白插入排序的过程
插入排序进行的过程中会构建已排序和未排序两部分子数组,每次从未排序的数组中选取一个数将它放到已排序数组中的正确位置。当未排序数组中的数字选完了以后,那么所有元素的位置就确定了,排序也就进行完成了。
既然是排序那么循环肯定必不可少,假设有一组无序的数 11,2,13,4,56,7,9 ,先选择一个数作为未排序数组部分,按照循环的位置变化,就选第一个位置的元素11,由它先行构成一个未排序数组,只是比较特殊只有一个元素。完成后他就是已排序数组部分,然后在之后的数中选一个数,选择2,确定2在已排序数组中的位置,显然此时需要比较2和已排序数组中的元素值大小以确定其位置,完成后,以排序数组就是2,11 ,然后继续选择下一个元素13确定他在已排序数组中的位置,那么需要继续比较。
重点来了,怎么比呢?
2、插入排序比较过程
通常的做法是拿13依次和2,和11作比较,13>2,然后比较13>11,好,13比有此时有序部分的元素都大,那么它的位置可以不变,此时构成2,11,13有序数组部分,然后选择下一个4和有序数组中的每一个比较,2<4<11,那么4应在2和11的中间位置,那么要把4插入到2和11之间。我们知道数组中插入元素牵涉到元素的位置移动,用一个for循环就可以搞定——这是一种比较直接的方式。那么另外一种,不选择插入元素的形式,采用相邻位置比较交换的形式,2,11 数组和4, 用4和11比较,11>4,交换形成2,4,11,然后继续比较,2<4,前一个比后一个小,那么不交换,4的位置确定;接着2,4,11和56比较,11<56,不用交换,56位置确定。可以类推整个有数数组变化过程如下
原数组:11,2,13,4,56,7,9
11 >2,交换
2,11,13,4,56,7,9
11<13,13位置确定
2,11,13,4,56,7,9
13>4,交换构成2,11,4,13 11>4交换,构成2,4,11,13 2<4,不交换,4位置确定
2,4,11,13,56,7,9
13<56,不交换56位置确定
2,4,11,13,56,7,9
56>7,交换->2,4,11,13,7,56 13>7,交换->2,4,11,7,13,56 11>7,交换->2,4,7,11,13,56, 4<7,交换,7位置确定
2,4,7,11,13,56,9
56>9, 交换->2,4,7,11,13,9,56 13>9,交换2,4,7,11,9,13,56 11>9交换->2,4,7,9,11,13,56, 7<9不交换,9位置确定
2,4,7,9,11,13,56
排序完成
3、插入排序算法实现:
C语言:
int nums[7]={11,2,13,4,56,7,9};
int i=0,j=0;
int len=sizeof(nums)/sizeof(int);
for(i=1;i<len;i++){
for(j=i;j-1>=0;j--){//拿无序数组中的值和有序数组,比较过程中相邻位置比较
if(nums[j-1]>nums[j]){//交换
nums[j-1]=nums[j-1]+nums[j];
nums[j]=nums[j-1]-nums[j];
nums[j-1]=nums[j-1]-nums[j];
}
else//位置确定,不交换
break;
}
}
好了,插入排序介绍完了;下面进入正题——希尔排序
在插入排序中,每次选取一个无序数组中的值和有序数组比较,确定其位置。而在希尔排序中,是先将数组按照一定的步长做分组,然后逐步将步长减小,每变化一次步长就做一次分组。在每次分组后得到的子数组做插入排序。
如:11,2,13,4,56,7,9 ,先按照步长为数组长度的一半做分组,然后每次步长都取前一次的一半且是在上一次排序完成后做的分组,最后一次步长为1
步长 分组
3 11,4,9 2,56 13,7(步长为3,共分3组)
1 4,9,11,2,56,13,7(步长为1)
需要分别对三组数排序
- 在这里需要代码实现,分析一下,按照循环的特点递增或者递减遍历数组的时候,可以根据元素的位置和步长的值确认某个元素是位于哪一个组中,如当循环走到0位置,0+3位置就是4,0+3+3位置就是9,循环走到1位置时,1+3位置就是56,1+3+3位置超出数组范围不计入循环之内,以此类推。我们可以得到一个结论:根据元素位置和步长的值,可以确定某个元素和其所在的全部组元素。
- 然后就是每一组之内实现插入排序算法, 每次从无序的数组中选择一个数字和有序的数组每个元素比较,最多比较的次数就是有序数组中元素的长度(这时候被比较元素排序后将位于有序数组的最前面一个位置)。也就是说每轮比较次数小于等于有序部分的数组长度。
- 一个无序数会引发一轮和有序数组的比较,就是说数组中的元素都会经历从无序——有序的过程,每一个元素都要经理一轮比较,理解这一点很重要。结合循环遍历和上述得到的第一个结论,我们可以通过遍历数组元素的时候用以步长为单位的相邻位置比较交换,也就是最一开始说的插入排序。每一轮完毕后步长逐渐递减,直到最后步长衰减为0停止比较。
- 算法实现如下
C语言
int nums[7]={11,2,13,4,56,7,9};
int i=0,j=0;
int len=sizeof(nums)/sizeof(int);
int gap=len/2;//步长先取数组长度的一半
while(gap>0)
{
for(i=gap;i<len;i++){//遍历数组,不同于直接插入排序的做法是从步长gap位置开始遍历,相当于插入排序一开始选取1位置和0位置比较,而希尔排序这里是选gap位置和其之前的位置元素比较
j=i;
while(j-gap>=0){//循环确定i位置元素所在分组的前向元素,超出范围的不计入
if(nums[j-gap] > nums[j]){//比较以gap为步长的相邻位置元素的大小,前项大于后项那么交换
nums[j-gap]=nums[j-gap]+nums[j];
nums[j]=nums[j-gap]-nums[j];
nums[j-gap]=nums[j-gap]-nums[j];
j-=gap;
}
else//前项小于或者等于后项,不加换,并退出当前轮次比较
break;
}
}
gap/=2;//步长衰减为原来的一半
}
个人感觉比较难理解的是,按照分析过程,应该是每次提取分组后的子组,然后在子组中做排序,但是这里采用循环遍历的方式,不是逐个的完成子组排序,而是把子组排序混合了,也就是如步长为3的时候,原数组为
11,2,13,4,56,7,9
从3位置就是元素4开始,和4所在分组中4之前的元素比较(就是11了),然后遍历外层遍历到56,它不属于4所在分组,不过想一下发现组和组之间的比较并不相互影响,56所在组的元素不牵涉到4所在分组,所以最外层循环控制的是一个在组和组之间跳转比较的过程。
当然了,gap为1的时候只有一组不涉及到什么跳转。
而每提取一个组中未排序元素都和已排序数组比较,循环控制条件采用了while(j-gap>=0)和j-=gap的组合,比较一次游标就在前向有序数组中递减gap,控制条件就是j-gap>=0,不超过数组之外,保证元素在所有的分组内。
可见,最后步长gap为1的时候,其实就是直接的插入排序嘛!那希尔排序又改良在哪里了?
个人感觉这个改良就应该是改良在了相较直接的插入排序,希尔排序在每衰减一次步长的时候,做元素比较时,已有部分元素经过上一次步长变换比较后是阶段性的有序了,插入排序一旦元素比较到某个位置满足了值的大小条件就不再当前轮次向下继续比较了,如果数组中有序的元素排列越多那么比较次数也就越少,希尔通过步长衰减,分组比较,避免了直接插入排序的部分重复“做功”。
以上就是个人对希尔排序的一些体会总结,难免粗陋了些,欢迎斧正!