排序算法-6-希尔排序

希尔排序(Shell Sort)

希尔排序是对插入排序的一种优化。对插入排序不熟悉的同窗能够参考插入排序一文。html

原理

插入排序中每次比较以后只能将数据挨着移动一位,所以效率并不高。可是插入排序对于几乎已经排好序的数据操做时,效率是很高的。希尔排序的思想就是针对这两点作了优化,使每一次比较以后元素能够跨过多个数据移动,从而提升了总体效率。优化方式是对要排序的元素进行分组,而后在每一个分组内进行插入排序。web

这样说仍是比较抽象,咱们用一个类比来讲明原理。假设一行人要从低到高排队,不妨设10我的吧,步骤以下:shell

  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。方法简单直接,也能达到效果。app

实现

下面咱们按照原始的希尔排序来用代码实现。考虑到希尔排序是插入排序的改进,而插入排序是能够原址排序的,因此不须要另外再开辟空间。svg

下面就是用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");
}

运行结果:spa

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) ,由于每层循环的量级并非 n n 。事实上,在最坏的状况下,希尔排序的时间复杂度也就是 O ( n 2 ) O(n^2) 。但通常状况却很差估计,由于其依赖于增量序列的取法。.net

前面咱们说了希尔同窗当年直接用了简单的方法取增量序列:初次取序列的一半为增量,之后每次减半,直到增量为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...] ,从而避免了前面例子所遇到的状况,提升了希尔排序的效率。固然其余大佬还有其余大佬的一些方法,整体原则是应该尽可能避免序列中的值互为倍数的状况。

因此希尔排序的时间复杂度是不定的,如果取Hibbard增量序列,最坏的状况是 O ( n 1.5 ) O(n^{1.5})

空间复杂度

由于希尔排序直接在原址进行,不须要另外的空间,因此空间复杂度是 O ( 1 ) O(1)