希尔排序(Shell Sort)
希尔排序是对插入排序的一种优化。对插入排序不熟悉的同学可以参考插入排序一文。
原理
插入排序中每次比较之后只能将数据挨着移动一位,因此效率并不高。但是插入排序对于几乎已经排好序的数据操作时,效率是很高的。希尔排序的思想就是针对这两点做了优化,使每一次比较之后元素可以跨过多个数据移动,从而提高了整体效率。优化方式是对要排序的元素进行分组,然后在每个分组内进行插入排序。
这样说还是比较抽象,我们用一个类比来说明原理。假设一行人要从低到高排队,不妨设10个人吧,步骤如下:
- 第一次分组,10个人按1,2,3,4,5循环报数,报到相同数字的为一组。
- 分到同一组的人组内排序,因为每一组只有两人,所以只需比较一次即可,低的在前,高的在后。
- 第二次分组,10个人按1,2循环报数,报到相同数字的为一组。
- 分到同一组的人在组内进行插入排序。
- 第三次分组,此时10个人整个为一大组。
- 组内进行插入排序。结束。
回头再看一下以上步骤,不难发现其实以上6步就是分组、插入排序的反复循环,于是我们可以得到希尔排序的一般步骤:
- 取一个增量,按其进行分组。
- 组内进行插入排序。
- 减小增量,再次分组。
- 重复2,3直到增量为1时结束。
那这里问题就来了,这个增量怎么取呢?希尔同学当年是初次取序列的一半为增量,以后每次减半,直到增量为1。方法简单直接,也能达到效果。
实现
下面我们按照原始的希尔排序来用代码实现。考虑到希尔排序是插入排序的改进,而插入排序是可以原址排序的,所以不需要另外再开辟空间。
下面就是用C语言实现的代码。
- 要排序的数组a有n个元素。
- d为每一次的增量;每次排序之后把d减半。
- 组内进行插入排序,可以与插入排序一文中的代码比较一下看。
void shell_sort(int a[], int n)
{
if(n<=0)
return;
int i, j, key;
int d = n/2; //以d为增量进行分组
while (d > 0) {
/* 对组内元素进行插入排序 */
for (j=d; j<n; j++) { //分别向每组的有序区域插入
key = a[j]; //插入a[j]到该组的有序区
i = j-d; //a[j-d]是a[j]所在组的有序区的最后一个元素
while( i>=0 && a[i]>key ) {
a[i+d] = a[i]; //后移
i -= d;
}
a[i+d] = key; //插入
}
d = d/2; //减小d以进行下一次分组
}
}
为了验证此函数的效果,加上了如下辅助代码,对3个数组进行排序,运行结果在最后,可见排序成功。
#include <stdio.h>
#include <stdlib.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void shell_sort(int a[], int n);
void show_array(int a[], int n);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
shell_sort(array1, SIZE_ARRAY_1);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
shell_sort(array2, SIZE_ARRAY_2);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
shell_sort(array3, SIZE_ARRAY_3);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
运行结果:
Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18
分析
时间复杂度
从代码看,希尔排序用了三层循环,但它的时间复杂度却不是 O ( n 3 ) O(n^3) O(n3),因为每层循环的量级并不是 n n n。事实上,在最坏的情况下,希尔排序的时间复杂度也就是 O ( n 2 ) O(n^2) O(n2)。但一般情况却不好估计,因为其依赖于增量序列的取法。
前面我们说了希尔同学当年直接用了简单的方法取增量序列:初次取序列的一半为增量,以后每次减半,直到增量为1。然而这种取法在一些特殊情况下,会有效率问题。
举一个简单的例子:比如4个数[1,3,2,4]用希尔排序。第一步取增量为4/2=2,分组结果:1,2为一组,3,4为一组,组内排序,结果还是[1,3,2,4]。发现没?经过一轮排序,居然一点效果都没有,纯属浪费时间。
于是针对这个问题,有一些大佬们就开始改进增量的取法。其中一个叫Hibbard的大佬把增量序列的取法改为 D k = 2 k − 1 = [ 1 , 3 , 7 , 15 , 31 , 63 , 127 , 255 , 511 , 1023 , 2047 , 4095 , 8191... ] D_k=2^k−1=[1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191...] Dk=2k−1=[1,3,7,15,31,63,127,255,511,1023,2047,4095,8191...],从而避免了前面例子所遇到的情况,提高了希尔排序的效率。当然其他大佬还有其他大佬的一些方法,总体原则是应该尽量避免序列中的值互为倍数的情况。
所以希尔排序的时间复杂度是不定的,若是取Hibbard增量序列,最坏的情况是 O ( n 1.5 ) O(n^{1.5}) O(n1.5)。
空间复杂度
因为希尔排序直接在原址进行,不需要另外的空间,所以空间复杂度是 O ( 1 ) O(1) O(1)。