希尔排序/选择排序
前言:
本篇主要对常见的排序算法进行简要分析,代码中均以数组 arr[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 } 为例,进行升序排列。
常见的排序算法有如下:

选择排序中,直接选择排序没有任何实际与教育意义,而堆排序在先前文章中有提及,不在考虑。
1:插入排序
1.1 :直接插入排序
1.1.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; }
}
1.1.2:图例分析上述代码
首次进入 for 循环时,如图一所示:
注:end 对应数组下标,tmp 对应数组元素

图一
此时 end = 0,进入 while 循环;此时 arr[end] = 5 ,tmp = 3 ,满足 if 条件,将数组第一个元素的值赋值给第二个元素,end-- 后为 -1,不满足 while 循环条件,结束while 循环。
当第一次跳出while循环时,此时数组中的元素如图二

图二
此时并没有得到我们想要的数组,下标 0和1 的元素重复 ,我们需要将 tmp 的值传递到 下标0 处,而通过 end+1 便可以访问 下标0处,因此 arr[end + 1] = tmp; 该条语句的目的就是为了得到正确排列的数组。
第二次进入 for 循环时,如图三所示:

图三:
此时 end = i = 1,而 tmp = 9,进入while循环后,不满足 if 条件,因此直接结束。
第三次进入for循环时,如图四所示:

图四:
此时 end = i = 2 ,而 tmp = 6,进入while循环后,满足 if 条件,元素 6 会被 9 替代,而 end-- 指向前一个位置处,此时如图五所示

图五:
此时end = 1 再次进入while循环时,此时不在满足 if 条件 ,结束 while 循环,通过最后一步 end+1,我们可以访问被代替元素的前一个元素的下标位置处,即下标2处,再将 tmp 赋值给 arr[end+1]即arr[2]我们可以得到正确的排列如图六:

图六
第四次进入循环时,如图七所示:

图七:
此时 end = 3,tmp = 2,进入循环后,因为 2 小于 前面所有元素,因此不断循环直至end = -1,如图八所示:

图八:
再次将tmp的值赋值给 arr[end+1] 便可以得到正确的排序。
1.1.3:直接插入排序的特点
①、元素越接近有序,直接插入排序算法的时间效率越高
②、时间复杂度为O(N^2)
③、空间复杂度为O(1)
1.2:希尔排序
1.2.1:思路
希尔排序是在直接插入排序的基础进行的优化,前面所说,元素越接近有序,直接插入排序算法的时间效率越高,希尔排序正是按着这个特性,先尽可能的让数组元素有序,再进行排序。
因此希尔排序的思路为:
先将数组内的元素按间隔排序,这个间隔一般为 n/3+1,n为数组内元素个数,并且每排序完一次,gap = gap /3 +1 再按新的gap再次排序,直至 gap为1时,此时就变成了直接插入排序,但这是的数组已接近有序,因此时间复杂度会大大降低。
1.2.2:代码:
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;}}}
}
分析:
在直接插入排序的基础上,在外层又嵌套了一层while循环,这个while循环就是用来对gap进行限制,当gap较大时,此时每个元素间的间距(gap)比较大,当gap = 4 时,如下图

当外层while第一次循环结束时,如图所示,此时较原来相比,已接近有序,此时 gap = 2 再次循环。

当外层while第二次循环结束时,如图所示,数组元素变得更加有序。

最后当gap = 1时,此时就为直接插入排序。
1.2.3:希尔排序的特点:
时间复杂度 O(N^1.3)
2:交换排序
2.1:冒泡排序
2.1.1:代码
void BubbleSort(int* arr, int n)
{for (int i = 0; i < n; i++){int exchange = 0;for (int j = 0; j < n - i - 1; j++){//升序if (arr[j] < arr[j + 1]){exchange = 1;swap(&arr[j], &arr[j + 1]);}}if (exchange == 0){break;}}
}
分析:
有着一定的教育意义,能够让初学者初步熟悉代码,时间复杂度为O(N^2),在有序的情况下,时间复杂度为O(N);
2.2:快速排序
快速排序一共有四种实现方式,前三种实现方式都是基于递归的思想,在找基准值上存在着区别,而第四种方法是借助堆,通过循环模拟递归的思想来实现。
2.2.1:基于递归思想实现快速排序
先前说到,递归实现快速排序只在找基准值的方法上存在区别,那么什么是基准值?我们所要找的基准值,以升序为例,就要是在数组中找到这样一个位置,他的左边的元素都小于它,右边的元素都大于它,这个元素对应的下标大小就为基准值
2.2.1.1:hoare法
找基准值的代码:
int _QuickSort3(int* arr, int left, int right)
{int keyi = left;left++;while (left <= right){while (left <= right && arr[right] > arr[keyi]){right--;}//while循环结束时,此时right下标处对应为较小元素while (left <= right && arr[left] < arr[keyi]){left++;}//while循环结束时,此时left下标处对应为较大元素if (left <= right)//当满足条件时,将left 和 right 对应下标的元素交换,得到相对有序数组{swap(&arr[left++], &arr[right--]);}}//上述循环结束时,此时right对应较小元素swap(&arr[keyi], &arr[right]);//将假设值与较小值交换return right;//返回基准值
}
图例分析:
未进入第一个while循环前,各变量对应关系如图一所示,以keyi作为参考值

图一:
当进入外层while循环后,right开始向右找较小值,left开始向左找较大值,当内层的两个while循环结束时,此时各变量对应关系如图二所示:

图二:
此时 left < right 交换两者下标对应元素后 left++,right--,各变量对应关系如图三所示:

图三:
此时left和while仍满足外层while循环条件,继续重复上述步骤直至如图四所示。

图四:
此时已经跳出外层循环,将 keyi 与 right 对应下标元素交换后,此时 right 左侧元素均小于 5,右侧元素均大于5,而right就是我们所找的基准值。
递归实现部分代码:
void QuickSort3(int* arr, int left, int right)
{if (left >= right){return;}int key = _QuickSort3(arr, left, right);QuickSort3(arr, left, key - 1);QuickSort3(arr, key+1, right);
}
递归过程:

对于初学者而言,在分析过程中容易忽略 left值 和 right值 的变化,在上述递归的过程中,可以看到,key的改变会影响下次递归 left 和 right 的值,正是 left 和 right 改变,才能使递归满足停止条件,从而返回。
注:其他问题。
1、为什么外层的 while 要去等号?
分析:
以下图为案例:
我们顺着上述代码,最终第一次循环会来到如下位置:

此时left = right跳出循环,再将right对应的值与key对应值交换,此时得到如下图所示数组:

显然上图的基准值是不符合条件的,因此外层需要加上等号。
2、为什么内层交换两个元素前,需要加该 if 条件
分析:
以下图数组为例:

顺着代码,当第一次循环结束前,各个变量对应关系如图所示

此时若没有该 if 条件,right 和 left 的对应元素会再交换一次,这样就不符合基准值的条件,因此需要有该 if 条件,代码才能够正常运行。
2.2.1.2:挖坑法
前面所说,三种递归实现快速排序的方法只在找基准值的方法上有所不同,因此这里不再对其他代码过多追叙,直接分析找基准值部分的代码:
找基准值代码:
int _QuickSort2(int* arr, int left, int right)
{int hole = left;int keyi = arr[left];while (left < right){while (left < right && arr[right] > keyi){right--;}arr[hole] = arr[right];hole = right;while (left < right && arr[left] < keyi){left++;}arr[hole] = arr[left];hole = left;}arr[hole] = keyi;return hole;
}
图例分析:
起始时,各个变量对应关系如图所示:

假设坑(hole)的位置在 left 处,并且将 left 处对应的元素临时存放在 keyi 中,我们先从 right 开始向左找比keyi小,找到第一个位置处时,将 hole 位置处的值赋为 right 位置处的值,同时将 hole 移动到 right 位置处,此时新坑位于 right 处,此时各变量对应关系如下图所示:

然后我们再从 left 向右开始找比keyi大的值,找到第一个位置处 9 时,我们重复上述的过程,把元素 9 赋值给 hole 位置处,然后把 hole 移到 left 位置处,此时各变量对应关系如下图所示:

注:其实此时我们能够发现,left 和 right 位置对应的值已经发生改变,第二次循环开始时,已然满足内层循环的 while 条件,因此 right 可以继续向左减减找小值,而 left 也能够向右加加找大值。
重复上述过程,最终各个变量对应关系如下图:

注:为什么内层循环要多一个 left < right 的判定条件?
分析:可以看到,如果没有这个条件,left 会继续向右找大值,即到下标为5的位置处,会让 hole 多一次变化,而这一次变化,会导致所找的基准值发生错误,因此内层的 while 循环需要多这一个条件。当while循环结束时,我们再把临时值 keyi 赋值给 hole 位置处,此时 hole 位置就是对应基准值的位置,各个变量关系如下图所示

2.2.1.3:lumoto法
找基准值代码:
int _QuickSort(int* arr, int left, int right)
{int prev = left;int keyi = left;int cur = prev + 1;while (cur <= right){if (arr[cur] < arr[keyi] && ++prev != cur){swap(&arr[cur], &arr[prev]);}cur++;}swap(&arr[keyi], &arr[prev]);return prev;
}
图例分析:
起始时,各变量对应关系图下图:

定义一个前后变量 prev 和 cur,同时定义临时值 keyi = 5 。当 cur 中对应的值小于 keyi时,同时 ++prev 不等于 cur 时,我们让 prev 对应的值与 cur 对应的值交换,内层循环的 if 隐含着一些码字菜鸟(我)容易忽略的信息,为什么++prev 而不是 prev++,首先这个条件是为了避免重复交换,其次 ++prev 后此时虽然不满足 if 条件,但是 prev 的值还是 +1 了,因此此时 prev 和 cur 已经指向了同一个位置,只不过不会发生交换而已,各个变量关系如下图所示:

判定完 if 条件后,让 cur 持续++。观察上述数组,能够发现,3 后面的两个元素均大于临时值 keyi,此时 prev会来到元素 2 的位置处,而 2 已经满足内层 if 条件,因此我们将 prev+1 后 与 cur 位置对应的元素交换,此时各个变量关系对应如图所示:

重复上述过程,最终各个变量的对应关系如图所示:

当外层 while 循环结束时,我们交换 下标0 与 prev位置对应的元素,即得到了我们想要的数组,同时 prev 为基准值对应的位置处。
2.2.1.4:递归版本快速排序的特性:
1.时间复杂度为:O(nlogn)
2.空间复杂度为:O(logn) —— 这个空间复杂度来源与递归的层数,每次递归会像系统申请新的空间,同时原空间会被保留。
2.2.2:非递归版本的快速排序的实现
前言:非递归版本的快速排序是借助栈的方式来实现的,将数组首尾下标入队后,取栈内两个数据作为数组尾和头再出栈,通过lumoto方法找到该数组对应的基准值key,再将 left ~ key-1 的下标,以及 key+1 ~ right 的下标入堆,重复上述过程,直至堆中不再有任何元素。
注:因为传递的是数组的地址,入栈的也只是数组对应的下标,当出栈时,当然可以通过下标去访问原先的数组,同时令原先数组发生改变。
代码:
void QuickSortNonR(int* arr, int left, int right)
{ST s;STInit(&s);STPush(&s, left);STPush(&s, right);while (!BoolEmpty(&s)){int end = STTop(&s);STPop(&s);int begin = STTop(&s);STPop(&s);int prev = begin;int keyi = begin;int cur = prev + 1;while (cur <= end) //找基准值同时对数组进行排序{if (arr[cur] < arr[keyi] && ++prev != cur){swap(&arr[cur], &arr[prev]);}cur++;}swap(&arr[keyi], &arr[prev]);int key = prev;if (key - 1 > begin) //模拟返回的过程,若不满足条件则不入栈{STPush(&s, begin);STPush(&s, key-1);}if (key + 1 < end){STPush(&s, key+1);STPush(&s, end);}}
}
图例演示一下这个while循环的过程:
初始时,将下标 0 和下标 8 入栈

由此我们开始分析while循环部分,以栈为空作为外层 while 循环的结束条件
进入外层 while 循环后取栈顶元素并且出栈,这种取元素再出栈的操作保证了我们能够取到我们想要的元素,同时将这两个元素作为 lumoto法找基准值中的 cur 的结束位置和 prev 初始大小。
注:注意入栈的顺序,入栈顺序会影响后续读取栈顶元素时,begin 和 end 的取值。
当我们取到下标 0 和 8 时,通过下标访问数组元素,同时基于lumoto法实现找基准值的方法,这里不在过多追叙,此时各变量关系如图所示:

此时显然满足 begin < key - 1、 key+1 < end ,因此将下标 begin、key-1、key+1、end 分别入栈,当第二次进入循环时,首两次出栈会取到下标 key+1 和 end ,这正对应了2.2.2.1中提到的递归过程一样,left的值发生变化的过程,(编者菜,会觉得left一直等于0,其实正如刚才的递归过程一样,每个子节点的向右递归left值会发生改变),于是我们先对这对范围内的数组进行找基准值和排列操作,操作完成后,再次向栈中,push下标,直至不在满足 if 条件为止,不过此时栈中元素不为空,因为原先找的基准值 4 左边的数组尚未进行排序,接下来从栈中取出的元素,正是对左半部分进行找基准值和排序的操作,如此循环往复直至结束。

上图为对基准值右半部分的循环过程,可以看到当不满足 begin < key - 1、 key+1 < end 时,不再入栈,右半部分循环结束,此时栈中还保存着基准值左半部分的下标位置,取出栈内元素后,可以对左半部分进行找基准值和排列操作。
相关文章:
希尔排序/选择排序
前言: 本篇主要对常见的排序算法进行简要分析,代码中均以数组 arr[] { 5, 3, 9, 6, 2, 4, 7, 1, 8 } 为例,进行升序排列。 常见的排序算法有如下: 选择排序中,直接选择排序没有任何实际与教育意义,而堆排…...
漫谈设计模式 [16]:中介者模式
引导性开场 菜鸟:老鸟,我最近在开发一个聊天应用的时候遇到了点问题。每个用户都需要与其他用户直接通信,这让我在代码中写了很多复杂的逻辑来管理这些联系。这样下去,代码越来越难维护了。你有什么建议吗? 老鸟&…...
深度学习-物体检测YOLO(You only look once)
目录 一:YOLO算法的网络结构 流程 1.图像分割 2.图片在网格中的处理 3.非极大值抑制 二:训练 三:分类误差 四:与Faster R-CNN对比 一:YOLO算法的网络结构 GooleNet4个卷积2个全连接层 流程 输入原始图片resize到…...
redisson中的分布式锁
我的博客大纲 我的后端学习大纲 a.redisson概述: 1.Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)2.redisson介绍官方文档地址:3.Redisson它不仅提供了一系列的分布式的Java常用对象,还…...
如何将镜像推送到docker hub
前言 这一篇应该是最近最后一篇关于docker的博客了,咱来个有始有终,将最后一步——上传镜像给他写完,废话不多说,直接进入正题。 登录 首先需要确保登录才能推送到你的仓库中去,在终端输入docker login,输入用户名和…...
GO 匿名函数
GO 匿名函数 文章目录 GO 匿名函数1. **回调函数**2. **goroutine 中的操作**3. **延迟操作(defer)**4. **内联处理逻辑**5. **闭包**6. **过滤、映射等函数式编程风格**7. **测试中的临时逻辑**8. **短期存在的逻辑操作**总结 匿名函数在 Go 语言中的使…...
JuiceFS 在多云架构中加速大模型推理
在大模型的开发与应用中,数据预处理、模型开发、训练和推理构成四个关键环节。本文将重点探讨推理环节。在之前的博客中,社区用户 BentoML 和贝壳的案例提到了使用 JuiceFS 社区版来提高模型加载的效率。本文将结合我们的实际经验,详细介绍企…...
【DCL】Dual Contrastive Learning for General Face Forgery Detection
文章目录 Dual Contrastive Learning for General Face Forgery Detectionkey points:贡献方法数据视图生成对比学习架构实例间对比学习实例内对比学习总损失函数实验实验细节定量结果跨数据集评估跨操作评估消融实验可视化Dual Contrastive Learning for General Face Forgery…...
https的特点
https的特点 优点:缺点:HTTPS是如何保证安全的? 优点: 使用HTTPS协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器;使用HTTPS协议可以进行加密传输、身份认证,通信更加安全、防止…...
〖open-mmlab: MMDetection〗解析文件:mmdet/models/losses/cross_entropy_loss.py
目录 深入解析MMDetection中的CrossEntropyLoss及其应用1. 概述2. 核心函数2.1 cross_entropy2.1.1 函数定义和参数说明2.1.2 函数体2.1.3 总结 2.2 binary_cross_entropy2.2.1 _expand_onehot_labels函数2.2.2 binary_cross_entropy函数2.2.3 总结 2.3 mask_cross_entropy2.3.…...
【PyTorch单点知识】torch.nn.Embedding模块介绍:理解词向量与实现
文章目录 0. 前言1. 基础介绍1.1 基本参数1.2 可选参数1.3 属性1.4 PyTorch源码注释 2. 实例演示3. embedding_dim的合理设定4. 结论 0. 前言 按照国际惯例,首先声明:本文只是我自己学习的理解,虽然参考了他人的宝贵见解及成果,但…...
Jedis 操作 Redis 数据结构全攻略
Jedis 操作 Redis 数据结构全攻略 一 . 认识 RESP二 . 前置操作2.1 创建项目2.2 关于开放 Redis 端口的问题2.2.1 端口转发?2.2.2 端口配置 2.3 连接到 Redis 服务器 三 . 通用命令3.1 set 和 get3.2 exists 和 del3.3 keys3.4 expire、ttl、type 三 . string 相关命令3.1 mse…...
ctf.show靶场ssrf攻略
前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 web351 解析:post传入url参数他就会访问。 解法: hackbar传入url参数写入https://127.0.0.1/flag.php web352 解析:post传入url参数,不能是127.0.0.1和localhost 解法:缩写127.1传入 web353 解析…...
在 PyTorch 中,如何使用 `pack_padded_sequence` 来提高模型训练的效率?
在PyTorch中,pack_padded_sequence 是一个非常有用的函数,它可以用来提高模型训练的效率,特别是在处理变长序列数据时。这个函数的主要作用是将填充后的序列数据打包,以便循环神经网络(RNN)可以更高效地处理…...
Gossip协议
主要用在Redis Cluster 节点间通信 Gossip协议,也称为流行病协议(Epidemic Protocol),是一种在分布式系统中用于信息传播和故障探测的算法。 一、工作原理 随机选择传播对象 每个节点会定期随机选择一些其他节点作为传播对象。这…...
数据结构————双向链表
内存泄漏: 内存泄漏(Memory Leak)是指程序中已动态分配的内存由于某种原因程序未释放或无法释放,导致系统内存的浪费,严重时会导致程序运行缓慢甚至崩溃。这种情况在长时间运行的程序或大型系统中尤为常见,…...
55 - I. 二叉树的深度
comments: true difficulty: 简单 edit_url: https://github.com/doocs/leetcode/edit/main/lcof/%E9%9D%A2%E8%AF%95%E9%A2%9855%20-%20I.%20%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%B7%B1%E5%BA%A6/README.md 面试题 55 - I. 二叉树的深度 题目描述 输入一棵二叉树的根节点…...
Redis——初识Redis
初识Redis Redis认识Redis 分布式系统单机架构为什么要引入分布式理解负载均衡数据库的读写分离引入主从数据库 引入缓存数据库分库分表业务拆分——微服务常见概念了解 Redis背景介绍特性应用场景Redis不能做的事情Redis客户端redis客户端的多种形态 Redis 认识Redis 存储数…...
Xshell or Xftp提示“要继续使用此程序,您必须应用最新的更新或使用新版本”
Xshell提示“要继续使用此程序,您必须应用最新的更新或使用新版本”,笔者版本是xshell 6 方法一:更改系统时间 对于Windows 10用户,首先找到系统日期,右键点击并选择“调整时间/日期”。将日期设定为上一年。完成调整后&#x…...
table用position: sticky固定多层表头,滑动滚动条border边框透明解决方法
问题:我发现,这个上下滑动有内容经过就会出现如图的情况。 解决的方法:用outline(轮廓)替代border,以达到我们想要的样式。 outline主要是在元素边框的外围设置轮廓,outline不占据空间,绘制于…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...
centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
