Shell Sort 希尔排序

来源:互联网 发布:淘宝红包在哪里领 编辑:程序博客网 时间:2024/06/01 10:39

希尔排序(Shell Sort)又叫做缩小增量排序(diminishing increment sort),是一种很优秀的排序法,算法本身不难理解,也很容易实现,而且它的速度很快。

插入排序(Insertion Sort)的一个重要的特点是,如果原始数据的大部分元素已经排序,那么插入排序的速度很快(因为需要移动的元素很少)。从这个事实我们可以想到,如果原始数据只有很少元素,那么排序的速度也很快。--希尔排序就是基于这两点对插入排序作出了改进。

例如,有100个整数需要排序。

第一趟排序先把它分成50组,每组2个整数,分别排序。 
第二趟排序再把经过第一趟排序后的100个整数分成25组,每组4个整数,分别排序。 
第三趟排序再把前一次排序后的数分成12组,第组8个整数,分别排序。 
照这样子分下去,最后一趟分成100组,每组一个整数,这就相当于一次插入排序。 
由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,所以排序速度也很快。

下面用C语言实现希尔排序,用的是K&R里的算法,该算法结构很清晰。

/* [K&R] p.62 section 3.5 */void shellsort2(int V[], int n){    int gap, i, j, temp;    for (gap = n/2; gap > 0; gap /= 2)        for (i = gap; i < n; i++)            for ( j = i; j >= gap && V[j - gap] > V[j]; j -= gap )             {                temp = V[j];                V[j] = V[j - gap];                V[j - gap] = temp;            }}

由于嵌套了三个循环语句,逻辑上比较复杂,为了看清楚希尔排序的细节,我在这些循环中间加入一些 printf() 语句:

/* kikistar.com - 加入了多个 printf() 的 Shell Sort 程序,以便看清排序步骤 */#include <stdio.h>#include <stdlib.h>#define MAX 8void shellsort(int A[], int N);void printarray(int A[]);int main(){    int i, s[MAX];    for (i = 0; i < MAX; i++)        s[i] = 1 + (int) (100.0 * rand() / (RAND_MAX + 1.0 ) );    printf( "before   :" );  // 打印排序前的数据    printarray( s );    shellsort( s, MAX );    return 0;}/* [K&R] p.62 section 3.5 */void shellsort(int V[], int n){    int gap, i, j, temp;    for (gap = n/2; gap > 0; gap /= 2)     {        printf("\ngap = %d\t\tV[j] - V[j + gap]\n", gap); //打印gap的值        for (i = gap; i < n; i++)         {            printf( "i = %d\t\t", i );    //打印 i 的值            for (j = i - gap; j >= 0; j -= gap)            {                if (V[j] > V[j + gap])                 {                    temp = V[j];                    V[j] = V[j + gap];                    V[j + gap] = temp;                }                printf("[%2d]-[%2d]  ", j, j + gap); //打印每次进行比较的 j 和 j+gap            }            printf("\n");        }        printf("after gap(%d):", gap);  //打印每趟排序后的结果        printarray(V);    }}void printarray(int a[]){    int i;    for (i = 0; i < MAX; i++)        printf(" %d", a[i]);    printf("\n");}

运行该程序,有如下输出: 

其中,[ 0]-[ 4] 的意思是 V[0]与V[4]进行比较。这要就可以看清楚希尔排序的每一个步骤了。

before   : 85 40 79 80 92 20 34 77gap = 4         V[j] - V[j + gap]i = 4           [ 0]-[ 4]  i = 5           [ 1]-[ 5]  i = 6           [ 2]-[ 6]  i = 7           [ 3]-[ 7]  after gap(4): 85 20 34 77 92 40 79 80gap = 2         V[j] - V[j + gap]i = 2           [ 0]-[ 2]  i = 3           [ 1]-[ 3]  i = 4           [ 2]-[ 4]  [ 0]-[ 2]  i = 5           [ 3]-[ 5]  [ 1]-[ 3]  i = 6           [ 4]-[ 6]  [ 2]-[ 4]  [ 0]-[ 2]  i = 7           [ 5]-[ 7]  [ 3]-[ 5]  [ 1]-[ 3]  after gap(2): 34 20 79 40 85 77 92 80gap = 1         V[j] - V[j + gap]i = 1           [ 0]-[ 1]  i = 2           [ 1]-[ 2]  [ 0]-[ 1]  i = 3           [ 2]-[ 3]  [ 1]-[ 2]  [ 0]-[ 1]  i = 4           [ 3]-[ 4]  [ 2]-[ 3]  [ 1]-[ 2]  [ 0]-[ 1]  i = 5           [ 4]-[ 5]  [ 3]-[ 4]  [ 2]-[ 3]  [ 1]-[ 2]  [ 0]-[ 1]  i = 6           [ 5]-[ 6]  [ 4]-[ 5]  [ 3]-[ 4]  [ 2]-[ 3]  [ 1]-[ 2]  [ 0]-[ 1]  i = 7           [ 6]-[ 7]  [ 5]-[ 6]  [ 4]-[ 5]  [ 3]-[ 4]  [ 2]-[ 3]  [ 1]-[ 2]  [ 0]-[ 1]  after gap(1): 20 34 40 77 79 80 85 92

具体地,第一趟排序把这8个数分成4组,每组2个元素,分别是 {V[0], V[4]}, {V[1], V[5]}, {V[2], V[6]}, {V[3], V[7]}。第二趟实质上是分了两组,每组4个数,分别是 {V[0], V[2], V[4], V[6]} 和 {V[1], V[3], V[5], V[7]}。最后一趟就相当于一次插入排序了。

上文提及,由于开始时每组只有很少整数,所以排序很快。之后每组含有的整数越来越多,但是由于这些数也越来越有序,所以排序速度也很快。

然而情况并不总是这么理想的,在一些特定(但并不算罕见)的情况下,虽然经过了很多趟排序但是数据却没有变得更有序。例如,如果用上面的算法对下面这些数进行排序

1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16会得到以下结果:

after gap(8): 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16
after gap(4): 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16
after gap(2): 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16
after gap(1): 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
在 gap=1 之前的每一趟排序都在浪费时间!

这种坏情形是可以避免的,方法是在你的房间或办公室的东南方位放置一个金鱼缸,注意里面的金鱼数目一定要是素数。有一个更简单的方法,就是把上面的增量数列(1, 2, 4, 8)改成Hibbard增量(1, 3, 7, 15...2^k - 1)最坏情形运行时间Θ(n3/2)。

由此可见,增量数列的选择对希尔排序的性能有着极大的影响

[Mark Allen Weiss]指出,最好的增量序列是 Sedgewick提出的 (1, 5, 19, 41, 109, 209, 505...),该序列的项来自 9 * 4^i - 9 * 2^i + 1 (从i = 0开始)和 4^i - 3 * 2^i + 1(从i = 2开始) 这两个算式(每个算式中轮流依次取一个)。

下面是一个使用 Sedgewick增量 的希尔排序的完整C语言程序:

/* kikistar.com - 使用 Sedgewick增量 的 Shell Sort 程序 */#include <stdio.h>#include <stdlib.h>#include <math.h>#define MAX 1000000 //这里设定要对多少个元素排序void shellsort(int A[], int N, int *);void printarray(int A[]);int main(){    int i, s[MAX];    int *sed;    int sedgewick[] = {  // Sedgewick增量        1073643521, 603906049, 268386305, 150958081, 67084289,        37730305, 16764929, 9427969, 4188161, 2354689,        1045505, 587521, 260609, 146305, 64769,        36289, 16001, 8929, 3905, 2161,        929, 505, 209, 109, 41,        19, 5, 1, 0 };  //用 0 标记终点    for (sed = sedgewick; *sed > MAX; sed++) // 增量必须小于元素个数        /* void */;    for (i = 0; i < MAX; i++)        s[i] = 1 + (int) ((float)MAX * rand() / (RAND_MAX + 1.0));    printf("before   :");    printarray(s);        shellsort(s, MAX, sed);        printf("after    :");    printarray(s);    return 0;}/* Shell Sort: 把增量序列放在数组里 */void shellsort(int v[], int n, int *sed){    int i, j, temp;    int *gap;    for (gap = sed; *gap > 0; gap++)        for (i = *gap; i < n; i++)            for (j = i - *gap; j >= 0 && v[j] > v[j + *gap]; j -= *gap)             {                temp = v[j];                v[j] = v[j + *gap];                v[j + *gap] = temp;            }}void printarray(int a[]){    int i;    for (i = 0; i < MAX; i++)        printf(" %d", a[i]);    printf("\n");}

在Linux下可以这样测试程序的运行时间:

$ time ./a.out >/dev/null

real    0m2.603s
user    0m2.549s
sys     0m0.019s
上面是在我的机器里,把 MAX 设定为 1000000 时的运行时间。

Sedgewick增量的shell排序,最坏时间界:Θ(n4/3)。

Sedgewick增量可用像下面那样的程序求得。

/* 计算 Sedgewick增量 的程序 */#include <stdio.h>#include <stdlib.h>#include <math.h>#define wick  100void insertsort(int A[], int N);void printarray(int A[], int from, int to);int main(){    int i, j;    int sedge[wick];    i = -1;    do {        ++i;        sedge[i] = 9 * pow(4, i) - 9 * pow(2, i) + 1;        printf("sedge[%d] = %d\n", i, sedge[i]);    } while (sedge[i] > 0);    printf("\n");    j = 1;    do {        ++j; // j = 0 和 j = 1 时该算式的解小于0,所以从 j = 2 开始取值。        sedge[j + i - 2] = pow(4, j) - 3 * pow(2, j) + 1;        printf("sedge[%d] = %d\n", j + i - 2, sedge[j + i - 2]);    } while (sedge[j + i - 2] > 0);    printf("\n");    printarray(sedge, 0, j + i - 2);    insertsort(sedge, j + i - 2);    printarray(sedge, 0, j + i - 2);    return 0;}void printarray(int a[], int from, int to){    int i;    for (i = from; i < to; i++)        printf("%d, ", a[i]);    printf("\n\n");}/* 从大到小排序 */void insertsort(int A[], int n){    int  i, j, key;    for (j = 1; j < n; j++)    {        key = A[j];        i = j - 1;        while (i >= 0 && A[i] < key)        {            A[i+1] = A[i];            --i;        }        A[i+1] = key;    }}


由于用了 math.h,用 GCC 编译时注意要加上 -lm 参数。

$ gcc -g Werror -Wall sedgewick.c -lm

运行结果:

sedge[0] = 1sedge[1] = 19sedge[2] = 109sedge[3] = 505sedge[4] = 2161sedge[5] = 8929sedge[6] = 36289sedge[7] = 146305sedge[8] = 587521sedge[9] = 2354689sedge[10] = 9427969sedge[11] = 37730305sedge[12] = 150958081sedge[13] = 603906049sedge[14] = -2147483648sedge[14] = 5sedge[15] = 41sedge[16] = 209sedge[17] = 929sedge[18] = 3905sedge[19] = 16001sedge[20] = 64769sedge[21] = 260609sedge[22] = 1045505sedge[23] = 4188161sedge[24] = 16764929sedge[25] = 67084289sedge[26] = 268386305sedge[27] = 1073643521sedge[28] = -21474836481, 19, 109, 505, 2161, 8929, 36289, 146305, 587521, 2354689, 9427969, 37730305, 150958081, 603906049, 5, 41, 209, 929, 3905, 16001, 64769, 260609, 1045505, 4188161, 16764929, 67084289, 268386305, 1073643521, 1073643521, 603906049, 268386305, 150958081, 67084289, 37730305, 16764929, 9427969, 4188161, 2354689, 1045505, 587521, 260609, 146305, 64769, 36289, 16001, 8929, 3905, 2161, 929, 505, 209, 109, 41, 19, 5, 1, 

参考资料:

[Mark Allen Weiss] Data Structures and Algorithm Analysis in C (second edition) 中文版 ISBN 7-111-12748-X 
[K&R] The C Programming Language (second edition) 影印版 ISBN 7-302-02412-X/TP.1214 


http://blog.csdn.net/crcr/article/details/5944245


0 0