一:基本思想
先选定一个整数gap,把待排序文件中所有记录分成个组,按照所有距离为整数gap的记录分在同一组内,并对每一组内的记录进行排序。然后,通过整数gap逐渐变小,重复上述分组和排序的工作。当整数gap变小到达=1时,也就是执行插入排序,这样所有记录在统一组内就排好序了。
所以一定得先理解:插入排序-CSDN博客
不然很难理解此博客
二:预排序的意义
**Q:**既然最后都要执行插入排序,那多一步预排序,不是会徒增运算了
**A:**预排序的意义在于让最后一步的插入排序的运算量大大的减小,比直接的单独的插入排序更优
三:预排序的核心
正如希尔排序的思想,我们需要一个整数gap,这个整数gap会逐渐变小,最终变小到1为止
一般是gap 初始化为N,也就是数组的元素个数,然后在循环中 gap= gap/2 ,这样每次进入循环这个gap就 /2,会逐渐的减小,最终为1(跟着除法原则,一个大于2的数不断的/2,一定会有一次为1)
例子:
解释:
1:
gap = N(10);
gap = gap/2 = 5;
根据思想可知:gap为5,即按照所有距离(下标差值)为5的记录分在同一组内,如图内的:
第一组:9 4
第二组:1 8
第三组:2 6
第四组:5 3
第五组:7 5
然后每组进行组内的排序:
第一组:4 9
第二组:1 8
第三组:2 6
第四组:3 5
第五组:5 7
也就是上图中的:
2:
gap = gap/2 = 2;
此时的gap变成2,所以:
第一组:4 2 5 8 5
第二组:1 3 9 6 7
然后进行每组的组内排序:
第一组:2 4 5 5 8
第二组:1 3 6 7 9
也就是上图中的:
3:
gap = gap/2 = 1;
gap为1:
只有一组:2 1 4 3 5 6 5 7 8 9
组内排序后:
1 2 3 4 5 5 6 7 8 9
也就是上图中的:
说白了gap为1 的时候,进行的就是一次插入排序,而且可以看的出来,最后一次插入排序之前 ,我们接收到的数组已经有了一定的顺序。
下面是博主找到的一个动态演示图:不过数据和上面的不一样,一样的也是gap =5 到 gap = 2再到最后的 gap =1:
四:代码展示
cpp
//希尔排序的第一种写法(双for)
void ShellSort(int* arr, int N)
{
//gap初识为N,元素的个数
int gap = N;
//gap不为1就要继续的缩小并排序
while (gap > 1)
{
//gap缩小
gap = gap / 2;
//这个for控制每组的元素
for (int j = 0; j < gap; j++)
{ //这个for控制每组内的排序
for (int i = j; i< N - gap; i += gap)
{
int end = i;
//即将排序的元素,保留在tmp
int tmp = arr[end + gap];
//end>=0代表还有元素未比较
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
//来到这里分为两种情况
//1:break->遇到比元素tmp小或和tmp相等的,将m放在它的后面
//2:全部比较完了,都没有遇到<=tmp的,最后tmp放在数组第一个位置
arr[end + gap] = tmp;
}
}
}
}
}
解释:
**1:**跟纯粹的插入排序相比,咱们多了一个控制每组的元素的for循环 ,以及多了一个while来确保gap最后为1执行完排序才终止
**2:**第二个for循环:
该for循环控制的是每组元素的内部排序,可以看作插入排序,不过元素是间隔的!i<N-gap和插入排序中的i<N-1意义一致,在插入排序中我们最后的end(i)要停留在倒数第二个元素是,下标为N-2,所以end才<N-1,才能取到N-2。所以们这里i<N-gap,也是为了确保end停留在倒数第二个元素上。
**3:**第一个for循环:
该for控制的是每组的元素,gap为5,数组被分成了5组,j会每次都赋给end,这样end的起始位置不同,也就进行的组的更换,再在第二个for中进行组内的排序:
如图所示:
gap为5的最终结果:
然后gap就会变小,进行新一轮的分组排序,最后gap =1 的那一次的分组排序执行完,就获得了一个有序的数组,这就是希尔排序。
希尔排序还有另一种写法:
cpp
//单for
void ShellSort(int* arr, int N)
{
//gap初识为N,元素的个数
int gap = N;
//gap不为1就要继续的缩小并排序
while (gap > 1)
{
//gap缩小
gap = gap / 2;
for (int i= 0; i< N - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
arr[end + gap] = tmp;
}
}
}
}
第一种写法是:分一组,排序一组,然后再去分一组,再排序
第二种写法是:多组并排,也就是直接对数组选择性的进行排序
如图:
end =1 就进行①的两个元素的排序,以此类推
最后得到:
'
然后再进行gap的缩小,进行新一轮的排序
个人觉得双for循环的写法更加的易懂
五:效果测试
六:与插入函数的比较
相信很多人都并觉得希尔比插入好,下面进行比较运算的时间(单位ms)来展示希尔的强大:
测一万个随机数,插入排序花了6ms,希尔花了1ms
测十万个随机数,插入排序花了0.6秒,希尔花了9ms
测一百万个随机数,插入排序花了63秒,希尔花了0.1秒,我就问你屌不屌??
七:一些容易出错的细节
**1:**gap = gap/2,对于N比较大的时候不太够看,建议gap = gap/3+1,+1是为了确保gap可以为1
**2:**千万不要觉得end 的值 要经过 i 的复制,感觉太过麻烦,直接把end写在for循环那里,这样会造成 end在for循环中改变自己大小,控制不住 !