乐观的调优(插入排序&希尔排序)(下)

简介: 乐观的调优(插入排序&希尔排序)(下)

0.png

正文


5.平均情况


确实,在最坏情况里,选择排序比插入排序快。但是我们还应该考虑平均情况。


最好情况和最坏情况很少发生。现实世界里,最常出现的是平均情况。


这是很有道理的。你设想一个随便洗乱的数组,出现完全升序或完全降序的可能性有多大?最可能出现的情况应该是随机分布。


下面试试在各种场景中测试插入排序。


完全降序的最坏情况之前已经见过,它每一轮都要比较和平移所遇到的值(这两种操作合计N^2 步)。


对于完全升序的最好情况,因为所有值都已在其正确的位置上,所以每一轮只需要一次比较,完全不用平移。


最坏情况是所有数据都要比较和平移;最好情况是每轮一次比较、零次平移;对于平均情况,总的来看,是比较和平移一半的数据。


如果说插入排序的最坏情况需要 N 2 步,那么平均情况就是 N 2 / 2步。尽管最终大 O都会写成 O(N^2 )。


可以看到插入排序的性能在不同场景中差异很大。最坏、平均、最好情况,分别需要 N^2 、

N^2 / 2、N步。


那么哪种算法更好?选择排序还是插入排序?答案是:看情况。对于平均情况(数组里的值随机分布),它们性能相近。如果你确信数组是大致有序的,那么插入排序比较好。如果是大致逆序,则选择排序更快。


6.希尔排序


希尔排序是对插入排序做了简单的优化,却产生了质的飞跃。直接让不太起眼的插入排序比肩闻名算法界的快速排序。我们知道对于插入排序,数据的元素越是接近有序,那么它的效率就越高;对于完全有序的数组,它甚至可以快到O(N)。


那么希尔排序的主要思想是,我们不断地对数组进行预排序,使数组里大的元素尽量到数组的后面,小的元素尽量到数组的前面。


完成对数组的预排序看,我们采取的方法是对数组进行分组,把数组里间隔相同长度的元素划分为一组。例如:、‘.png

接下来我们对每一组的元素进行排序。

···.png

如图所示,第一趟排序,较大的元素已经到了数组的后面了。接下来再次重新分组,再次预排序:

··.png

经过第二趟预排序,我们发现数组已经大致接近有序了,那么最后一次,我们取间隔为1分组,其实就是一次普通的插入排序:

·.png

至此,数组已经完全有序了。

通过上面的例子不难看出,希尔排序就是对插入排序的简单优化,引入了预排序的概念。

当gap>1时,进行的是预排序(也就是对每一组进行插入排序);

当gap=1时,进行对整个数组的插入排序。


、‘.png

7.希尔排序的实现


下面使用C语言实现的希尔排序:

void ShellSort(int* a, int n)
{
  int gap = n;             //间隔
  while (gap > 1)
  {
    gap = gap / 3 + 1;   //+1是为了使gap最后等于1
    for (int i = 0; i < n - gap; i++)
    {
      int end = i;
      int tmp = a[end + gap];
      while (end >= 0)
      {
        if (tmp < a[end])
        {
          a[end + gap] = a[end];
          end -= gap;
        }
        else
        {
          break;
        }
      }
      a[end + gap] = tmp;
    }
  }
}

代码相较于插入排序并没有改变多少,在插入排序的基础上多了一层循环用来控制gap。

此处可以看出,预排序并不是固定的3次或者4次。而是取决于数组元素个数,这么做一是为了方便控制gap,二是预排序越多次,对整体排序而言更有利,所以我们不用吝啬预排序,并不是预排序越多,花费的步数就越多,时间复杂度就越高。

gap = gap / 3 + 1;


除了这样控制gap外,还有一种常用的方法:

gap = gap / 2;

但是根据有人实验得出,第一种方式比第二种方式快一点点,但是差距很细微,所以两者皆可。


8.希尔排序的效率


希尔排序的时间复杂度并不好算,因为每次取gap的值都不同,而且gap取不同值的情况下,每次预排序所面临的数据也不同。希尔排序的时间复杂度计算需要运用到数学知识,而且目前为止我们也没有得到严格的标准答案。有人在大量实验的基础上得出希尔排序的时间复杂度接近O(N^1.3)。

《数据结构(C语言版)》--- 严蔚敏

123.png

数据结构-用面相对象方法与C++描述》--- 殷人昆

@.png

因为我们的gap取值的方式是按照Knuth的方式取的,所以以我们这种方式实现的希尔排序时间复杂度暂定为O(N^1.25)。


目前仅仅靠时间复杂度的对比,我们也许感受不到什么叫做质的飞跃,那我们就通过一组数据来对比一下二者的差距。

这是一个测试排序算法所用时间的函数:

void TestOP()
{
  srand(time(0));
  const int N = 1000000;
  int* a1 = (int*)malloc(sizeof(int) * N);
  int* a2 = (int*)malloc(sizeof(int) * N);
  int* a3 = (int*)malloc(sizeof(int) * N);
  int* a4 = (int*)malloc(sizeof(int) * N);
  int* a5 = (int*)malloc(sizeof(int) * N);
  int* a6 = (int*)malloc(sizeof(int) * N);
  int* a7 = (int*)malloc(sizeof(int) * N);
  int j = 0;
  for (int i = 0; i < N; ++i)
  {
    a1[i] = rand();
    a2[i] = a1[i];
    a3[i] = a1[i];
    a4[i] = a1[i];
    a5[i] = a1[i];
    a6[i] = a1[i];
    a7[i] = a1[i];
  }
  printf("%d\n", j);
  int begin1 = clock();
  InsertSort(a1, N);
  int end1 = clock();
  int begin2 = clock();
  ShellSort(a2, N);
  int end2 = clock();
  int begin3 = clock();
  //SelectSort(a3, N);
  int end3 = clock();
  int begin4 = clock();
  //HeapSort(a4, N);
  int end4 = clock();
  int begin7 = clock();
  //BubbleSort(a7, N);
  int end7 = clock();
  int begin5 = clock();
  //QuickSort(a5, 0, N - 1);
  int end5 = clock();
  int begin6 = clock();
  //MergeSort(a6, N);
  int end6 = clock();
  printf("InsertSort:%d\n", end1 - begin1);
  printf("ShellSort:%d\n", end2 - begin2);
  printf("SelectSort:%d\n", end3 - begin3);
  printf("HeapSort:%d\n", end4 - begin4);
  printf("BubbleSort:%d\n", end7 - begin7);
  printf("QuickSort:%d\n", end5 - begin5);
  printf("MergeSort:%d\n", end6 - begin6);
  free(a1);
  free(a2);
  free(a3);
  free(a4);
  free(a5);
  free(a6);
}

我们以排序1百万个元素为例,得到以下结果:

#.png

很显然两种排序算法已经不在一个数量级了。


9.总结


懂得区分最好、平均、最坏情况,是为当前场景选择最优算法以及给现有算法调优以适应环境变化的关键。记住,虽然为最坏情况做好准备十分重要,但大部分时间我们面对的是平均情况。

目录
相关文章
|
3天前
|
弹性计算 人工智能 安全
云上十五年——「弹性计算十五周年」系列客户故事(第二期)
阿里云弹性计算十五年深耕,以第九代ECS g9i实例引领算力革新。携手海尔三翼鸟、小鹏汽车、微帧科技等企业,实现性能跃升与成本优化,赋能AI、物联网、智能驾驶等前沿场景,共绘云端增长新图景。
|
9天前
|
存储 弹性计算 人工智能
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
2025年9月24日,阿里云弹性计算团队多位产品、技术专家及服务器团队技术专家共同在【2025云栖大会】现场带来了《通用计算产品发布与行业实践》的专场论坛,本论坛聚焦弹性计算多款通用算力产品发布。同时,ECS云服务器安全能力、资源售卖模式、计算AI助手等用户体验关键环节也宣布升级,让用云更简单、更智能。海尔三翼鸟云服务负责人刘建锋先生作为特邀嘉宾,莅临现场分享了关于阿里云ECS g9i推动AIoT平台的场景落地实践。
【2025云栖精华内容】 打造持续领先,全球覆盖的澎湃算力底座——通用计算产品发布与行业实践专场回顾
|
8天前
|
人工智能 自然语言处理 自动驾驶
关于举办首届全国大学生“启真问智”人工智能模型&智能体大赛决赛的通知
关于举办首届全国大学生“启真问智”人工智能模型&智能体大赛决赛的通知
|
8天前
|
云安全 人工智能 自然语言处理
阿里云x硅基流动:AI安全护栏助力构建可信模型生态
阿里云AI安全护栏:大模型的“智能过滤系统”。
|
9天前
|
编解码 自然语言处理 文字识别
Qwen3-VL再添丁!4B/8B Dense模型开源,更轻量,仍强大
凌晨,Qwen3-VL系列再添新成员——Dense架构的Qwen3-VL-8B、Qwen3-VL-4B 模型,本地部署友好,并完整保留了Qwen3-VL的全部表现,评测指标表现优秀。
666 7
Qwen3-VL再添丁!4B/8B Dense模型开源,更轻量,仍强大
|
4天前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
|
11天前
|
存储 机器学习/深度学习 人工智能
大模型微调技术:LoRA原理与实践
本文深入解析大语言模型微调中的关键技术——低秩自适应(LoRA)。通过分析全参数微调的计算瓶颈,详细阐述LoRA的数学原理、实现机制和优势特点。文章包含完整的PyTorch实现代码、性能对比实验以及实际应用场景,为开发者提供高效微调大模型的实践指南。
791 2
|
2天前
|
编解码 文字识别 算法
一张图能装下“千言万语”?DeepSeek-OCR 用视觉压缩长文本,效率提升10倍!
一张图能装下“千言万语”?DeepSeek-OCR 用视觉压缩长文本,效率提升10倍!
346 10