【初阶数据结构和算法】八大排序算法之插入排序(直接插入排序、希尔排序及其对比)

在这里插入图片描述

文章目录

  • 一、常见排序算法分类
  • 一、直接插入排序
  • 二、希尔排序
  • 三、直接插入排序和希尔排序性能对比

一、常见排序算法分类

   常见的排序算法有八种,我们简单盘点一下

  1. 插入排序:直接插入排序、希尔排序
  2. 选择排序:直接选择排序、堆排序
  3. 交换排序:冒泡排序、快排
  4. 希尔排序
  5. 计数排序

   以上就是我们常用的八大排序算法,我们会在后面的排序算法部分为大家一一介绍,今天我们要介绍的就是插入排序这一大类排序(更新)

一、直接插入排序

   我们先来简单地介绍一下直接插入排序的思想:
   直接插⼊排序是⼀种简单的插⼊排序法,其基本思想是:把待排序的记录按其关键码值的⼤小逐个插⼊到⼀个已经排好序的有序序列中,直到所有的记录插⼊完为⽌,得到⼀个新的有序序列,我们举一个简单的例子,如图:
在这里插入图片描述
   在我们玩扑克牌时,拿到牌后首先就是对牌进行排序,我们会将已经排好序的牌放在左手边,然后右边就是我们等待排序的牌
   随后我们会将右边没有排好序的牌依次去插入到已经有序的牌中,如上图,2,4,5,10就是一组已经排好序的牌,而7我们就可以看作是我们打牌时放在右边的还未排序的牌,我们想把7排好序,就只需要将7插入到5和10的中间,其它牌不变
   这也正是我们直接插入排序的算法思想,将所有的数据分成两部分,左边就是已经排好序的数据,右边则是待排序数据,就是我们上面扑克牌的思想,随后我们将右边待排序的数一个一个插入到有序的数据中,直到最后一个数据插入完成,所有数据也就有序了
   现在问题的关键就是,在最开始时我们所有的数据都是乱序,怎么才能将它划分成左右两部分,实际上很简单,只要我们把第一个数据看作一个有序的序列即可,这样左右两部分就被划分出来了,如图:
在这里插入图片描述
   这样我们就将最开始的数据划分成了两部分,左边只有一个数据,可以看作是有序的,右边是乱序的、待排序的数据,接下来我们要做的就是如何将右边一个一个的元素依次插入到左边的有序序列中
   这也是我们直接插入排序的重点,上面我们讲解了直接插入排序的原理,接下来我们就开始介绍直接插入排序的具体实现,我们以升序为例讲解
   首先我们可以创建一个变量end作为左边有序序列中的最后一个数据,在最开始时,end就在上图8这个位置上,因为8就是有序的序列的最后一个数据,随后我们要做的就是将3插入到这个有序序列
   方法就是,记录下来end+1位置的数据,因为end是左边有序的序列的最后一个数据,那么end+1自然就是右边待排序序列的第一个数据,我们要将end+1这个位置的数据插入到左边的有序序列中,首先就是将它用变量tmp记录下来,它就是我们要往左边有效序列中插入的数据
   接下来我们就开始比较end位置的数据和tmp这个数据,看看end位置的数据是否比tmp更大,如果更大,说明我们的tmp要往end位置的左边插入数据,于是我们就可以让end位置的数据往后挪动一下,如图:
在这里插入图片描述
   此时就可以看到,我们成功将右边的一个数据3插入到了左边的有序序列中,现在这些数据重新被我用绿色框划分成了两部分,左边是新的有序序列,而右边而是新的待排序数据,接下来我们要继续上面我们画出的操作
   这里我就不再画图演示了,我们这时候要再来讨论一个问题,是不是每次只需要end位置的数据和tmp比较一次就够了,还是说整个过程是循环进行的,很明显这个过程是循环进行的,那么为什么我们刚刚只比较了一次呢?
   因为我们刚刚的有序序列只有一个元素,自然就只需要比较一次了,如果有序序列中有多个元素,那么tmp就要一直去找比它小的元素,如果碰到比它大的数据就让它往后挪动,也可以参考我们上面的图理解
   如果一直遇到的都是比tmp大的数据,那么end就会一直- -,到最后就跟上图一样,end直接越界了,我们就需要直接结束循环,将tmp放到end+1的位置上
   然后根据上面我们说的思路,我们也会发现一个规律,就是第一次的end指向第一个元素,第二次的end指向第二个元素,因为经过第一次的排序,将右边的一个待排序数据成功插入到了左边,所以我们第二次的end就指向第二个元素(end就是有序序列的最后一个数据,不要忘记了)
   那么相当于就是这个end会从第一个数据开始,遍历所有数据,也就是会遍历我们要排序的数组,这里我们就需要一个for循环完成
   然后每一个end的比较又是一个循环,循环条件就是end >= 0,只要end没有越界,并且tmp的值比end位置的值小,就会一直将大的数据往后挪,end要- -,直到end越界,再将tmp放到end+1的位置
   当然,并不是所有情况都会越界,如果中途碰到了比tmp小的值,那么就直接退出循环,也是将tmp放到end+1的位置,可以根据上面的例子自己画图模拟一下
   那么以上就是直接插入排序的所有算法思路以及实现方法,接着我们就按照刚刚将的内容将直接插入排序写出来,如下:

//直接插入排序
void InsertSort(int* arr, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;}}arr[end + 1] = tmp;}
}

   这里我们要注意的是i的范围,i也就是end,它到n-2位置时,就会跟n-1位置进行比较,所以i只需要取0到n-2即可,不明白的可以自己试试写成i < n,然后自己调试看看,会发现当i等于n-1时,会将n-1位置的数据和n位置的数据作比较,会越界
   那么说完最后的注意事项之后,我们拿它来排序试试,如图:
在这里插入图片描述
   可以看到直接插入排序成功帮我们完成了排序任务,接下来我们来分析一下直接插入排序的时间复杂度,由于它有两层循环,外层循环一直都是n,内层循环最坏情况下也是n,所以最后直接插入排序的时间复杂度大致为O(N^2)

二、希尔排序

   希尔排序是直接插入排序的优化排序,也属于插入排序,我们上面学习的直接插入排序的时间复杂度为O(N^2),并不理想,至于原因我们可以文末测试给大家看看
   首先我们先来分析分析为什么直接插入排序很慢,其中一个很重要的原因就是,在我们将右边待排序的数据插入左边排好序的数据时,很可能出现右边的数据出现较大或较小的数据
   我们就以升序为例,如果右边的数据都是较小的数据,那么在往左边插入数据的时候,就会造成左边数据的多次挪动,导致性能下降,除非数据源本来就接近或稍微有序,那么我们数据的挪动就变少了,一下就能将数据插入到左边,效率就高了
   哎~,确实有道理,如果我们在执行直接插入排序前,让数组接近有序,就会让排序的速度快很多,这就是希尔排序优化的基本思路,那么它是怎么做到的呢?我们一起来学习一下
   希尔排序的思路就是通过“分组”排序进行优化,通过分组对数据源进行预处理,就是分别将每组的数据排成有序,这样将能将数据从杂乱无章,变得接近或稍微有序,然后再去进行一次直接插入排序,就可以让数据有序
   首先我们对数组进行分组,分组不是挨着的数据为一组,而是跳着跳着进行分组,我们可以简单画个图,如下:
在这里插入图片描述
   在上图中,我们将数据分成了两组,红色为一组,绿色为一组,每个组元素之间的距离为2,在分组时并没有选择挨着的数据为一组,这是为什么呢?
   如果我们将挨着的数据划为一组,那么各组排完序之后一定会出现这样的问题,如图:
在这里插入图片描述
   这样挨着区域划分,我们排序后,也只是局部有序,但是我们要从全局去看呀,我们希望经过预排序后,也就是每个组排序之后,整个数组看起来接近有序,上面的划分可以一下就看出来,局部是有序的,但是从整体的角度看起来,这些数据还是杂乱无章的
   那么如果我们分开划分呢?我们来看看分开划分的效果,如图:
在这里插入图片描述
   首先我们从肉眼直接进行观察,我们发现这样分组排序后,居然真的能让数据往整体有序进行挪动,这样的原理是什么呢?
   这也不难想到,就是因为每组的数据都是分开的,但是组与组之间是相邻的,那么每组在排序时,会将自己较小的数据往前放,由因为组与组之间相邻,最后就能将每组最小的数据相邻,并且出现在所有数据的前面
   又由于每组在排序时,会将自己较大的数据往后放,并且组与组之间相邻,那么就能让每组中较大的数据相邻,并且出现在所有数据的后面,这样综合下来,就能实现简单的对所有数据进行预排序,让整体看起来接近有序
   如果实在不理解也没关系,至少从我们画的图上面来看,很容易发现将分开的数据划分在一起更好,知道这一点就好了,关键在于我们要知道如何实现预排序
   这里我们就不卖关子了,直接开始介绍如何实现预排序,预排序分成很多步,因为如果数据量很大,那么我们无论如何分组,最后都很难让数组看起来有序,所以预排序本身会进行多次,也就是会分多次组进行排序
   那么每一次分组我们怎么划分呢?划分成多少组呢?经过一些大佬的研究发现,每次让gap除3+1就能将组分得更加合理,gap最初就是所有数据的个数,比如上图中一共有5个数据,那么它/3+1之后就变成了2,正好是两组,也可以说是每组中各个数据之间的间隔
   这里稍微提一下,分成多少组,那么每个数据就会间隔多少,这个是常识性的知识,就简单提一下,分多少组意味着每组间隔多少,注意是间隔,不是数据个数
   那么如果一下有1万条数据呢?gap/3+1后就是3千多,我们要将数据划分成3千多组,每组之间的元素间隔3千多的距离,每组则是有3个数据
   经过一次这样的分组排序,并不能保证整个数组看起来有序,我们要对它排序后的结果再次进行分组,继续让gap/3+1进行分组,然后对每组进行排序
   直到最后一步gap/3+1后为1了,此时每组之间的间隔为1,相当于没有分组,此时会进行一次最终的排序,将所有数据排成有序,这样我们的希尔排序就完成了
   最后我们总结一下希尔排序的思路,定义一个gap,默认为数组元素大小,让它循环地去分组,每次分组就是让gap/3+1,每分一次组就对每组进行一次排序,每组的排序就和直接插入排序的思路差不多,这样循环下去,直到gap == 1
   因为gap == 1后gap/3+1就为1了,然后我们会发现这时候一直/3+1会造成死循环,所以gap的循环条件为gap > 1,代码如下:

//希尔排序
void ShellSort(int* arr, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}
}

   我们来使用希尔排序简单排序一下一个数组,看看是否无误,如下:
在这里插入图片描述
   我们来简单谈谈希尔排序的时间复杂度,它看起来和直接插入排序很像,但是外层还有一个循环,难道是O(N^3)吗,比插入排序还糟糕
   其实不是,它是直接插入排序的优化,自然会比直接插入排序快,只是它套了三层循环比较具有迷惑性,希尔排序的时间复杂度很难直接计算,因为它的排序会根据gap的改变而改变,我们来看看《数据结构(C语⾔版)》—严蔚敏老师书中给出的时间复杂度为:
在这里插入图片描述
   在书中也明确谈到了希尔排序的时间复杂度很难计算,但是我们也要知道它的时间复杂度大致为O(N^1.3),比起直接插入排序要快很多
   但是其实很多人包括我,都不对这个有着三层循环的排序算法抱有期待,看起来根本差距不了多少,所以我们接下来进入测试阶段,一起来感受希尔排序带来的震撼

三、直接插入排序和希尔排序性能对比

   首先我们来对比一下直接插入排序和希尔排序的时间复杂度,如下:

  • 直接插入排序:O(N ^ 2)
  • 希尔排序:O(N ^ 1.3)

   看起来相差不远,接着我们来写一个测试程序来生成10万条数据,分别用它们进行排序,测试程序如下:

void TestOP()
{srand((unsigned int)time(NULL));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];}int begin1 = clock();InsertSort(a1, N);int end1 = clock();int begin2 = clock();ShellSort(a2, N);int end2 = clock();printf("InsertSort:%d\n毫秒", end1 - begin1);printf("ShellSort:%d\n毫秒", end2 - begin2);free(a1);free(a2);
}

   接着我们在主函数中调用这个函数,注意,要将VS调整到Release版本,这样才能测试出它们真正的性能,运行结果如下:
在这里插入图片描述
   可以看到在同一台设备上,排序10万条数据直接插入排序耗时598毫秒,也就是差不多0.6秒,而希尔排序只有0.006秒,如果你觉得,它们两个的差距也不远啊,反正都等不了多久,那是因为我们排序的数据量还是太小了
   我们将测试程序中的N改为100万,让程序生成100万个随机数用来排序,如下:

    const int N = 1000000;

   我们重新运行程序,看看程序的结果:
在这里插入图片描述
   这时我们惊讶地发现,排序100个随机数时,直接插入排序所耗的时间增长了100倍,来到了60秒,而希尔排序的时间只是增长了10倍,还是在0.1秒之内排好了100万个随机数
   60秒和0.1秒,相信大家都清楚之间的差距,但是如果我们要排序的数据更多,它们之间的差距只会更大,具体原因就是O(N^2)这个时间复杂度实在太差了,每增长10倍数据量,时间会增长差不多100倍
   所以我们说希尔排序已经是非常优秀的排序算法了,和其余几个O(N * logN)的排序算法差不多,也让我们见识到了直接插入排序和希尔排序的差距,一个三层循环的排序算法如此快,有没有震惊你呢?

   那么今天的插入排序的介绍就到这里啦,有什么问题欢迎私信我,后面我们继续讲解排序算法,感谢观看
   bye~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/13072.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型综述一镜到底(全文八万字) ——《Large Language Models: A Survey》

论文链接&#xff1a;https://arxiv.org/abs/2402.06196 摘要&#xff1a;自2022年11月ChatGPT发布以来&#xff0c;大语言模型&#xff08;LLMs&#xff09;因其在广泛的自然语言任务上的强大性能而备受关注。正如缩放定律所预测的那样&#xff0c;大语言模型通过在大量文本数…

4种架构的定义和关联

文章目录 **1. 各架构的定义****业务架构&#xff08;Business Architecture&#xff09;****应用架构&#xff08;Application Architecture&#xff09;****数据架构&#xff08;Data Architecture&#xff09;****技术架构&#xff08;Technology Architecture&#xff09;*…

实时波形与频谱分析———傅立叶变换

实时波形与频谱分析&#xff1a;一个交互式动画演示 在信号处理领域&#xff0c;时域波形和频域频谱是理解信号特性的重要工具。通过时域波形&#xff0c;我们可以直观地观察信号随时间的变化&#xff0c;而频域频谱则揭示了信号中所包含的频率成分及其幅值。为了帮助大家更好…

数据结构:时间复杂度

文章目录 为什么需要时间复杂度分析&#xff1f;一、大O表示法&#xff1a;复杂度的语言1.1 什么是大O&#xff1f;1.2 常见复杂度速查表 二、实战分析&#xff1a;解剖C语言代码2.1 循环结构的三重境界单层循环&#xff1a;线性时间双重循环&#xff1a;平方时间动态边界循环&…

基于Springboot+vue的租车网站系统

基于SpringbootVue的租车网站系统是一个现代化的在线租车平台&#xff0c;它结合了Springboot的后端开发能力和Vue的前端交互优势&#xff0c;为用户和汽车租赁公司提供了一个高效、便捷、易用的租车体验和管理工具。以下是对该系统的详细介绍&#xff1a; 一、系统架构 后…

[x86 ubuntu22.04]进入S4失败

目录 1 问题描述 2 解决过程 2.1 查看内核日志 2.2 新建一个交换分区 2.3 指定交换分区的位置 1 问题描述 CPU&#xff1a;G6900E OS&#xff1a;ubuntu22.04 Kernel&#xff1a;6.8.0-49-generic 使用“echo disk > /sys/power/state”命令进入 S4&#xff0c;但是无法…

Java 大视界 -- Java 大数据在智慧文旅中的应用与体验优化(74)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

使用Python和TensorFlow/Keras构建一个简单的CNN模型来识别手写数字

一个简单的图像识别项目代码示例,使用Python和TensorFlow/Keras库来训练一个基本的CNN模型,用于识别MNIST手写数字数据集,并将测试结果输出到HTML。 代码运行效果截图: 具体操作步骤: 1. 安装所需的库 首先,确保你已经安装了所需的Python库: pip install tensorflow…

Redis --- 使用zset处理排行榜和计数问题

在处理计数业务时&#xff0c;我们一般会使用一个数据结构&#xff0c;既是集合又可以保证唯一性&#xff0c;所以我们会选择Redis中的set集合&#xff1a; 业务逻辑&#xff1a; 用户点击点赞按钮&#xff0c;需要再set集合内判断是否已点赞&#xff0c;未点赞则需要将点赞数1…

kamailio-osp模块

该文档详细讲解了如何在Kamailio中配置和使用OSP模块&#xff08;Open Settlement Protocol Module&#xff09;&#xff0c;以实现基于ETSI标准的安全多边对等互联&#xff08;Secure Multi-Lateral Peering&#xff09;。以下是核心内容的总结&#xff1a; 1. 模块功能 OSP模…

北大AGI与具身智能评估新范式!Tong测试:基于动态具身物理和社会互动的评估标准

作者&#xff1a;Yujia Peng, Jiaheng Han, Zhenliang Zhang, Lifeng Fan, Tengyu Liu, Siyuan Qi, Xue Feng, Yuxi Ma, Yizhou Wang, Song-Chun Zhu 单位&#xff1a;北京通用人工智能研究院国家通用人工智能重点实验室&#xff0c;北京大学人工智能研究所&#xff0c;北京大…

开发板上Qt运行的环境变量的三条设置语句的详解

在终端中运行下面三句命令用于配置开发板上Qt运行的环境变量&#xff1a; export QT_QPA_GENERIC_PLUGINStslib:/dev/input/event1 export QT_QPA_PLATFORMlinuxfb:fb/dev/fb0 export QT_QPA_FONTDIR/usr/lib/fonts/设置成功后可以用下面的语句检查设置成功没有 echo $QT_QPA…

一文讲解Spring如何解决循环依赖

Spring 通过三级缓存机制来解决循环依赖&#xff1a; 一级缓存&#xff1a;存放完全初始化好的单例 Bean。 二级缓存&#xff1a;存放正在创建但未完全初始化的 Bean 实例。 三级缓存&#xff1a;存放 Bean 工厂对象&#xff0c;用于提前暴露 Bean。 试问:三级缓存解决循环依…

Linux+Docer 容器化部署之 Shell 语法入门篇 【Shell 替代】

&#x1f380;&#x1f380;Shell语法入门篇 系列篇 &#x1f380;&#x1f380; LinuxDocer 容器化部署之 Shell 语法入门篇 【准备阶段】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell变量】LinuxDocer 容器化部署之 Shell 语法入门篇 【Shell数组与函数】LinuxDocer 容…

[c语言日寄]赋值操作对内存的影响

【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋&#xff1a;这是一个专注于C语言刷题的专栏&#xff0c;精选题目&#xff0c;搭配详细题解、拓展算法。从基础语法到复杂算法&#xff0c;题目涉及的知识点全面覆盖&#xff0c;助力你系统提升。无论你是初学者&#xff0c;还是…

HTML5 教程之标签(3)

HTML5 <center> 标签 (已废弃) 定义和用法 <center> 标签对其包围的文本进行水平居中处理。HTML5不支持使用<center>标签&#xff0c;因此有关该标签的更多信息&#xff0c;请参考“HTML <center>标签”部分&#xff01; 示例: <center>这个…

SQL 秒变 ER 图 sql转er图

&#x1f680;SQL 秒变 ER 图&#xff0c;校园小助手神了&#xff01; 学数据库的宝子们集合&#x1f64b;‍♀️ 是不是每次碰到 SQL 转 ER 图就头皮发麻&#xff1f;看着密密麻麻的代码&#xff0c;脑子直接死机&#xff0c;好不容易理清一点头绪&#xff0c;又被复杂的表关…

大语言模型轻量化:知识蒸馏的范式迁移与工程实践

大语言模型轻量化&#xff1a;知识蒸馏的范式迁移与工程实践 &#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 摘要 在大型语言模型&#xff…

RabbitMQ:python基础调用

前言 紧接上回在windows上安装了最新版的RabbitMQ&#xff1a; RabbitMQ&#xff1a;windows最新版本4.0.5安装方案-CSDN博客 这是官方给出的使用文档&#xff1a;How to Use RabbitMQ | RabbitMQ 这里我给出通过AI学习到的python使用方法 理论截图 python直接使用pip安装pi…

【多线程】线程池核心数到底如何配置?

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. 前置回顾2. 动态线程池2.1 JMX 的介绍2.1.1 MBeans 介绍 2.2 使用 JMX jconsole 实现动态修改线程池2.2.…