【数据结构与算法】排序算法(上)——插入排序与选择排序
文章目录
- 一、常见的排序算法
- 二、插入排序
- 2.1、直接插入排序
- 2.2、希尔排序( 缩小增量排序 )
- 三、选择排序
- 3.1、直接选择排序
- 3.2、堆排序
- 3.2.1、堆排序的代码实现
一、常见的排序算法
常见排序算法中有四大排序算法,第一是插入排序,二是选择排序,三是交换排序,四是归并排序。本站文章针对前两个排序,这其中不才将拿出每个排序中所具有代表性的排序算法进行深入解读。
二、插入排序
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想
2.1、直接插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
当插入第i(i>=1)
个元素时,前面的array[0]
,array[1
],…
,array[i-1]
已经排好序,此时用array[i]
的排序码与array[i-1]
,array[i-2]
,…
的排序码顺序进行比较,找到插入位置即将array[i]
插入,原来位置上的元素顺序后移
代码实现:
void InsertSort(int* arr, int n) {// 外层循环,从第二个元素开始,逐步处理每个元素for (int i = 0; i < n - 1; i++) {// tmp 是当前元素的索引,初始值是 i+1,表示从第二个元素开始int tmp = i + 1;// end 是已排序部分的最后一个索引,初始值是 iint end = i;// 把当前比较的数据保护起来int num = arr[tmp];// 内层循环,寻找当前元素插入的位置while (end >= 0) {// 如果已排序部分的元素大于当前元素if (arr[end] > num) {// 已经把当前元素比较保护好,可以将已排序部分的元素向右移动arr[tmp] = arr[end];// tmp 和 end 都向左移一位tmp--;end--;}else {// 找到合适的位置,不需要继续向左移动了break;}}// 将当前元素插入到合适的位置arr[tmp] = num;}
}
时间复杂度:
- 最好的情况是:如果数组已经是有序的(或者几乎有序),只需要进行一轮比较,时间复杂度是O(n)。
- 最坏的情况是:数组是逆序的,每次都要比较所有已排序的元素,时间复杂度是O(n²)。
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
2.2、希尔排序( 缩小增量排序 )
希尔排序法又称缩小增量法。希尔排序法的基本思想是:
先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当
到达=1
时即为直接插入排序
,所有记录在统一组内排好序。
希尔排序的工作原理:
-
初始间隔(
gap
):希尔排序的关键点在于选择一个合适的“间隔”序列,也叫做增量序列。初始时,希尔排序会使用一个较大的间隔,比如n/2
,然后通过逐渐缩小间隔来进行多次排序。 -
分组插入排序:每次排序时,希尔排序将待排序的序列按间隔分成多个子序列。然后对每个子序列分别进行插入排序。例如:间隔为
2
时,序列的第一个元素、第三个元素、第五个元素、第七个元素、第九个元素,形成一个子序列(如下图gap=2
时)。然后对这个子序列进行插入排序。接着处理间隔更小的子序列,直到间隔为1时,整个序列就是有序的。 -
逐渐减小间隔:随着间隔的逐步减小,元素变得越来越接近排序完成,最后,当间隔为1时,希尔排序就变成了直接插入排序。
总的来说,希尔排序就是直接排序PRO MAX版本。使用希尔排序,它可以快速地把大数放在右边,小数放在左边。在快速区分大小数位置之后,就比原先的混乱顺序变得更有序,在直接插入排序
中,我们知道元素集合越接近有序,直接插入排序算法的时间效率越高,我们就不断的往有序的方向靠近,最后再直接在使用直接排序就可以缩短大部分时间。如下图,我们使用gap
来表示每一次比较的跨越元素个数。
代码的实现:(两种循环的实现)
void shellSort(int* arr, int n) {int gap = n / 3 + 1;while (gap > 1) {// 判断收缩gap的值,直到gap值为1时,完成插入排序//int gap = 3;//for (int j = 0; j < gap; j++) {//end要分别对gap分出的gap个数组进行排序,这样便可完成数组中每个位置的比较//for (int i = 0; i < n - 1 - gap; i += gap) { // 把间距把分开的每个tmp位置都进行插入排序for (int end = 0; end < n - gap; end++) { //这样设置循环可以把end在数组中的每个位置都走一边 与上面两层循环相比只是逻辑不同,效率上没有变化//int end = i + j;int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;}//}//}gap = gap / 3 + 1;}
}
- 排序代码的实现得有里到外的编写,这样容易把控
- 首先编写一个正常的直接插入排序,并且把
gap
加上。
for (int i = 0; i < n - 1; i++) {int gap = 1;int tmp = i + gap;int end = i;int num = arr[i + gap];while (end >= 0){if (arr[end] > num) {arr[tmp] = arr[end];tmp--;end--;}else {break;}}arr[tmp] = num;
}
- 这样我们就改好了直接插入排序有
gap
时的写法了 - 我们开始修改gap每次跳转的范围,首先以
gap=2
为例,首先我们把i
每次增加的个数都增加gap
个。我们也优化变量
int gap = 2;
for (int i = 0; i < n - 1 - gap; i += gap) {int end = i;int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;
}
- 这个时候,我们就完成了一个子序列:第一个元素、第三个元素、第五个元素、第七个元素、第九个元素的元素排序。但是,我们
gap = 2
时,我们是把原数组分为两个子序列。所以要对两个子序列都进行排序。这样我们就必须在外面再套一层循环来把gap
分开的子序列都进行排序。(如下图中被分开为红蓝两个子序列)
int gap = 2;
for (int j = 0; j < gap; j++) {for (int i = 0; i < n - 1 - gap; i += gap) {int end = i + j;int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;}
}
子序列
的第一个节点起始点,永远不会再第一个子序列的第二个节点的后面,所以我们可以通过套用外层循环j
遍历的控制end
的起始地址,则这样就可以完成多个子序列的访问。
- 这时候我们就可以完成所有子序列的第一次排序。但是为了完成原数组的整体排序,我们必须要让
gap
每完成一个排序就减少,直到gap = 1
时,变为直接插入排序完成数组的排序。
int gap = 2;
while (gap > 0) {for (int j = 0; j < gap; j++) {for (int i = 0; i < n - 1 - gap; i += gap) {int end = i + j;int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;}}gap--;
}
- 这样我们就完成了一个低效版本的希尔排序
为何说是低效版本呢?因为gap的值是固定的。当数据量达到数十亿的级别之后。我们一个区区的常量2
的效率与直接插入排序的效率几户一样。
这时候就有大佬研究出目前位置希尔排序gap
的最好取值之二(n
是数组的元素个数):gap = n/2
或gap = n/3 + 1
(最快)。我们再自己手搓希尔排序时,使用gap
选择哪个都可以。不才选择gap = n/3 + 1
作为示范。
int gap = n;
while (gap > 1) {gap = (gap / 3) + 1;for (int j = 0; j < gap; j++) {for (int i = 0; i < n - 1 - gap; i += gap) {int end = i + j;int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;}}
}
- 在使用
gap = n/3 + 1
之后,每次gap
缩小值都是gap/3 + 1
。无论是什么数循环到一定次数后最后除三的都会变为零。当除3等于0时再加一gap
就等于1,这时候就是直接插入排序。当gap == 1
时,就不会再进入循环。
但此时,上面循环中i、j
就只是为了控制end
变量起始位置可以遍历一遍数组,end
每次都是与gap
位后的数值进行比较。那么我们就可以把两层循环变为一层循环
void shellSort(int* arr, int n) {int gap = n;while (gap > 1) {// 再收缩gap的值,直到gap值为1时,完成插入排序gap = gap / 3 + 1;for (int end = 0; end < n - gap; end++) { //这样设置循环可以把end在数组中的每个位置都走一边,但效率上没有变化int tmp = arr[end + gap];while (end >= 0) { //把当前tmp插入到合适的位置if (arr[end] > tmp) {arr[end + gap] = arr[end];//每次都与前gap值为比较end -= gap;}else {break;}}arr[end + gap] = tmp;}}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书籍中给出的希尔排序的时间复杂度都不固定:
《数据结构-用面相对象方法与C++描述》— 殷人昆
不才上面使用的是Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,暂时就按照:O(n1.25)到O(1.6*n1.25)来算,按照我们也可以粗略的归类与O(n * logn)的量级,但是真实的时间复杂度是比O (n * logn)大。
三、选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
3.1、直接选择排序
- 在元素集合
array[i]--array[n-1]
中选择关键码最大(小)的数据元素 - 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
3.2、堆排序
在堆的的逻辑结构中是严格遵循有序但这并不意味着整个堆的物理存储结构是有序的。
堆排序的目的
是对堆中的元素进行排序,通过堆这种数据结构的特性来实现元素的排序。
排序中分为升序和降序,堆排序即利用堆的思想来进行排序
- 排升序对应着建大堆
- 排降序对应着建小堆
堆排序的方法:
- 因为堆排序的逻辑与堆的删除逻辑是完全一致的,都是先把堆顶元素与最后一个元素进行交换之后向下调整。与删除不同的是,删除需要把数组中最后一个元素完全删除,排序只需要不再理会数组最后一个元素,不用真正删除元素。
排升序建大堆的原因在把堆顶元素与最后一个元素进行交换
之后,堆中的中最大的值被放置在物理结构的最右边,如此循环即可完成结构的升序。降序同理
把堆中元素进行升序排序
我们使用上述大堆的例子创建有序的物理结构,物理结构:[95,70,8,21,5,3,4,6,9,1]
首先交换堆顶与最后一个元素(如下图)
在交换完成后逻辑结构上不再把95
结点当作堆的结点,之后进行向下调整(如下图)
此时,物理结构为:[70,21,8,9,5,3,4,6,1,95]
。这样就把最大值放置在物理结构最右边,并且忽略最后一个结点后,其他结点依旧保持着大堆结构。(与删除堆顶逻辑完全相同)
循环上述操作可得下图:
一定次数的循环后,会得到下图
观察上图可以看到此时物理结构:[8,6,3,1,5,4,9,21,70,95]
,只要循环次数足够,就可以把物理结构排为升序
最终可得下图:
此时我们就完成了:堆中元素的升序排序。物理结构为:[1,3,4,5,6,8,9,21,70,95]
。
3.2.1、堆排序的代码实现
void HeapSort(HPDataType* arr, int capacity, int farent) {assert(arr); // 确保传入的数组指针不为空int cp = capacity; // 存储堆的初始容量// 当堆中还有元素时进行排序while (cp != 0) {// 将堆顶元素(最大或最小元素)与当前堆的最后一个元素交换HeapSwap(arr, 0, cp - 1); // 减少堆的有效大小(去除已排序的最大元素)--cp;// 调整堆结构,确保堆的性质依然保持AdjustDown(arr, cp, farent);}
}
- 首先把堆顶元素与最后一个叶子节点的元素进行交换。
- 之后
--元素个数
,把已经交换完成的最大值(最小值)忽略。 - 完成后再向下调整。把交换完成后的顺序表,重新调整为大堆(小堆)。
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
ps:剩下的两大排序真正紧张制作中,欲知后事如何,请听下回分解~~
以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有帮助的话,就请多多为我点赞收藏吧~~~💖💖
ps:表情包来自网络,侵删🌹
相关文章:

【数据结构与算法】排序算法(上)——插入排序与选择排序
文章目录 一、常见的排序算法二、插入排序2.1、直接插入排序2.2、希尔排序( 缩小增量排序 ) 三、选择排序3.1、直接选择排序3.2、堆排序3.2.1、堆排序的代码实现 一、常见的排序算法 常见排序算法中有四大排序算法,第一是插入排序,二是选择排序ÿ…...

Linux操作系统性能优化
Linux操作系统性能优化 1. TCP连接出现大量ESTABLISHED连接解决方法 1. TCP连接出现大量ESTABLISHED连接解决方法 TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于…...

iOS与Windows间传文件
想用数据线从 windows 手提电脑传文件入 iPhone,有点迂回。 参考 [1],要在 windows 装 Apple Devices。装完、打开、插线之后会检测到手机,界面: 点左侧栏「文件」,不是就直接可以传,而是要通过某个应用传…...

在数据库设计中同步冗余字段的思考与实践
目录 前言1. 冗余字段设计的背景与场景1.1 场景描述1.2 冗余字段的必要性 2. 冗余字段设计的优点2.1 提高查询效率2.2 简化应用逻辑 3. 冗余字段设计的缺点与挑战3.1 数据不一致问题3.2 更新开销增加3.3 数据冗余占用存储空间 4. 如何同步更新冗余字段4.1 手动更新方式4.2 使用…...

Qt 带数据库功能的项目部署之后,数据库无法打开问题解决方法
前言:最近项目添加了sqlite数据库功能,在qtcreator直接运行时,打开数据库正常,但是部署之后,发现数据库打开会失败,提示“driver not loaded”错误,后来发现是因为sqldrivers文件夹目录不对导致…...

汇编语言学习-二
好吧,已经隔了两天,下完班看了两天,在电脑上装了虚拟机版的MS_DOS,主要是怕折腾坏我的电脑系统; 这个第二天应该是称为第二章更为合适,目前第二章已经看完,基本的命令也是敲了敲; 下面就进行一…...

【嘟嘟早教卡】 小程序源码分享带后台管理
【嘟嘟早教卡】是专门为 3-6 岁婴幼儿童学习普通话、英语研发的早教启蒙认知识字的小程序 小程序由 Taro 及 Tailwind CSS 构建而成,后台管理使用 Laravel 及 Tailwind CSS 想法源于小时候玩的认知卡片,基本大部分家庭都买过认知卡片,我按照…...

JavaEE-经典多线程样例
文章目录 单例模式设计模式初步引入为何存在单例模式饿汉式单例模式饿汉式缺陷以及是否线程安全懒汉式单例模式基础懒汉式缺陷以及是否线程安全懒汉式单例模式的改进完整代码(变量volatile) 阻塞队列生产者消费者模型生产者消费者模型的案例以及优点请求与响应案例解耦合削峰填…...

从 HTML 到 CSS:开启网页样式之旅(五)—— CSS盒子模型
从 HTML 到 CSS:开启网页样式之旅(五)—— CSS盒子模型 前言一、盒子模型的组成margin(外边距):border(边框):padding(内边距):conten…...

数据分析(一): 掌握STDF 掌握金钥匙-码农切入半导体的捷径
中国的半导体行业必然崛起!看清这个大势,就会有很多机会。 今天,我们一起来了解一下半导体行业的一朵金花:STDF。 实际上这只是一种文件格式,但是当你熟练掌握解析这种文件的时候,你就已经打开在这个基础…...

HCIA-openGauss_1_4基本功能介绍
openGauss支持标准SQL SQL是用于访问和处理数据库的标准计算机语言,SQL标准的定义分成核心特性以及可选特性,绝大部分的数据库都没有100%支撑SQL标准。openGuass支持SQL2003标准语法,支持主备部署的高性能可用关系型数据库。openGauss数据库…...

医学临床机器学习中算法公平性与偏差控制简析
摘要 随着医疗领域中数据的不断积累和计算能力的提升,临床机器学习技术发展迅速,但算法不公平性和偏差问题凸显。本文深入探讨了临床机器学习算法公平性的重要性、概念与定义、在临床应用中的影响、偏差来源、降低偏差方法及提升公平性策略。通过对不同…...

Leetcode打卡:棋盘上有效移动组合的数目
执行结果:通过 题目:2056 棋盘上有效移动组合的数目 有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的…...

生产看板到底在看什么?
说起生产看板,可能很多人脑海里冒出来的画面是:车间里一块挂在墙上的大板子,上面贴满了各式各样的卡片、表格,甚至还有几个闪闪发光的指示灯。但是,无论是精益生产方式代表——丰田,还是当下以“智能制造”…...

12,攻防世界simple_php
simple_php 题目来源:Cyberpeace-n3k0 题目描述: 小宁听说php是最好的语言,于是她简单学习之后写了几行php代码。 进入靶场 这段PHP代码是一个简单的web应用示例,让我们逐步分析这段代码: show_source(__FILE__);:这行代码会显示当前文件的…...

解决Jupyter Notebook无法转化为Pdf的问题(基于Typora非常实用)
笔者在完成各项作业和做笔记时,经常用到jupyter notebook;其因为可以同时运行python并提供格式化的数字公式的输入方式,得到了广大用户的喜爱。 当我们想要将.ipynb文件导出为pdf时,有两种常用方法。 1.Ctrlp 2.通过File ->…...

齐护机器人ModbusRTU RS485转TTL通信模块与ESP32 Arduino通信可Mixly的图形化编程Scratch图形化编程
齐护机器人ModbusRTU RS485-TTL通信模块 一、概念理解 Modbus协议是一种由Modicon公司(现为施耐德电气Schneider Electric)于1979年发表的网络通信协议,旨在实现可编辑逻辑控制器(PLC)之间的通信。 1.1 什么是Mod…...

python学习笔记15 python中的类
上一篇我们介绍了python中的库 ,学习了一些常见的内置库。详细内容可点击–>python学习笔记14 python中的库,常见的内置库(random、hashlib、json、时间、os) 这一篇我们来看一下python中的类 创建一个类 class 类的名称():de…...

PMP–一、二、三模、冲刺–分类–10.沟通管理
文章目录 技巧十、沟通管理 一模10.沟通管理--1.规划沟通管理--文化意识--军事背景和非军事背景人员有文化差异5、 [单选] 项目团队由前军事和非军事小组成员组成。没有军事背景的团队成员认为前军事团队成员在他们的项目方法中过于结构化和僵化。前军事成员认为其他团队成员更…...

android-studio开发第一个项目,并在设备上调试
恭喜你成功安装并配置好了 Android Studio!下面是开发你的第一个 Android 项目并在设备上调试的详细步骤: 1. 启动 Android Studio 首先,启动 Android Studio。你可以通过以下几种方式启动: 使用桌面快捷方式(如果已…...

springboot/ssm线上教育培训办公系统Java代码web项目在线课程作业源码
springboot/ssm线上教育培训办公系统Java代码web项目在线课程作业源码 基于springboot(可改ssm)htmlvue项目 开发语言:Java 框架:springboot/可改ssm vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库&…...

Spring 依赖 详解
Spring 依赖详解 在 Spring 框架中,依赖 是指一个对象(Bean)需要另一个对象(Bean)来完成其功能的情况。Spring 通过 依赖注入(Dependency Injection, DI) 和 控制反转(Inversion of…...

千益畅行,旅游卡有些什么优势?
千益畅行共享旅游卡是一种创新的旅游服务模式,旨在通过整合各类旅游资源,为用户提供一站式的旅游解决方案。这张旅游卡支持2至6人同行,涵盖了接机、酒店、用餐、大巴、导游、景区门票等服务,用户只需自行承担往返交通费用即可享受…...

Ubuntu24 cgroupv2导致rancher(k3s)启动失败的处理
方案一: 修改系统镜像为ubuntu18 方案二: 修改当前系统的cgroup版本,由v2改成v1 修改步骤: 1、查看当前cgroup版本 stat -fc %T /sys/fs/cgroup cgroup v2,输出结果为cgroup2fs cgroup v1,输出为tm…...

学习CSS第二天
学习文章目录 一.内部样式 一.内部样式 写在 html 页面内部,将所有的 CSS 代码提取出来,单独放在 <style> 标签中 语法: <style> h1 { color: red; font-size: 40px; } </style>注意点: <style> 标签理…...

2021数学分析【南昌大学】
2021 数学分析 求极限 lim n → ∞ 1 n ( n + 1 ) ( n + 2 ) ⋯ ( n + n ) n \lim_{n \to \infty} \frac{1}{n} \sqrt [n]{(n+1)(n+2) \cdots (n+n)} n→∞limn1n(n+1)(n+2)⋯(n+n) lim n → ∞ 1 n ( n + 1 ) ( n + 2 ) ⋯ ( n + n ) n = lim n → ∞ ( n + …...

单端和差分信号的接线法
内容来源:【单端信号 差分信号与数据采集卡的【RSE】【 NRES】【 DIFF】 模式的连接】 此篇文章仅作笔记分享。 单端输入 单端信号指的是输入信号由一个参考端和一个信号端构成,参考端一般是地端,信号就是通过计算信号端口和地端的差值所得…...

力扣-图论-2【算法学习day.52】
前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程(例如想要掌握基础用法,该刷哪些题?)我的解析也不会做的非常详细,只会提供思路和一些关键点,力扣上的大佬们的题解质量是非…...

MySQL如何区分幻读和不可重复读
在MySQL中,幻读和不可重复读都是并发事务中可能出现的问题,但它们的表现和原因略有不同。 不可重复读 (Non-Repeatable Read) 不可重复读是指在同一个事务内,多次读取同一行数据时,可能会得到不同的结果。这种情况发生在一个事务…...

界面控件Syncfusion Essential Studio®现在已完全支持 .NET 9
Syncfusion Essential Studio现在完全支持 .NET 9,可最新版本2024 Volume 3 版本中使用!通过此更新,Blazor、.NET MAUI、WPF、WinForms、WinUI和ASP.NET Core 平台中的 Syncfusion 组件以及文档处理库已准备好让您利用 .NET 9 中的最新功能。…...