希尔排序(Shell Sort)又叫做缩小增量排序(Diminishing-increment Sort),是由D.L.Shell在1959年提出来的,旨在对直接插入排序做出改进以得到更好的时间效率。
希尔排序的基本思想
设待排序对象序列有N个对象,首先取一个整数gap(<N)作为间隔,将全部对象分为gap个子序列,所有距离为gap的对象放在同一个子序列中,在每一个子序列中分别实施直接插入排序,然后缩小间隔gap,例如gap=[gap/2],重复上述的子序列划分和排序工作,直到最后取gap==1,将所有对象放在同一个序列中进行直接插入排序为止。
希尔排序为什么速度较快?
由于开始时gap的取值较大,每个子序列中的对象较少,排序速度较快;待到排序的后期,gap取值逐渐变小,子序列中的对象个数逐渐变多,但由于前面工作的基础,大多数对象已经基本有序,所以排序速度依然很快。
典型的希尔排序看起来是这样子滴,图片来源戳这里。
在希尔排序中用到的gap序列,常见的有下面几种:
Gap Sequences: // https://en.wikipedia.org/wiki/Shellsort#Gap_sequences 1. Shell's original sequence: N/2, ..., 4, 2, 1 (repeatedly divide by 2) 2. Hibbard's increments: 2**k-1, ..., 15, 7, 3, 1 3. Knuth's increments: ..., 121, 40, 13, 4, 1 4. Sedgewick's increments: ..., 109, 41, 19, 5, 1
下面以Shell先生的序列为例,介绍典型的希尔排序过程。
1. 输入序列为: 35 33 42 10 14 19 27 44 初始的增量为4, 分组为 {35, 14}, {33, 19}, {42, 27} 和 {10, 14}
2. 对上面的每一个分组进行插入排序,得到新的序列为 14 19 27 10 35 33 42 44
旧分组 | 动作 | 新分组 |
{35, 14} | 将14插入到35前面 | {14, 35} |
{33, 19} | 将19插入到33前面 | {19, 33} |
{42, 27} | 将27插入到42前面 | {27, 42} |
{10, 44} | 无需调整 | {10, 44} |
3. 将增量缩小为2对序列 14 19 27 10 35 33 42 44 进行处理, 分组为 {14, 27, 35, 42} 和 {19, 10, 33, 44}
4. 对上面的每一个分组进行插入排序,得到新的序列为 14 10 27 19 35 33 42 44
旧分组 | 动作 | 新分组 |
{14, 27, 35, 42} | 无需调整 | {14, 27, 35, 42} |
{19, 10, 33, 44} | 将10插入到19前面 | {10, 19, 33, 44} |
5. 将增量缩小为1对序列 14 10 27 19 35 33 42 44 进行处理,显然分组为 {14,10, 27, 19, 35, 33, 42, 44}
6. 将上面的分组进行插入排序,得到最终的序列为 10 14 19 27 33 35 42 44
注意: 以上6步的1-3步对应的图片来自于文章(Data Structure and Algorithms – Shell Sort)但是源链接上从第4步开始存在着错误(本来讨论的gap序列为4 2 1, 结果画图时用的gap序列却是4 1)(真是令人郁闷啦Orz),故4-6步对应的图片为私人定制(感谢贤妻对我博客写作的支持和不辞辛劳地PS!)。
下面给出基于Shell先生的gap序列的C代码实现。
1 void shellsort(int a[], size_t n) 2 { 3 int h = 1; 4 5 while (h < n / 2) 6 h = 2 * h; // 1, 2, 4, ... [N/2] 7 8 while (h >= 1) { 9 for (int i = h; i < n; i++) { 10 for (int j = i; j >= h && (a[j] < a[j-h]); j -= h) { 11 exchange(a, j, j-h); 12 } 13 } 14 15 h /= 2; 16 } 17 } 18 19 static void exchange(int a[], int i, int j) 20 { 21 int t = a[i]; 22 a[i] = a[j]; 23 a[j] = t; 24 }
完整的C代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 static void exchange(int a[], int i, int j); 5 static void show(int a[], size_t n); 6 7 void shellsort(int a[], size_t n) 8 { 9 int h = 1; 10 11 while (h < n / 2) 12 h = 2 * h; // 1, 2, 4, ... [N/2] 13 14 while (h >= 1) { 15 for (int i = h; i < n; i++) { 16 for (int j = i; j >= h && (a[j] < a[j-h]); j -= h) { 17 exchange(a, j, j-h); 18 19 printf("\t\t"); show(a, n); 20 printf("\t<--exchanged(a[%d], a[%d])\n", j, j-h); 21 } 22 } 23 24 printf("#h=%d\t\t", h); show(a, n); printf("\tDONE\n"); 25 26 h /= 2; 27 } 28 } 29 30 static void show(int a[], size_t n) 31 { 32 for (int i = 0; i < n; i++) 33 printf("%-2d ", a[i]); 34 } 35 36 static void exchange(int a[], int i, int j) 37 { 38 int t = a[i]; 39 a[i] = a[j]; 40 a[j] = t; 41 } 42 43 int main(int argc, char *argv[]) 44 { 45 if (argc < 2) { 46 fprintf(stderr, "Usage: %s <C1> [C2] ...\n", argv[0]); 47 return -1; 48 } 49 50 argc--; 51 argv++; 52 53 int n = argc; 54 int *a = (int *)malloc(sizeof(int) * n); 55 #define VALIDATE(p) do { if (p == NULL) return -1; } while (0) 56 VALIDATE(a); 57 58 for (int i = 0; i < n; i++) 59 *(a+i) = atoi(argv[i]); 60 61 printf(" "); 62 for (int i = 0; i < n; i++) 63 printf("%-2d ", i); 64 printf("\n"); 65 66 printf("Before sorting: "); show(a, n); printf("\n"); 67 shellsort(a, n); 68 printf("After sorting: "); show(a, n); printf("\n"); 69 70 free(a); a = NULL; 71 return 0; 72 }
o 编译并测试
$ gcc -g -Wall -m32 -std=c99 -o shellsort shellsort.c $ ./shellsort 8 7 6 5 4 3 2 1 0 1 2 3 4 5 6 7 Before sorting: 8 7 6 5 4 3 2 1 4 7 6 5 8 3 2 1 <--exchanged(a[4], a[0]) 4 3 6 5 8 7 2 1 <--exchanged(a[5], a[1]) 4 3 2 5 8 7 6 1 <--exchanged(a[6], a[2]) 4 3 2 1 8 7 6 5 <--exchanged(a[7], a[3]) #h=4 4 3 2 1 8 7 6 5 DONE 2 3 4 1 8 7 6 5 <--exchanged(a[2], a[0]) 2 1 4 3 8 7 6 5 <--exchanged(a[3], a[1]) 2 1 4 3 6 7 8 5 <--exchanged(a[6], a[4]) 2 1 4 3 6 5 8 7 <--exchanged(a[7], a[5]) #h=2 2 1 4 3 6 5 8 7 DONE 1 2 4 3 6 5 8 7 <--exchanged(a[1], a[0]) 1 2 3 4 6 5 8 7 <--exchanged(a[3], a[2]) 1 2 3 4 5 6 8 7 <--exchanged(a[5], a[4]) 1 2 3 4 5 6 7 8 <--exchanged(a[7], a[6]) #h=1 1 2 3 4 5 6 7 8 DONE After sorting: 1 2 3 4 5 6 7 8 $ ./shellsort 35 33 42 10 14 19 27 44 0 1 2 3 4 5 6 7 Before sorting: 35 33 42 10 14 19 27 44 14 33 42 10 35 19 27 44 <--exchanged(a[4], a[0]) 14 19 42 10 35 33 27 44 <--exchanged(a[5], a[1]) 14 19 27 10 35 33 42 44 <--exchanged(a[6], a[2]) #h=4 14 19 27 10 35 33 42 44 DONE 14 10 27 19 35 33 42 44 <--exchanged(a[3], a[1]) #h=2 14 10 27 19 35 33 42 44 DONE 10 14 27 19 35 33 42 44 <--exchanged(a[1], a[0]) 10 14 19 27 35 33 42 44 <--exchanged(a[3], a[2]) 10 14 19 27 33 35 42 44 <--exchanged(a[5], a[4]) #h=1 10 14 19 27 33 35 42 44 DONE After sorting: 10 14 19 27 33 35 42 44
参考资料:
- Comparison Sorting Algorithms (Very cool, 强烈推荐)
- ShellSort from wikipedia
- Computer Algorithms: Shell Sort
使用Knuth’s 的gap序列之C代码实现 (参考书籍: 《Algorithms》 Fourth Edition P259)
1 void shellsort(int a[], size_t n) 2 { 3 int h = 1; 4 5 while (h < n / 3) 6 h = 3 * h + 1; // 1, 4, 13, 40, 121, 364, 1093, ... 7 8 while (h >= 1) { // h-sort the array 9 for (int i = h; i < n; i++) { 10 // Insert a[i] among a[i-h], a[i-2*h], a[i-3*h]... . 11 for (int j = i; j >= h && (a[j] < a[j-h]); j -= h) { 12 exchange(a, j, j-h); 13 } 14 } 15 16 h /= 3; 17 } 18 } 19 20 static void exchange(int a[], int i, int j) 21 { 22 int t = a[i]; 23 a[i] = a[j]; 24 a[j] = t; 25 }
小结:
希尔排序是一种不稳定的排序方法。其空间复杂度为O(1),但时间复杂度不仅取决于gap,还取决于gap之间的数学性质,比如它们的公因子等。因此,对希尔排序的时间复杂度的分析很困难,在特定情况下可以准确地估算关键字的比较次数和对象移动次数,但是想要弄清关键字比较次数和对象移动次数与增量(gap)选择之间的依赖关系,并给出完整的数学分析,目前还没有人能够做到。在高德纳的书中,利用大量的实验统计资料得出,当N很大时,关键字平均比较次数和对象平均移动次数大约在n**1.25到1.6*N**1.25范围内,这是在利用直接插入排序作为子序列排序方法的情况下得到的。 高德纳的学生Robert Sedgewick也说了”透彻理解希尔排序的性能至今仍然是一项挑战“。
下一节,将介绍一种强大的选择排序算法,那就是令人魂牵梦萦的堆排序(Heap Sort)。