数据结构和算法学习之路——希尔排序详解(C++)
Many things in life are not what we can not do, but we do not believe that we can achieve them.
——生活中的许多事,并不是我们不能做到,而是我们不相信能够做到。
在上一篇博文里面我们学习了直接插入排序,其实呢,插入排序的大家庭里面可不仅仅只有直接插入排序哦,还有比如说折半插入排序,2-路插入排序等等,但是大家还记得么,这些算法的时间复杂度都没能突破O(n²),然而今天我们要讨论的这一个算法,同时该算法是冲破O(n²)的第一批算法之一:
它就是——希尔排序(也称缩小增量排序),是插入排序的升级版本
在本文中,我将和大家一起探讨该算法的过程,代码实现,而关于希尔排序的时间复杂度的研究一直是一个难题
1.希尔排序的具体过程
希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1,最后一步也就是回到了直接插入排序,但是此时整个数组已经基本宏观有序,只需要调整个别元素的位置,所以时间上的开销是比较小的
我们以下面的数组为例
int a[10] = {9, 1, 2, 5, 7, 4, 8, 6, 3, 5};
这个图是百度上的,我觉得画的挺不错的,但是其实我感觉gap用increment(增量)代替会比较容易理解,下面我们来分析分析这个过程:
首先,整个数组的长度位10,因此我们的增量increment取数组的长度(length)/2
说明:这里的increment的取值不是唯一的,有学者提出用increment = increment / 3 + 1 效率更高,而且关于增量如何选择也是一个数学难题,这里我们暂且用希尔本人提出的
increment = imcrement / 2 来计算增量
- 第一次分组,increment = 5,因此我们把数组中增量为5的数分为一组,像上图那样分,同样颜色的数字代表一组,因此整个数组就被分成了5组,每组只有两个数字
- 下面我们对每组的数字进行直接插入排序,注意我们的比较不再像直接插入排序那样一位一位的比,而是以increment为准,跳跃式的比较并且交换
- 那么第一趟排序的结果就是
4 1 2 3 5* 9 8 6 5 7 (5*是为了区别后面那个5) - 当进行完第一次排序之后,我们让increment / 2 以这个新的增量再次对该数组进行划分,第二次的increment为5/2 = 2,所以数组中增量为2的数字就被划为了新的组
- 继续进行上述操作,直到排序完成
2.希尔排序的C++实现
void shellsort(int a[], int length)
{
int i, j, k, temp, increment;
for(increment = length / 2; increment > 0; increment /= 2) //这个是增量每次变化的循环,下面所有的操作都要在这个大循环下进行
{
for(i = 0; i < increment; i++)
{
for(j = i + increment; j < length; j += increment) //分组操作
{
if(a[j] < a[j - increment])
{
temp = a[j];
for(k = j - increment; k >= 0 && a[k] > temp; k -= increment)
{
a[k + increment] = a[k]; //元素的移动操作
}
a[k + increment] = temp;
}
}
}
}
}
3. 希尔排序的时间复杂度分析
希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(n的二分之三次方),希尔排序时间复杂度的下界是O(n*log2n)。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。
本文中使用的是希尔增量