十大排序 —— 希尔排序

十大排序 —— 希尔排序

  • 什么是希尔排序
  • 插入排序
  • 希尔排序
  • 递归版本

我们今天来看另一个很有名的排序——希尔排序

什么是希尔排序

希尔排序(Shell Sort)是插入排序的一种更高效的改进版本,由Donald Shell于1959年提出。它通过比较相距一定间隔的元素来工作,间隔会逐步减小,直到最后变成1,此时算法实际上退化为普通的插入排序,但因为数组元素基本已经部分排序,所以插入排序在这个阶段会非常高效。

希尔排序的基本思想是将原始数据集分割成若干子序列,先使这些子序列基本有序,然后再对全体记录进行一次直接插入排序。这里的"基本有序"是指在子序列内,任意两个元素之间的距离都不超过某个预先设定的间隔(称为"增量"或"gap")时,它们的顺序要么是正确的,要么仅需很少的比较和移动就可以变得正确。

希尔排序的具体步骤如下:

  1. 选择一个增量序列:首先确定一个增量gap,常见的选择有希尔增量(如gap = gap / 2,直到gap = 1),或者使用更复杂的序列如Hibbard增量序列。
  2. 分组排序:将整个数组分成多个子序列,每个子序列包含相距gap的元素,然后对每个子序列应用插入排序。
  3. 减小增量:重复步骤1和2,但每次减小增量的大小,直到增量为1。
  4. 最终排序:当增量减至1时,整个数组就是一个子序列,对这个子序列执行插入排序,这时数组应该是(或接近)完全有序。

希尔排序的优势在于它能够较早地将远处的元素移动到更接近其最终位置,从而减少了数据移动的总次数,提高了排序效率。特别是在数组规模较大时,相比简单的插入排序,希尔排序能显著提高排序速度。希尔排序的时间复杂度依赖于所选的增量序列,最好的情况可以达到O(n log n),最坏情况则与所选增量序列有关,但通常优于O(n^2)

再讲希尔排序之前,我们讲一下插入排序。

插入排序

插入排序(Insertion Sort)是一种简单直观的排序算法,适用于对小型数据集合或者基本有序的数据集合进行排序。它的核心思想是将一个记录(或数据元素)插入到已经排序好的有序表中,从而得到一个新的、记录数增加1的有序表。

插入排序的基本步骤如下:

  1. 初始化:假设数组的第一个元素已经被排序。
  2. 遍历数组:从第二个元素开始,每次取出一个元素(记为key),在已排序序列中从后向前扫描。
  3. 比较与移动:如果该元素(key)小于它前面的元素,则将前面的元素向后移动一位,为key腾出位置;否则,停止扫描。
  4. 插入元素:将key插入到已排序序列中的适当位置,即找到的正确位置或扫描停止的位置。
  5. 重复步骤2-4:对数组中的每个元素都执行这样的插入操作,直到数组完全排序。

插入排序的时间复杂度分析:

  • 最好情况:当输入数组已经是排序状态时,插入排序只需要进行n-1次比较,不需要移动数据,时间复杂度为O(n)。
  • 最坏情况:当输入数组是反序时,每次插入都相当于在数组的最前面插入,每次插入操作需要移动所有的已排序元素,时间复杂度为O(n^2)。
  • 平均情况:对于随机数据,插入排序的时间复杂度也是O(n^2)。

插入排序的空间复杂度为O(1),因为它是一种原地排序算法,除了输入数组外,只需要常数级别的额外空间用于交换和临时存储。此外,插入排序是稳定的排序算法,即相等的元素的相对顺序不会改变。

画个图就是这样的:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 选择插入排序函数实现
void select_sort(int *array, int size)
{// 遍历整个数组,除了最后一个元素(因为它自然有序)for (int i = 0; i < size - 1; i++){// 选取当前位置i+1的元素作为待插入元素int temp = array[i + 1];// 初始化"已排序"部分的最后一个元素索引int end = i;// 当"已排序"部分的索引大于等于0时进入循环while (end >= 0){// 如果"已排序"部分的当前元素大于待插入元素if (array[end] > temp){// 将较大的元素向后移动一位,为待插入元素腾出空间std::swap(array[end + 1], array[end]);// 缩小"已排序"部分的范围,继续向前比较end--;}else{// 如果当前元素不大于待插入元素,说明找到了temp的正确位置// 或者已经检查到数组起始,此时无需再比较,直接跳出循环break;}}}// 循环结束,数组完成排序
}int main()
{int array[] = { 1,34,56,1,233,56,0 };for (auto e : array){std::cout << e << " ";}std::cout << std::endl;select_sort(array, sizeof(array) / sizeof(array[0]));for (auto e : array){std::cout << e << " ";}
}

在这里插入图片描述

大家可以画个图,结合着图能比较清晰理解。

希尔排序

我们已经了解到了希尔排序的特殊形式直接插入排序,现在我们来了解一下更一般的形式=希尔排序。
我们之前用直接插入排序来让数据升序排列,我们来考虑一下它最糟糕的情况:数据是降序排序的。
在这里插入图片描述
这样的话,9之后的数据都要进行一次插入,插入到9之前,时间复杂度可以达到O(N2),但我们会遇到这样一种情况:
在这里插入图片描述
10比9大故不用进行插入了,而我们可不可以让这种情况更普遍一些呢?所以我们可以在进行排序之前,先进行一次预排序,让数据整体是升序的,这就是希尔排序

希尔排序的步骤

  1. 将数据分成gap组。
  2. 以gap为间隔进行直接插入排序。

在这里插入图片描述

//希尔排序
void ShellSort(int* a, int n)
{assert(a);int gap = 3;for (int i = 0; i < n - gap ; i+=gap){int end = i;int temp = a[end + gap];while (end >= 0){if (temp < a[end]){std::swap(array[end + gap], array[end]);end-=gap;}else{break;}}}
}

这段代码的结果是什么样的呢?我们来画图看看:
在这里插入图片描述
其实我们只完成了一组红色数据的预排序,其实我们的预排序可以不止一组:
在这里插入图片描述
那么如何完成这三组的排序呢?很简单,加一层for循环。

//希尔排序
void shell_sort(int* array, int size)
{int gap = 3;for (int j = 0; j < 3; j++){for (int i = j; i < size - gap; i += gap){//后面一个待排元素int temp = array[i + gap];int end = i;while (end >= 0){if (array[end] > temp){std::swap(array[end + gap], array[end]);end-=gap;}else{break;}}}}}

我们看看程序结果怎么样:
在这里插入图片描述
我们发现数据的排序是有一些变化,但好像还是有点混乱,这是因为gap的原因,这时候我们把gap改为2看看:
在这里插入图片描述
我们发现gap越大,数据排序越混乱,但移动的越快,**gap越小,数据越有序,但移动的越慢**这直接和时间复杂度有关。

// 希尔排序函数实现
void shell_sort(int* array, int size)
{// 初始化间隔(gap)为数组的长度int gap = size;// 当间隔大于1时,持续执行以下步骤,逐步减小间隔以达到排序效果while (gap > 1){// 计算新的间隔,这里使用的是一个简化的规则,将间隔除以3并加1,这是一种常用的递减策略gap = gap / 3 + 1;// 这个循环是为了处理不同子序列的起始位置,确保每个间隔下的元素都能被遍历到for (int j = 0; j < gap; j++){// 遍历数组,从当前子序列的起始位置开始,步长为当前的间隔for (int i = j; i < size - gap; i += gap){// 保存当前间隔位置的下一个元素,准备进行插入排序int temp = array[i + gap];int end = i;// 内部循环实现插入排序逻辑,将temp插入到已排序的子序列中正确的位置while (end >= 0){// 如果当前子序列中的元素大于temp,将该元素后移if (array[end] > temp){std::swap(array[end + gap], array[end]); // 交换元素end -= gap; // 移动到前一个位置继续比较}else{// 如果找到了temp的正确位置或已经比较到子序列的起始位置,跳出循环break;}}}}}// 当所有间隔遍历完成后,数组已经基本排序完成,由于最后一次gap为1,实质上进行了最后一次的插入排序收尾
}

其实这段代码还有一个变形,也可以达到效果:多组并排

//希尔排序
void shell_sort(int* array, int size)
{int gap = size;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < size - gap; i ++){//后面一个待排元素int temp = array[i + gap];int end = i;while (end >= 0){if (array[end] > temp){std::swap(array[end + gap], array[end]);end -= gap;}else{break;}}}}
}

递归版本

这里实现递归版本完全是为了锻炼思维,实际中不会这样写,仅供参考:

void select_sort(int* array, int size, int index)  
{  // 如果index小于0,递归结束  if (index < 0)  return;  // 递归调用,先对前面的部分进行排序  select_sort(array, size, index - 1);  // 如果index已经是最后一个元素(或者超过最后一个元素的索引),则不需要再进行排序  if (index >= size - 1)  return;  // 临时存储index+1位置的元素,这个元素可能是当前未排序部分的最小值  int temp = array[index + 1];  int end = index; // 从index位置开始向前比较  // 这是一个插入排序(Insertion Sort)的插入过程,但逻辑上不适合选择排序  while (end >= 0)  {  // 如果当前元素比temp大,则将当前元素后移  if (array[end] > temp)  {  std::swap(array[end], array[end + 1]);  end--;  }  else  {  // 如果找到合适的位置,或者已经到达数组的开头,就跳出循环  break;  }  }  }

希尔排序:

// 递归实现的插入排序,用于希尔排序中每轮的细分排序
void select_sort(int* array, int size, int index, int gap)
{// 递归基础情况:当索引小于0时,说明已处理完当前gap下的所有元素if (index < 0)return;// 递归调用,处理当前元素前一个gap位置的元素,逐步向前select_sort(array, size, index - gap, gap);// 防止数组越界,当索引达到size-gap时,说明当前gap下已无更多元素需要处理if (index >= size - gap)return;// 保存当前gap位置的元素值,准备进行插入排序int temp = array[index + gap];int end = index;// 插入排序逻辑,将temp插入到前方已排序的子序列中的正确位置while (end >= 0){if (array[end] > temp){// 交换元素,将较大的元素后移,为temp腾出位置std::swap(array[end + gap], array[end]);end -= gap; // 更新索引,继续向前比较}else{// temp已到达或超过其正确位置,停止循环break;}}
}// 实现希尔排序的主体函数,使用递归的select_sort进行每一轮的细分排序
void shell_sort(int* array, int size, int index)
{int gap = size;// 循环直到gap减至1,每轮使用一个较小的gap进行插入排序while (gap > 1){    // 计算下一轮的gap值,这里采用gap = gap / 3 + 1是一种常见但非唯一的策略gap = gap / 3 + 1;// 使用当前gap值调用select_sort进行插入排序select_sort(array, size, index, gap);}
}

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

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

相关文章

路由策略案例

一、路由策略案例 如图所示&#xff0c;某公司内终端通过Switch接入公司内部网络。如果该公司内存在非如图1所示&#xff0c;运行OSPF协议的网络中&#xff0c;RouterA从Internet网络接收路由&#xff0c;并头RouterB提供了部分Internet路由。其中: RouterA仅提供172.1…

工厂模式——工厂方法模式+注册表

工厂方法模式的瑕疵 在前一篇笔记中我们介绍了工厂方法模式&#xff0c;示例的类图如下&#xff1a; 考虑一种情况&#xff1a;现在要在程序运行时&#xff0c;根据外部资源&#xff0c;动态的实例化对象。也就是说在编译期我们无法知道要实例化的对象的类型。因此在实例化的过…

Java 新手入门:基础知识点一览

Java 新手入门&#xff1a;基础知识点一览 想要踏入 Java 的编程世界&#xff1f;别担心&#xff0c;这篇文章将用简单易懂的表格形式&#xff0c;带你快速了解 Java 的基础知识点。 一、Java 是什么&#xff1f; 概念解释Java一种面向对象的编程语言&#xff0c;拥有跨平台、…

JDBC-MySQL

JDBC-MySQL 1.JDBC 操作步骤1.1 DriverManager1.2.Connection对象1.3 Statement1.4 PreparedStatement 1.JDBC 操作步骤 public void quickStart() throws ClassNotFoundException, SQLException {//1、注册驱动 &#xff08;确认要使用哪个数据库&#xff09;Class.forName(&…

短视频直播教学课程小程序的作用是什么

只要短视频/直播做的好&#xff0c;营收通常都不在话下&#xff0c;近些年&#xff0c;线上自媒体行业热度非常高&#xff0c;每条细分赛道都有着博主/账号&#xff0c;其各种优势条件下也吸引着其他普通人冲入。 然无论老玩家还是新玩家&#xff0c;面对平台不断变化的规则和…

《2024年DDoS趋势报告》:DDoS攻击规模飙升233.33%

2023年&#xff0c;数字领域面临着分布式拒绝服务&#xff08;DDoS&#xff09;攻击的变革浪潮&#xff0c;攻击速度创纪录地达到了每秒700 Gbps和8000万数据包。这些事件跨越了从游戏到金融服务的各个行业&#xff0c;突显了DDoS是一种普遍存在的风险。 值得注意的是&#xf…

Ubuntu server 24 (Linux) sudo 免输密码

1 sudo 使用要输入密码&#xff0c;费时费力。 2 sudo命令免输密码&#xff0c;需要修改/etc/sudoers文件 #本文以test用户为例,#允许不需要输入密码执行 sudo vi /etc/sudoers test ALL(ALL) NOPASSWD: ALL %sudo ALL(ALL:ALL) ALL --> #%sudo ALL(ALL:ALL) ALL#所有…

掌握 NestJS 10.x:NestJS 结合 PostgreSQL 使用详解

NestJS 是一个用于构建高效、可扩展的 Node.js 服务端应用的框架。结合 PostgreSQL 数据库,可以为应用提供强大的数据存储和查询功能。本文将详细介绍如何在 NestJS 项目中集成和使用 PostgreSQL,并创建一个包含增加用户接口的完整示例。 1. 安装必要的依赖 首先,确保你的…

基于VITA57.4标准的单通道6GSPS 12位采样ADC,单通道 6GSPS 16位采样DAC子卡模块

板卡概述 FMC147是一款单通道6.4GSPS&#xff08;或者配置成2通道3.2GSPS&#xff09;采样率的12位AD采集、单通道6GSPS&#xff08;或配置成2通道3GSPS&#xff09;采样率16位DA输出子卡模块&#xff0c;该板卡为FMC标准&#xff0c;符合VITA57.4规范&#xff0c;该模块可以作…

Day 42 LVS四层负载均衡

一&#xff1a;负载均衡简介 1.集群是什么 ​ 集群&#xff08;cluster&#xff09;技术是一种较新的技术&#xff0c;通过集群技术&#xff0c;可以在付出较低成本的情况下获得在性能、可靠性、灵活性方面的相对较高的收益&#xff0c;其任务调度则是集群系统中的核心技术 …

量化研究---大qmt实盘实现禄得可转债策略轮动

前面写了一个强大的可转债自定义系统&#xff0c;我们可以利用这个快速对接到大qmt,我提供实时数据支持 量化研究---强大的可转债分析系统上线&#xff0c;提供api&#xff0c;实时数据支持 打开网页 http://120.78.132.143:8023/ 强大可转债选择系统 http://120.78.132.143:8…

容器中运行ifconfig提示bash: ifconfig: command not found【笔记】

容器中运行ifconfig提示bash: ifconfig: command not found 这个问题是因为在容器中没有安装ifconfig命令。 在容器中安装ifconfig命令&#xff0c;可以使用以下命令&#xff1a; 对于基于Debian/Ubuntu的容器&#xff0c;使用以下命令&#xff1a; apt-get update apt-get …

每日一练:利用多态思想和ArrayList集合,编写一个模拟KTV点歌系统的程序。【多态思想和ArrayList集合的综合应用】

目录 一、设计程序使用ArrayList集合&#xff0c;编写一个模拟KTV点歌系统的程序。参考代码歌曲类歌单类KTV类测试类运行效果 总结 最后 一、设计程序 使用ArrayList集合&#xff0c;编写一个模拟KTV点歌系统的程序。 要求&#xff1a; 输入0代表添加歌曲输入1代表将所选歌曲…

云原生环境下GPU算力调度发展分析

云原生环境下GPU算力调度深度分析 概述&#xff1a; 云原生时代&#xff0c;GPU算力调度与管理备受瞩目&#xff0c;成为企业和云服务提供商关注的焦点&#xff0c;助力AI、深度学习、高性能计算等领域&#xff0c;满足对GPU资源的迫切需求。 容器化与编排&#xff1a; Kube…

突破开源天花板!最强文本转语音工具ChatTTS:对话式高可控的语音合成模型

ChatTTS 一夜爆火&#xff0c; 极速出圈&#xff0c; 3 天就斩获 9k 的 Star 量&#xff0c; 截止 2024.06.04&#xff0c; 已经 19.3k 的 star&#xff0c; 极速接近 GPT-soVITs 当天的 26.2k 的 star 数。 什么是ChatTTS&#xff1f; TTS全称&#xff1a;Text To Speech&am…

数字生命计划1.0

一. 背景 即使人们强调思维与肉体是两大构成个体的先决条件&#xff0c;但科学界始终没有放弃探索摆脱肉体的介质&#xff0c;从而让思维上载的研究。 这就是数字生命计划的初衷。所谓数字生命&#xff0c;就是将人类的意识思维抽离&#xff0c;上传至云端或智能终端&#xf…

eMMC的封装类型及特点有哪些?

eMMC&#xff08;Embedded MultiMediaCard&#xff09;是一种高度集成的嵌入式存储解决方案&#xff0c;广泛应用于智能手机、平板电脑、笔记本电脑、消费电子产品和物联网设备等领域。eMMC的封装形式对其性能、应用和设备设计有着重要的影响。目前&#xff0c;eMMC主要有两种封…

航空航天技术翻译要求

航空航天技术是一项国际化产业&#xff0c;航空技术更新迅速涉及的专业知识较为广泛。在航空领域&#xff0c;每一个术语都有着特定的含义和用法&#xff0c;稍有差池就可能导致翻译出现严重的后果&#xff0c;因此&#xff0c;航空航天翻译扮演者至关重要的角色。 专业航空航天…

【ARM Cache 与 MMU 系列文章 7.6 -- ARMv8 MMU 相关寄存器介绍】

文章目录 MMU 转换控制寄存器 TCR_ELxTCR_ELx 概览TCR_ELx 寄存器字段详解TCR 使用示例Normal MemoryCacheableShareability MMU 内存属性寄存器 MAIR_ELx寄存器结构内存属性字段使用实例 MMU 地址翻译表基址寄存器 TTBR0/1_ELxTTBR0_ELx 寄存器概述寄存器结构功能和用途编程示…

idea Pycharm Webstorm 2024年 最新版 永久使用2099年教程 附激活码亲测可用

idea Pycharm Webstorm 2024年 最新版 永久使用2099年教程 附激活码亲测可用 链接&#xff1a; 下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE (jetbrains.com)&#xff08;这是官网的&#xff09; 下载 安装 1.双击idea的安装包&#xff0c;点击next 选择创建桌面快捷…