手撕排序算法系列之:希尔排序。

从本篇文章开始,我会介绍并分析常见的几种排序,大致包括插入排序,冒泡排序,希尔排序,选择排序,堆排序,快速排序,归并排序等。


 本篇主要来手撕希尔排序~~ 

目录

1.常见的排序算法

2.希尔排序

3.希尔排序实现代码

4.希尔排序测试

5.希尔排序的时间复杂度

6.希尔排序和直接插入排序效率测试

7.希尔排序特性总结​


1.常见的排序算法

1.1 插入排序基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

实际中我们玩扑克牌时,就用了插入排序的思想

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序_02[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_03[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_04

2.希尔排序

2.1希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成个组,所有距离相差gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取gap = gap/3+1重复上述分组和排序的工作。当gap到达=1时,所有记录在统一组内排好序

画图分析: 

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序_05[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_06

如上图所示,如果我们要默认升序排序:

2.1.1预排序阶段

1、在第一趟的时候,我们取gap = 5,此时9和4,1和8,2和6,5和3,7和5分到了一组,然后我们在每组之内进行比较,如果前面的数字大于后面的数字,就交换,此时每组中较小的数字就被交换到前面,较大的数字没交换到了后面。

2、在第二趟的时候,我们取gap = gap/3+1(除以三是一个普遍的写法,这里的重点是为什么要+1,这是因为如果gap小于3的情况下,gap/3会让gap=0,此时就会陷入死循环),这时gap= 2,4和2,5,8,5分到了一组,1,3,9,6,7分到了一组。此时,在一组内,进行排序,这一组就有序。

2.1.2插入排序阶段

3、待gap = 1得时候,我们就是用插入排序,这时候经过之前的预排阶段,整个数组已经变得大致有序了,较大的数字都会被移到后面,较小的数字都会移到前面,此时是用插入排序,效率就会很高。

2.2单趟希尔排序

2.2.1思路分析

单趟希尔排序就是将差为gap的数字分为一组,较大的放在后面,较小的放在前面

for (int i = 0; i < n - gap; ++i)//++i  可以直接让多组同时进行交换(只是少写个循环但是不提高任何效率)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if(tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_07

 [ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_08[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_09


具体插入排序的思想大家可以参考博文:​​[ 数据结构 -- 手撕算法第一篇 ]插入排序​


3.希尔排序实现代码

在单趟排序的基础上,加上一个gap循环即可

void ShellSort(int* a, int n)
{
//1.gap > 1 预排序
//2.gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//后面 +1 保证当gap小于3时能让gap == 1
for (int i = 0; i < n - gap; ++i)//++i 可以直接让多组同时进行交换(只是少写个循环但是不提高任何效率)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if(tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}

4.希尔排序测试

void ShellSort(int* a, int n)
{
//1.gap > 1 预排序
//2.gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//后面 +1 保证当gap小于3时能让gap == 1
for (int i = 0; i < n - gap; ++i)//++i 可以直接让多组同时进行交换(只是少写个循环但是不提高任何效率)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if(tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}

//希尔排序
void TestShellSort()
{
int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
ShellSort(a, sizeof(a) / sizeof(int));
PrintArray(a, sizeof(a) / sizeof(int));
}

int main()
{
//希尔排序
TestShellSort();

return 0;
}

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_10

测试结果:

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_11[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_12

5.希尔排序的时间复杂度

希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定。

但是我们可以将希尔排序的时间复杂度相比较直接插入排序进行分析,在最坏情况下野就是逆序排顺序,假设有N个数字。直接插入排序的时间复杂度是O(n),但是如果使用希尔排序此时进行分组时候每个组组进行排序,在最后gap=1时时间复杂度可以认为是O(n),所以结合起来希尔排序的时间复杂度是肯定小于O(n^2)。


但是希尔排序在一种情况下也是存在缺点的,这种情况下就是原数组本身有序,那直接插入排序的时间复杂度是O(N),而使用希尔排序在gap>1的时候进行预排序其实是没有作用的,但是计算机是不知道的,还是会做一遍预排序,当gap = 1的时候在直接插入排序。因此时间复杂度是大于O(N)。但是这毕竟是少数情况,大多数排序都是随机数组或是逆序排顺序。


为了更好的比较希尔排序和直接插入排序的效率,我们可以生成一组随机数进行比较查看他们所消耗的时间。

以下是两本书中对希尔排序时间复杂度的描述:

因为我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序_13[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_14


来算。

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_15[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序_16[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_17[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_18

6.希尔排序和直接插入排序效率测试

6.1测试

我们创建一个数组,数组的元素个数有100000个,让其数字全部随机生成,我们通过希尔排序和直接插入排序分别对这个数组进行排序。观察他们所消耗的时间。

//时间对比
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);

for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
}

int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();

int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();


printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);

free(a1);
free(a2);
}

int main()
{
TestOP();
return 0;
}

6.2结论

6.2.1随机生成数

通过结果我们能够发现,在100000个数字下,希尔排序的效率远远高于直接插入排序。

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_希尔排序_19[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_20

我们也可以改变数组大小再测试测试

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_21[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_直接插入排序_22

 我们能够发现当数组越大时,希尔排序比直接插入排序的效率越高。

6.2.2有序数组

 当我们让数组变得有序我们再查看结果:

[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_23[ 数据结构 -- 手撕排序算法第三篇 ] 希尔排序_时间复杂度_24

 正如我们所分析的那样,当数组本身有序时,希尔排序的预排序阶段就会不起作用,此时希尔排序的效率不及直接插入排序。

7.希尔排序特性总结

希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定。

4. 稳定性:不稳定

(本篇完)