您的位置:首页 > 运维架构 > Shell

Shell Sort 希尔排序

2014-10-17 09:10 483 查看
希尔排序(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 8

void 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 77

gap = 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 80

gap = 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 80

gap = 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  100

void 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] = 1
sedge[1] = 19
sedge[2] = 109
sedge[3] = 505
sedge[4] = 2161
sedge[5] = 8929
sedge[6] = 36289
sedge[7] = 146305
sedge[8] = 587521
sedge[9] = 2354689
sedge[10] = 9427969
sedge[11] = 37730305
sedge[12] = 150958081
sedge[13] = 603906049
sedge[14] = -2147483648

sedge[14] = 5
sedge[15] = 41
sedge[16] = 209
sedge[17] = 929
sedge[18] = 3905
sedge[19] = 16001
sedge[20] = 64769
sedge[21] = 260609
sedge[22] = 1045505
sedge[23] = 4188161
sedge[24] = 16764929
sedge[25] = 67084289
sedge[26] = 268386305
sedge[27] = 1073643521
sedge[28] = -2147483648

1, 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

/article/2398249.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: