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

news2024/11/27 2:35:05

文章目录

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

前言

为什么会有希尔排序,要从插入排序说起,希尔排序一开始设计出来是为了改进插入排序,因为插入排序在处理大量数据时效率不高,特别是对于近乎有序的数据。
对于插入排序:
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.coloradmin.cn/o/1832369.html

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

相关文章

结合Boosting理论与深度ResNet:ICML2018论文代码详解与实现

代码见&#xff1a;JordanAsh/boostresnet: A PyTorch implementation of BoostResNet 原始论文&#xff1a;Huang F, Ash J, Langford J, et al. Learning deep resnet blocks sequentially using boosting theory[C]//International Conference on Machine Learning. PMLR, 2…

Bagging与Boosting的应用与优势

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

深浅拷贝以及正则表达式(python)

浅拷贝和深拷贝&#xff1a; 浅拷贝&#xff1a; copy函数是浅拷贝&#xff0c;支队可变类型的第一层对象进行拷贝&#xff0c;对拷贝的对象开辟显得内存空间进行存储&#xff0c;不会拷贝对象内部的子对象 不可变类型的浅拷贝示例&#xff1a; 浅拷贝不会对不可变类型进行…

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

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

ssm宠物网站系统-计算机毕业设计源码07183

摘 要 在信息飞速发展的今天&#xff0c;网络已成为人们重要的信息交流平台。宠物网站每天都有大量的信息需要通过网络发布&#xff0c;为此&#xff0c;本人开发了一个基于B/S&#xff08;浏览器/服务器&#xff09;模式的宠物网站系统。 该系统以JJava编程语言、MySQL和SSM框…

Linux自旋锁

面对没有获取锁的现场&#xff0c;通常有两种处理方式。 互斥锁&#xff1a;堵塞自己&#xff0c;等待重新调度请求自旋锁&#xff1a;循环等待该锁是否已经释放 本文主要讲述自旋锁 自旋锁其实是一种很乐观的锁&#xff0c;他认为只要再等一下下锁便能释放&#xff0c;避免…

最适合程序员的编程字体,漂亮、独特、优雅!(2024-06-17)

Monaco Monaco 字体是一款专为编程和代码编辑设计的等宽字体&#xff0c;以其简洁明了的无衬线设计风格、高可读性和清晰的字符区分度&#xff0c;受到开发者们的青睐&#xff0c;Mac 自带 Monaco 字体。 Consolas Consolas 是一款等宽无衬线字体&#xff0c;专为编程和代码编…

vscode c++ 开发环境配置

今天各位同学已经安装了mingw环境&#xff0c;但部分同学vscode开发环境又问题&#xff0c;究其原因&#xff0c;还是vscode 编译环境配置错误&#xff0c;有问题的同学 按如下步骤处理&#xff1a; 1、卸载相关插件&#xff0c;特别是中文插件&#xff0c;原因是暂时回避中文…

Unity2D游戏制作入门 | 13 ( 之人物三段攻击 )

上期链接&#xff1a;Unity2D游戏制作入门 | 12(之人物受伤和死亡的逻辑动画)-CSDN博客 上期我们聊了人物的受伤和死亡的逻辑和动画&#xff0c;我们主要学习了事件的执行&#xff0c;即我们在人物受伤时可能会触发很多的事件&#xff0c;比如触发人物受伤的动画以及播放音乐等…

定个小目标之刷LeetCode热题(16)

针对本题排序流程&#xff0c;主要是将链表拆分为长度为subLength的子链表1和子链表2&#xff0c;然后把子链表1和子链表2合并为一条有序链表&#xff0c;重复上述步骤直到把链表都拆分完&#xff0c;这样这条链表每段长度为2的子链表都是有序的&#xff0c;那么要整条链表有序…

Ubuntu的文件权限介绍

Linux系统是一个多用户系统&#xff0c;每个用户都会创建自己的文件。为了防止其他人擅自改动他人的文件&#xff0c;需要拥有一套完善的文件保护机制。在Linux系统中&#xff0c;这种保护机制就是文件的访问权限。文件的访问权限决定了谁可以访问和如何访问特定的文件。 为了…

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

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

一行代码实现鼠标横向滚动

&#x1f9d1;‍&#x1f4bb; 写在开头 点赞 收藏 学会&#x1f923;&#x1f923;&#x1f923; 在项目中我们可能会遇到当鼠标在某个区域内&#xff0c;我们希望滚动鼠标里面的内容可以横向滚动&#xff1b; 比如我们一些常见的后台状态栏&#xff1a; 那这种该怎么写&…

使用 calibre 拆分电子书合辑

文章目录 引言下载插件拆书设置封面等元信息 引言 下载电子书合辑后&#xff0c;想拆分为单独成册的文件 https://bookfere.com/post/603.html 教程使用 calibre 的 EpubSplit 插件&#xff0c;这里我跟着实践&#xff0c;记录在此&#xff0c;希望能帮助你。 本文基于 macOS …

llamaindex原理与应用简介(宏观理解)

llamaindex原理与应用简介&#xff08;宏观理解&#xff09; 文章目录 llamaindex原理与应用简介&#xff08;宏观理解&#xff09; 这是我认为对于 llamaindex 应用的场景概述讲的相对比较好的视频&#xff1a;llamaindex原理与应用简介

PLSQL、Oracle以及客户端远程连接服务器笔记(仅供参考)

1.PLSQL参考链接&#xff1a; 全网最全最细的PLSQL下载、安装、配置、使用指南、问题解答&#xff0c;相关问题已汇总-CSDN博客文章浏览阅读2.9w次&#xff0c;点赞98次&#xff0c;收藏447次。双击之后&#xff0c;这里选择安装目录&#xff0c;你安装目录选的哪里&#xff0…

第三方软件测试报告包括哪些内容?如何获取专业第三方测试报告?

第三方软件测试报告是由独立的第三方公司进行软件测试后所生成的报告。该报告会清晰地呈现出软件在各个方面的测试结果和评估。通过第三方公司的专业测试&#xff0c;这些报告具有公正、中立和权威的特点。 一、第三方软件测试报告包括哪些内容? 1、功能测试&#xff1a;验证…

Day 44 Ansible自动化运维

Ansible自动化运维 几种常用运维工具比较 ​ Puppet ​ —基于 Ruby 开发,采用 C/S 架构,扩展性强,基于 SSL,远程命令执行相对较弱ruby ​ SaltStack ​ —基于 Python 开发,采用 C/S 架构,相对 puppet 更轻量级,配置语法使用 YAML,使得配置脚本更简单 ​ Ansible ​ —基于 …

鸿蒙: 基础认证

先贴鸿蒙认证 官网10个类别总结如下 https://developer.huawei.com/consumer/cn/training/dev-cert-detail/101666948302721398 10节课学习完考试 考试 90分合格 3次机会 1个小时 不能切屏 运行hello world hvigorfile.ts是工程级编译构建任务脚本 build-profile.json5是工程…