排序算法-6-希爾排序

希爾排序(Shell Sort)

希爾排序是對插入排序的一種優化。對插入排序不熟悉的同學可以參考插入排序一文。

原理

插入排序中每次比較之後只能將數據挨着移動一位,因此效率並不高。但是插入排序對於幾乎已經排好序的數據操作時,效率是很高的。希爾排序的思想就是針對這兩點做了優化,使每一次比較之後元素可以跨過多個數據移動,從而提高了整體效率。優化方式是對要排序的元素進行分組,然後在每個分組內進行插入排序。

這樣說還是比較抽象,我們用一個類比來說明原理。假設一行人要從低到高排隊,不妨設10個人吧,步驟如下:

  1. 第一次分組,10個人按1,2,3,4,5循環報數,報到相同數字的爲一組。
  2. 分到同一組的人組內排序,因爲每一組只有兩人,所以只需比較一次即可,低的在前,高的在後。
  3. 第二次分組,10個人按1,2循環報數,報到相同數字的爲一組。
  4. 分到同一組的人在組內進行插入排序。
  5. 第三次分組,此時10個人整個爲一大組。
  6. 組內進行插入排序。結束。

回頭再看一下以上步驟,不難發現其實以上6步就是分組、插入排序的反覆循環,於是我們可以得到希爾排序的一般步驟:

  1. 取一個增量,按其進行分組。
  2. 組內進行插入排序。
  3. 減小增量,再次分組。
  4. 重複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(n3)O(n^3),因爲每層循環的量級並不是nn。事實上,在最壞的情況下,希爾排序的時間複雜度也就是 O(n2)O(n^2)。但一般情況卻不好估計,因爲其依賴於增量序列的取法。

前面我們說了希爾同學當年直接用了簡單的方法取增量序列:初次取序列的一半爲增量,以後每次減半,直到增量爲1。然而這種取法在一些特殊情況下,會有效率問題。

舉一個簡單的例子:比如4個數[1,3,2,4]用希爾排序。第一步取增量爲4/2=2,分組結果:1,2爲一組,3,4爲一組,組內排序,結果還是[1,3,2,4]。發現沒?經過一輪排序,居然一點效果都沒有,純屬浪費時間。

於是針對這個問題,有一些大佬們就開始改進增量的取法。其中一個叫Hibbard的大佬把增量序列的取法改爲 Dk=2k1=[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...],從而避免了前面例子所遇到的情況,提高了希爾排序的效率。當然其他大佬還有其他大佬的一些方法,總體原則是應該儘量避免序列中的值互爲倍數的情況。

所以希爾排序的時間複雜度是不定的,若是取Hibbard增量序列,最壞的情況是 O(n1.5)O(n^{1.5})

空間複雜度

因爲希爾排序直接在原址進行,不需要另外的空間,所以空間複雜度是 O(1)O(1)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章