本篇博客将会详细解释插入排序以及希尔排序,即讲诉这两种算法的思想,实现逻辑以及具体C语言代码,并且简单说明以下这两种排序的时间复杂度。
这里为学习排序算法的初学者提供一个小小的建议,要想更快的理解排序,最好研究一个数据如何放到其正确的位置(也就是排序后正确的位置),再研究如何使得数组整体有序。
本篇博客在讲解这两种排序时也将会用这种形式进行讲解。
即先研究单趟排序,再研究整体
注: 本篇博客全部以排升序为例
插入排序是一种简单的插入排序法,其基本思想如下:
把待排序的记录按其大小插入到一个已经排好序的有序序列中,知道所有的记录插入完为止,得到一个新的有序序列。
其实,我们在幼崽阶段就已经有接触过这种算法了,还记得我们打扑克牌的时候是怎么整理手上行的牌让其从小到大排列的吗?
没错,其核心就是插入排序。
那么到底具体是如何实现的呢?按照前言的方法,从单趟开始进行讲解。
我们就以一个例子来讲诉把,看看下面这组数据:
这里我们想把2放到数组中正确的位置上,那么该如何进行呢?
其实思想还是相对简单,如图,我们先用一个索引指向已经有序的数组的最后一位,并且依次将索引指向的值和待排序的值进行比较,如果索引指向的值更大,那么就让索引的值后移,然后索引指向当前的前一个数,直到索引指向的值比待排序的值小为止,最后把待排序的数放到索引后一个位置即可,末状态如下:
那么,有了这个基础,排序一整个数组就很好办了,当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移,来看一个动图来更好的理解这个过程:
如对代码有疑问,可以在评论区提出
接下来附上C语言实现代码:
void InsertSort(int* a, int n)
{
//模拟处理扑克牌时的情景
//第一张牌不需要排序
//先单趟,再整体
for (int i = 1; i < n; ++i)
{
int end = i - 1, tmp = a[i];
while (end >= 0)
{
//如果前一个要比tmp更大,需要往前排
//切记不能和a[i]比,因为在进行位次处理时前面的数有可能会覆盖a[i]的
//数,应该先把a[i]保存
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
//这里跳出循环更好,这样就不用分开判断tmp
//最小的情况了(即tmp比a[0]还小)
break;
}
a[end + 1] = tmp;
}
}
最坏情况: O ( n 2 ) O(n^2) O(n2) (实现次数是一个等差数列)
最好情况: O ( n ) O(n) O(n) 一遍过
希尔排序法又称为是缩小增量排序,其是对插入排序的改进,并且相对于插入排序在效率上有了很大的进步。
先选定一个整数gap,把待排序文件中所有记录分成个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,依次以一定规律缩小gap,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
下面看一张图来理解以下这一过程:
图中,相同颜色的数字在单趟排序中被分为一组。
同样,我们首先研究单趟排序,也就是在一次gap不变的前提下,如何让每组数据排好。
其实,此时的单趟排序可以理解为是步长更大的插入排序,这里有两种解决方法:
由于单组根插入排序差不多,这里不进行详细讲解,直接上代码!
//第一层循环表示处理的是第几组数据
for(int j = 0; j<gap;++j)
{
//由于比较的是间隔gap的数据,所以是+=gap
for (int i = gap + j; i < n; i += gap)
{
//end首先指向组中的最后一个数据,并从此开始一一比较进行插入
int end = i - gap, tmp = a[i];
while (end >= j)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
//同时处理多组,每次插入对应组的正确位置
for (int i = gap; i < n; ++i)
{
int end = i - gap, tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
每次排完一次序让gap缩小,最后缩小到1时其实就相当于是插入排序了,所以整体排序的重点是每次排完一次序让gap变小,并且最后能够达到1,这里大家比较认同的取法是每次取gap=gap/2或者gap = gap/3+1,这两种做法都能使得gap最后变成1,下面以同时处理多组数据的单趟排序为方法附上整体排序代码。
void ShellSort(int* a, int n)
{
//希尔排序其实是经过预处理之后的插入排序
//在预处理时将数据间隔gap分为一组
//这样能够让大的数尽快跑到前面去
//先考虑对间隔gap的一组数如何排序
//然后考虑多组数
//每次让gap减小,到1时就相当于插入排序了
//同时处理三组,效率一样,但是代码更简洁
int gap = n;
while(gap>1)
{
gap = gap / 3 + 1;
for (int i = gap; i < n; ++i)
{
int end = i - gap, tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
在希尔排序这块,时间复杂度非常难以计算,因为其受到gap如何取的影响。但是我们可以大致的理解为什么希尔排序会比插入排序更加高效,其实很简单,仔细理解希尔排序的排序过程,你会发现:
- 希尔排序由于数据之间间隔gap能够让大的数更快的跳到后面去,而插入排序只能允许数据一个一个的跳。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
通过对性能的测试,可以发现希尔排序的时间是和快速排序等 O ( n l o g n ) O(n logn) O(nlogn)的所用的时间差不多,所以大多人会认为希尔排序的时间复杂度是 O ( n l o g n ) O(n logn) O(nlogn),但根据一些书籍上所写这并不准确。
这里有一个猜想,有没有可能,在某一种gap的取值下希尔排序的效率能超过快速排序呢?如果真的找到了这种取值,那么排序界估计就要变样了哈哈,当然想要做到这步,需要的数学功底一定是非常深厚的,需要很大的努力才行。