一:基本思想
先选定一个整数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:
四:复杂度和稳定性
时间复杂度:
-
最坏情况时间复杂度:在某些特定的输入数据下,希尔排序的最坏情况时间复杂度可以达到O(n^2)。
-
平均情况时间复杂度:O(nlogn) ~ O(n^2)
-
最好情况时间复杂度:希尔排序可以达到O(n^1.3)的时间复杂度。
空间复杂度:
希尔排序的空间复杂度是O(1)。这是因为希尔排序是一种原地排序算法
稳定性:
希尔排序是不稳定的排序算法。
稳定性是指对于具有相同键值的元素,在排序后它们的相对顺序是否保持不变。如果一个排序算法能够保持具有相同键值的元素之间的相对顺序,则称该算法是稳定的。
在希尔排序中,因为涉及到跨间隔的元素比较和交换,这可能会导致具有相同键值的元素之间的原始顺序被改变。例如,考虑一个简单的希尔排序步骤,如果有一个间隔为h的序列,算法会同时比较和可能交换间隔为h的元素。这种比较和交换可能会把相同键值的元素交换到彼此的位置上,从而打破了它们的原始顺序。
因此,由于希尔排序不保证相同键值元素的相对顺序在排序过程中保持不变,它被分类为不稳定的排序算法。
五:代码展示
//希尔排序的第一种写法(双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 的那一次的分组排序执行完,就获得了一个有序的数组,这就是希尔排序。
希尔排序还有另一种写法:
//单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循环中改变自己大小,控制不住 !