排序算法-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 ( n 3 ) O(n^3) O(n3),因为每层循环的量级并不是 n n n。事实上,在最坏的情况下,希尔排序的时间复杂度也就是 O ( n 2 ) O(n^2) O(n2)。但一般情况却不好估计,因为其依赖于增量序列的取法。

前面我们说了希尔同学当年直接用了简单的方法取增量序列:初次取序列的一半为增量,以后每次减半,直到增量为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...] Dk=2k1=[1,3,7,15,31,63,127,255,511,1023,2047,4095,8191...],从而避免了前面例子所遇到的情况,提高了希尔排序的效率。当然其他大佬还有其他大佬的一些方法,总体原则是应该尽量避免序列中的值互为倍数的情况。

所以希尔排序的时间复杂度是不定的,若是取Hibbard增量序列,最坏的情况是 O ( n 1.5 ) O(n^{1.5}) O(n1.5)

空间复杂度

因为希尔排序直接在原址进行,不需要另外的空间,所以空间复杂度是 O ( 1 ) O(1) O(1)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
排序作业 选择题(每题2分,共22分)。 1.若表R在排序前已按键值递增顺序排列,则(   )算法的比较次数最少。 A.直接插入排序            B.快速排序     C.归并排序                D.选择排序 2.对各种内部排序方法来说,(   )。 A.快速排序时间性能最佳                             B.归并排序是稳定的排序方法 C.快速排序是一种选择排序                          D.堆排序所用的辅助空间比较大 3.  排序算法的稳定性是指(   )。 A.经过排序之后,能使值相同的数据保持原顺序的相对位置不变。 B.经过排序之后,能使值相同的数据保持原顺序的绝对位置不变。 C.排序算法的性能与被排序元素的数量关系不大 D.排序算法的性能与被排序元素的数量关系密切 4. 如下序列,(   )序列是大顶堆。 A.  {4,5,3,2,1}               B.  {5,3,4,1,2}        C.  {1,2,3,4,5}               D.  {1,2,3,5,4} 5. 若将{3,2,5,4,1}排为升序,则实施快速排序一趟后的结果是(   )(其,枢轴记录取首记录)。 A.  {1,2,3,4,5}                  B.  {1,2,4,5,3}        C.  {1,3,5,4,2}                  D.  {2,5,4,1,3} . 若将{1,2,3,4,5,6,7,9,8}排为升序,则(   )排序方法的“比较记录”次数最少。 A.  快速排序                   B.  简单选择排序     C.  直接插入排序               D.  冒泡排序 7. 若将{5,4,3,2,1}排为升序,则(   )排序方法的“移动记录”次数最多。 A.  快速排序                                B.  冒泡排序 C.  直接插入排序                       D.  简单选择排序 8. 用简单选择排序将顺序表{2,3,1 ,3′,2′}排为升序,实施排序1趟后结果是{1 ,3,2 ,3′,2′},则排序3趟后的结果是(   )。 A.  {1 ,2,3 ,3′,2′}                       B.  {1 ,2 ,2′,3 ,3′} C.  {1 ,2′,2 ,3 ,3′}                      D.  {1 ,2 ,2′,3′,3 } 9.下列排序算法,(    )排序在某趟结束后不一定选出一个元素放到其最终的位置上。 A.选择             B.冒泡           C.归并           D.堆 10.下列排序算法,稳定的排序算法是(  )。 A.堆排序                B.直接插入排序   C.快速排序              D.希尔排序 11.堆排序的时间复杂度是(    )。 A.O(n*n)                 B.O(n*log n)       C.O(n)                   D.O(log n) 填空题(每空4分,共4分)。 对n个元素进行归并排序,空间复杂度为         。 综合题(共24分)。 1. (共12分)有一组待排序的关键字如下: (54,38,96,23,15,72,60,45,83) 分别写出希尔排序(d=5)、快速排序、堆排序、归并排序第一趟升序排序后的结果(其堆排序的第一趟指序列完成初始建堆、将堆顶元素置为最末位置后其余元素调整为堆的结果)(每个3分)。 希尔排序:   快速排序: 堆排序: 归并排序:  2. (共12分)已知数据序列为(12,5,9,20,6,31,24),对该项数据序列进行排序,分别写出直接插入排序、简单选择排序、快速排序、堆排序、二路归并排序及基数排序第一趟升序排序结果(其堆排序的第一趟指序列完成初始建堆、将堆顶元素置为最末位置后其余元素调整为堆的结果)(每个2分)。 直接插入排序: 简单选择排序: 快速排序: 堆排序: 二路归并排序: 基数排序:    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值