希尔排序法的使用

希尔排序法的使用

希尔排序法的背景:

希尔排序按其设计者希尔(Donald Shell)的名字命名,该算法由希尔1959年公布。一些老版本教科书和参考手册把该算法命名为Shell-Metzner,即包含Marlene Metzner Norton的名字,但是根据Metzner本人的说法,“我没有为这种算法做任何事,我的名字不应该出现在算法的名字中。”

其设计者还真是风趣十足!实际上,希尔的话确实有一定道理,他并没有创造一种新的算法。因为从严格意义上来讲:希尔排序法其实是插入排序法的一个改良版本。而希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

经过改良后的插入排序法就变得更加高效了。这种新的排序方法还有一个别名,叫做:缩小增量排序,个人觉得这个名字更能体现希尔排序法的核心思想。那么下面让我们从这个名字出发,彻底理解希尔排序法!

希尔排序法的理解:

所谓缩小增量排序,即一步步使增量递减来完成排序。那么什么是增量呢?理解这个概念之前,我们知道希尔排序法与插入排序法最大的区别是希尔排序法使用了分组的思想。在分组时我们会使用 g a p gap gap 来作为我们分组的标准,而这个 g a p gap gap,就是我们所说的增量。希尔排序法就是通过这样一个增量来把数组分为若干组,对组内元素进行插入排序。排序完一轮之后,再缩小这个增量,使之变为 g a p − 1 gap - 1 gap1 ,再进行排序。直至 g a p = 1 gap = 1 gap=1 (此时退化为插入排序法),最后一轮排序结束时,数组的排序也就完成了。

假设这里有一个存放有 10 个元素的数组 nums,现在对它进行希尔排序:

0123456789
5526432129732637

我们需要先确定第一个 g a p gap gap ,一般为数组长度的一半,也就是 5,来进行第一次分组:

  1. (nums[0])55, (nums[5])12
  2. (nums[1])2, (nums[6])9
  3. (nums[2])6, (nums[7])73
  4. (nums[3])4, (nums[8])26
  5. (nums[4])32, (nums[9])37

之后再使用插入法分别对每一组进行排序:

  1. (nums[0])12, (nums[5])55
  2. (nums[1])2, (nums[6])9
  3. (nums[2])6, (nums[7])73
  4. (nums[3])4, (nums[8])26
  5. (nums[4])32, (nums[9])37
0123456789
1226432559732637

这样整个数组就稍微整齐了那么一点点(第 1 轮只有 1 次对换)。但是这个数组整体来看还是混乱的,我们减少增量( g a p = 4 gap = 4 gap=4),再继续下一轮的分组:

  1. (nums[0])12, (nums[4])32, (nums[8])26
  2. (nums[1])2, (nums[5])55, (nums[9])37
  3. (nums[2])6, (nums[6])12
  4. (nums[3])4, (nums[7])73

使用插入法分别对每一组进行排序:

  1. (nums[0])12, (nums[4])26, (nums[8])32
  2. (nums[1])2, (nums[5])37, (nums[9])55
  3. (nums[2])6, (nums[6])12
  4. (nums[3])4, (nums[7])73
0123456789
12264263712733255

这样,整个数组又整齐了一些(第 2 轮有 2 次对换)。

总共经过 5 轮操作之后( g a p = ( 5 , 4 , 3 , 2 , 1 ) gap = (5, 4, 3, 2, 1) gap=(5,4,3,2,1)),我们就完成了对数组的排序。

希尔排序法的实现代码:

void ShellSort(int nums[], int len, int count)
{
    int i, j, gap;
    for (gap = len / 2; gap > 0; gap -= 1) //最外层循环控制增量的减少(轮数)
    {
        for (i = 0; i < gap; i++) //次外层循环控制每一小组
        {
            for (j = i + gap; j < len; j += gap) //内层循环控制小组中每一元素
            {
                if (nums[j] < nums[j - gap]) //判断是否需要进行排序
                {
                    int ins = nums[j], idx = j - gap; //需要则定义两个变量,插入数与被插索引
                    for (; ins < nums[idx] && idx >= 0; idx -= gap) //最内层循环在小组中寻找插入位置
                    {
                        nums[idx + gap] = nums[idx]; //寻找并替换元素
                        count++; //用于计算对换次数
                    }
                    nums[idx + gap] = ins; //将插入数插入该索引(for循环最后做了一次索引减 gap 所以这里要加一个 gap 回来)
                }
            }
        } //以下呈现希尔排序并展示对换次数
	for (int k = 0; k < len; k++)
            cout << nums[k] << ((k == i) ? "|" : ",");
        cout << endl;
        cout << endl;
        cout << "count = " << count <<endl;
    }
}        

传入数组后的程序运行结果:(“ | ” 之前为已排好序的组)
在这里插入图片描述

插入排序法的实现代码(对照):

void InSort(int nums[], int len, int count)
{
 for (int i = 1; i < len; i++)
 {
  int ins = nums[i], idx = i - 1;
  for ( ; ins < nums[idx] && idx >= 0; idx--)
   nums[idx + 1] = nums[idx]; //寻找位置
  nums[idx + 1] = ins; //插入!
  for (int j = 0; j < len; j++)
  {
   cout << nums[j] << ((j == i) ? "|" : ",");
   count++;
  }
  cout << endl;
  cout << endl;
 }
 cout << "count = " << count;
}

传入数组后的程序运行结果:(“ | ” 之前为已排好序的组)
在这里插入图片描述

希尔排序法和插入排序法的区别:

两者的区别主要通过时间复杂度稳定性体现:

  • 时间复杂度:
    希尔排序法的时间复杂度 O ( n 3 2 ) O(n^\frac{3}{2}) O(n23)(下界为 O ( n l o g 2 n ) O(nlog2n) O(nlog2n))。
    插入排序法的时间复杂度 O ( n 2 ) O(n^2) O(n2)

二者的速度我们通过变量 count 就能有直观的感受,插入排序法仅适用于几乎已经排好序的数据,而希尔排序法适用于中等大小规模的数据,对规模非常大的数据排序不是最优选择。但是比 O ( n 2 ) O(n^2) O(n2) 复杂度的算法快得多,并且希尔排序非常容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。

因此专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。

有人通过大量的实验,给出了较好的结果:当数据较大时,时间复杂度约在 O ( n 5 4 ) O(n^\frac{5}{4}) O(n45) 1.6 O ( n 5 4 ) 1.6O(n^\frac{5}{4}) 1.6O(n45) 之间。

  • 稳定性:
    希尔排序法为不稳定排序
    插入排序法为稳定排序

关于稳定排序的定义:保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。如果 n u m s [ 1 ] = n u m s [ 2 ] nums[1] = nums[2] nums[1]=nums[2] n u m s [ 1 ] nums[1] nums[1] 原来在 n u m s [ 2 ] nums[2] nums[2] 位置前,那么排序后 n u m s [ 1 ] nums[1] nums[1] 仍在 n u m s [ 2 ] nums[2] nums[2] 的位置前。)

如:(黄色数字为 n u m s [ 1 ] nums[1] nums[1],加粗数字为 n u m s [ 2 ] nums[2] nums[2]

排序前:1, 3, 3, 2
排序后:1, 2, 3, 3

稳定排序

排序前:1, 3, 3, 2
排序后:1, 2, 3, 3

不稳定排序

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值