【排序算法】希尔排序详解(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.mfbz.cn/a/717732.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【数据库编程-SQLite3(三)】Ubuntu下sqlite3的使用

学习分享 1、安装sqlite3命令2、sqlite3点命令3、在Linux命令行下&#xff0c;启动sqlite33.1、编写sql脚本3.2、脚本编写--DDL3.3、进入xxx.db数据库&#xff0c;读取脚本。3.4、再次查看数据库中的表。证明表创建成功。3.5、查看数据表中用户内容3.6、查看表结构3.7、在数据库…

JAVAEE值之网络原理(1)_用户数据报协议(UDP)、概念、特点、结构、代码实例

前言 在前两节中我们介绍了UDP数据报套接字编程&#xff0c;但是并没有对UDP进行详细介绍&#xff0c;本节中我们将会详细介绍传输层中的UDP协议。 一、什么是UDP&#xff1f; UDP工作在传输层&#xff0c;用于程序之间传输数据的。数据一般包含&#xff1a;文件类型&#xff0…

【图像分割】DSNet: A Novel Way to Use Atrous Convolutions in Semantic Segmentation

DSNet: A Novel Way to Use Atrous Convolutions in Semantic Segmentation 论文链接&#xff1a;http://arxiv.org/abs/2406.03702 代码链接&#xff1a;https://github.com/takaniwa/DSNet 一、摘要 重新审视了现代卷积神经网络&#xff08;CNNs&#xff09;中的atrous卷积…

WPF 深入理解一、基础知识介绍

基础知识 本系列文章是对个人 B站 up 微软系列技术教程 记录 视频地址 https://www.bilibili.com/video/BV1HC4y1b76v/?spm_id_from333.999.0.0&vd_source0748f94a553c71a2b0125078697617e3 winform 与 wpf 异同 1.winform 项目结构 编辑主要是在 Form1.cs(页面)&#…

【QT5】<重点> QT串口编程

目录 前言 一、串口编程步骤 0. 添加串口模块 1. 自动搜索已连接的串口 2. 创建串口对象 3. 初始化串口 4. 打开串口 5. 关闭串口 6. 发送数据 7. 接收数据 二、简易串口助手 1. 实现效果 2. 程序源码 3. 实现效果二 前言 本篇记录QT串口编程相关内容&#xff0…

早期发现,健康生活!第三届ZAODX世界肿瘤早筛大会圆满落幕!

2024年6月15日-16日&#xff0c;第三届ZAODX世界肿瘤早筛大会在雄安新区盛大开幕&#xff01;本次会议由河北雄安新区管理委员会公共服务局指导&#xff0c;第三届ZAODX世界肿瘤早筛大会组委会和早筛网主办&#xff0c;粤港澳大湾区精准医学研究院&#xff08;广州&#xff09;…

Paragon NTFS for Mac 15软件下载及安装教程

简介&#xff1a; NTFS For Mac 15是首个支持Mac上读写NTFS外置存储设备解决方案 &#xff0c;解决mac不能读写外置让您更加简单直观的在Mac机上随意对NTFS文件修改、删除等操作。 安 装 包 获 取 地 址&#xff1a; Paragon Ntfs For Mac 15版&#xff1a; ​​https://sou…

04 远程访问及控制

1、SSH远程管理 SSH是一种安全通道协议&#xff0c;主要用来实现字符界面的远程登录、远程复制等功能。 SSH协议对通信双方的数据传输进行了加密处理&#xff08;包括用户登陆时输入得用户口令&#xff09;。 终端&#xff1a;接收用户的指令 TTY终端不能远程&#xff0c;它…

《跟我一起学“网络安全”》——等保风评加固应急响应

等保风评加固应急响应 一、安全加固 背景 随着IP技术的飞速发展&#xff0c;一个组织的信息系统经常会面临内部和外部威胁的风险&#xff0c;网络安全已经成为影响信息系统的关键问题。 虽然传统的防火墙等各类安全产品能提供外围的安全防护&#xff0c;但并不能真正彻底的消…

吴恩达2022机器学习专项课程C2W3:2.26 机器学习发展历程

目录 开发机器学习系统的过程开发机器学习案例1.问题描述2.创建监督学习算法3.解决问题4.小结 误差分析1.概述2.误差分析解决之前的问题3.小结 增加数据1.简述2.增加数据案例一3.增加数据案例二4.添加数据的技巧5.空白创建数据6.小结 迁移学习1.简述2.为什么迁移学习有作用3.小…

【设计模式-12】代理模式的代码实现及使用场景

&emsp&#xff1b;代理模式是一种应用很广发的结构性设计模式&#xff0c;它的设计初衷就是通过引入新的代理对象&#xff0c;在客户端和目标对象之间起到中介的作用&#xff0c;从而实现控制客户端对目标对象的访问&#xff0c;比如增强或者阉割某些能力。 1. 概述 代理模…

如何理解电流镜负载的差分对的增益

我们知道最普通的电阻负载的差分对的差分增益是-gmRD&#xff0c;如果我们不希望输出是双端的&#xff0c;而是希望单端输出&#xff0c;那么使用电阻负载的差分对会导致增益变为原先的一半&#xff0c;因此引入了电流镜负载的差分对&#xff0c;它可以在保证增益与原先相同的情…

一小时搞定JavaScript(1)——JS基础

前言,本篇文章是依据bilibili博主(波波酱老师)的学习笔记,波波酱老师讲的很好,很适合速成!!! 文章目录 1.基本语法1.1 JS语言的特点1.2 创建一个JS文件1.3 输入与输出1.4 变量1.4.1 命名规则1.4.2 变量声明数字类型字符串类型布尔类型未定义类型 1.4.3 类型转换隐式转换显示转…

数据可视化实验一:Panda数据处理及matplotlib绘图初步

目录​​​​​​​ 2024-6-17 一、请将所有含有发明家“吴峰”的发明专利的“申请日”打印出来。并将含有“吴峰”的所有发明专利条目保存到Excel中 1.1 代码实现 1.2 运行结果 二、读取文件创建城市、人口、性别比、城镇化率DataFrame对象&#xff0c;计算指标排名&…

嘴尚绝卤味:传统与创新的味蕾碰撞,尝鲜必备美食!

在当今的餐饮市场中&#xff0c;各式各样的美食层出不穷&#xff0c;让人目不暇接。而在这一片美食的海洋中&#xff0c;嘴尚绝卤味以其独特的魅力和口感&#xff0c;成功吸引了众多食客的目光。今天&#xff0c;就让我们一起来探索一下&#xff0c;嘴尚绝卤味究竟有何魔力&…

MongoDB~分片数据存储Chunk;其迁移原理、影响,以及避免手段

分片数据存储&#xff1a;Chunk存储 Chunk&#xff08;块&#xff09; 是 MongoDB 分片集群的一个核心概念&#xff0c;其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据&#xff0c;互不相交且并集为全部数据。 分片集群不会记录每条数据…

Java多线程下载工具,多线程,多任务,断点续传,GUI

目录 一、题目要求 二、效果展示 三、功能实现 四、代码 一、题目要求 序号 功能名称 功能需求标识 简要描述 1 下载功能 Download 当用户输入一个下载链接后&#xff0c;能识别链接并开始多线程下载工作&#xff0c;包括线程监听、线程管理等。 2 续传功能 …

[面试题]Spring

[面试题]Java【基础】[面试题]Java【虚拟机】[面试题]Java【并发】[面试题]Java【集合】[面试题]MySQL[面试题]Maven[面试题]Spring Boot[面试题]Spring Cloud[面试题]Spring MVC[面试题]Spring[面试题]MyBatis Spring 是一个很庞大的技术体系&#xff0c;可以说包容一切&…

APP Android

APP Android 安卓源生应用程序 APP IOS-CSDN博客 05.04 06:11Testing

MySQL修改用户权限(宝塔)

在我们安装好的MySQL中&#xff0c;很可能对应某些操作时&#xff0c;不具备操作的权限&#xff0c;如下是解决这些问题的方法 我以宝塔创建数据库为例&#xff0c;创建完成后&#xff0c;以创建的用户名和密码登录 这里宝塔中容易发生问题的地方&#xff0c;登录不上去&#…