【数据结构与算法】排序算法(上)——插入排序与选择排序

文章目录

  • 一、常见的排序算法
  • 二、插入排序
    • 2.1、直接插入排序
    • 2.2、希尔排序( 缩小增量排序 )
  • 三、选择排序
    • 3.1、直接选择排序
    • 3.2、堆排序
      • 3.2.1、堆排序的代码实现


一、常见的排序算法

在这里插入图片描述
常见排序算法中有四大排序算法,第一是插入排序,二是选择排序,三是交换排序,四是归并排序。本站文章针对前两个排序,这其中不才将拿出每个排序中所具有代表性的排序算法进行深入解读。


二、插入排序

基本思想:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

实际中我们玩扑克牌时,就用了插入排序的思想
在这里插入图片描述

2.1、直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:


把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

当插入i(i>=1)个元素时,前面的array[0],array[1],,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],的排序码顺序进行比较找到插入位置即将array[i]插入,原来位置上的元素顺序后移
在这里插入图片描述
代码实现:

void InsertSort(int* arr, int n) {
    // 外层循环,从第二个元素开始,逐步处理每个元素
    for (int i = 0; i < n - 1; i++) {
        // tmp 是当前元素的索引,初始值是 i+1,表示从第二个元素开始
        int tmp = i + 1;
        // end 是已排序部分的最后一个索引,初始值是 i
        int end = i;
        // 把当前比较的数据保护起来
        int num = arr[tmp];

        // 内层循环,寻找当前元素插入的位置
        while (end >= 0) {
            // 如果已排序部分的元素大于当前元素
            if (arr[end] > num) {
                // 已经把当前元素比较保护好,可以将已排序部分的元素向右移动
                arr[tmp] = arr[end];
                // tmp 和 end 都向左移一位
                tmp--;
                end--;
            }
            else {
                // 找到合适的位置,不需要继续向左移动了
                break;
            }
        }
        // 将当前元素插入到合适的位置
        arr[tmp] = num;
    }
}

时间复杂度:

  • 最好的情况是:如果数组已经是有序的(或者几乎有序),只需要进行一轮比较,时间复杂度是O(n)。
  • 最坏的情况是:数组是逆序的每次都要比较所有已排序的元素,时间复杂度是O(n²)。

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

2.2、希尔排序( 缩小增量排序 )

希尔排序法又称缩小增量法。希尔排序法的基本思想是:

先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时即为直接插入排序,所有记录在统一组内排好序。

希尔排序的工作原理:

  1. 初始间隔(gap:希尔排序的关键点在于选择一个合适的“间隔”序列,也叫做增量序列。初始时,希尔排序会使用一个较大的间隔,比如n/2,然后通过逐渐缩小间隔来进行多次排序

  2. 分组插入排序:每次排序时,希尔排序将待排序的序列按间隔分成多个子序列。然后对每个子序列分别进行插入排序。例如:间隔2时,序列的第一个元素、第三个元素、第五个元素、第七个元素、第九个元素形成一个子序列(如下图gap=2时)。然后对这个子序列进行插入排序。接着处理间隔更小的子序列,直到间隔为1时,整个序列就是有序的。

  3. 逐渐减小间隔:随着间隔的逐步减小,元素变得越来越接近排序完成,最后,当间隔为1时,希尔排序就变成了直接插入排序

在这里插入图片描述
总的来说,希尔排序就是直接排序PRO MAX版本使用希尔排序,它可以快速地把大数放在右边,小数放在左边。在快速区分大小数位置之后,就比原先的混乱顺序变得更有序,在直接插入排序中,我们知道元素集合越接近有序,直接插入排序算法的时间效率越高,我们就不断的往有序的方向靠近最后再直接在使用直接排序就可以缩短大部分时间。如下图,我们使用gap来表示每一次比较的跨越元素个数。

代码的实现:(两种循环的实现)

void shellSort(int* arr, int n) {
	int gap = n / 3 + 1;

	while (gap > 1) {// 判断收缩gap的值,直到gap值为1时,完成插入排序
		//int gap = 3;
		//for (int j = 0; j < gap; j++) {//end要分别对gap分出的gap个数组进行排序,这样便可完成数组中每个位置的比较

			//for (int i = 0; i < n - 1 - gap; i += gap) { // 把间距把分开的每个tmp位置都进行插入排序
		for (int end = 0; end < n  - gap; end++) { //这样设置循环可以把end在数组中的每个位置都走一边 与上面两层循环相比只是逻辑不同,效率上没有变化
			//int end = i + j;
			int tmp = arr[end + gap];
			while (end >= 0) { //把当前tmp插入到合适的位置
				if (arr[end] > tmp) {
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else {
					break;
				}
			}
			arr[end + gap] = tmp;
		}
			//}

		//}

		gap = gap / 3 + 1;
	}
}
  • 排序代码的实现得有里到外的编写,这样容易把控
  • 首先编写一个正常的直接插入排序,并且把gap加上。
for (int i = 0; i < n - 1; i++) {
	int gap = 1;
	int tmp = i + gap;
	int end = i;
	int num = arr[i + gap];

	while (end >= 0){
		if (arr[end] > num) {
			arr[tmp] = arr[end];
			tmp--;
			end--;
		}
		else {
			break;
		}
	}
	arr[tmp] = num;
}
  • 这样我们就改好了直接插入排序有gap时的写法了
  • 我们开始修改gap每次跳转的范围,首先以gap=2为例,首先我们把i每次增加的个数都增加gap个。我们也优化变量
int gap = 2;
for (int i = 0; i < n - 1 - gap; i += gap) {
	int end = i;
	int tmp = arr[end + gap];
	while (end >= 0) { //把当前tmp插入到合适的位置
		if (arr[end] > tmp) {
			arr[end + gap] = arr[end];
			end -= gap;
		}
		else {
			break;
		}
	}
	arr[end + gap] = tmp;
}
  • 这个时候,我们就完成了一个子序列:第一个元素、第三个元素、第五个元素、第七个元素、第九个元素的元素排序。但是,我们gap = 2时,我们是把原数组分为两个子序列。所以要对两个子序列都进行排序。这样我们就必须在外面再套一层循环来把gap分开的子序列都进行排序。(如下图中被分开为红蓝两个子序列)在这里插入图片描述
int gap = 2;
for (int j = 0; j < gap; j++) {
	for (int i = 0; i < n - 1 - gap; i += gap) {
		int end = i + j;
		int tmp = arr[end + gap];
		while (end >= 0) { //把当前tmp插入到合适的位置
			if (arr[end] > tmp) {
				arr[end + gap] = arr[end];
				end -= gap;
			}
			else {
				break;
			}
		}
		arr[end + gap] = tmp;
	}
}
  • 子序列第一个节点起始点永远不会再第一个子序列的第二个节点的后面,所以我们可以通过套用外层循环j遍历的控制end的起始地址,则这样就可以完成多个子序列的访问。
    在这里插入图片描述
  • 这时候我们就可以完成所有子序列第一次排序。但是为了完成原数组的整体排序,我们必须要让gap每完成一个排序就减少,直到gap = 1时,变为直接插入排序完成数组的排序。
int gap = 2;
while (gap > 0) {
	for (int j = 0; j < gap; j++) {
		for (int i = 0; i < n - 1 - gap; i += gap) {
			int end = i + j;
			int tmp = arr[end + gap];
			while (end >= 0) { //把当前tmp插入到合适的位置
				if (arr[end] > tmp) {
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else {
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
	gap--;
}
  • 这样我们就完成了一个低效版本的希尔排序

为何说是低效版本呢?因为gap的值是固定的。当数据量达到数十亿的级别之后。我们一个区区的常量2的效率与直接插入排序的效率几户一样。

这时候就有大佬研究出目前位置希尔排序gap的最好取值之二(n是数组的元素个数):gap = n/2gap = n/3 + 1(最快)。我们再自己手搓希尔排序时,使用gap选择哪个都可以。不才选择gap = n/3 + 1作为示范。

int gap = n;
while (gap > 1) {
	gap = (gap / 3) + 1;
	for (int j = 0; j < gap; j++) {
		for (int i = 0; i < n - 1 - gap; i += gap) {
			int end = i + j;
			int tmp = arr[end + gap];
			while (end >= 0) { //把当前tmp插入到合适的位置
				if (arr[end] > tmp) {
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else {
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}
  • 在使用gap = n/3 + 1之后,每次gap缩小值都是gap/3 + 1。无论是什么数循环到一定次数后最后除三的都会变为零。当除3等于0时再加一gap就等于1,这时候就是直接插入排序。当gap == 1时,就不会再进入循环。

但此时,上面循环中i、j就只是为了控制end变量起始位置可以遍历一遍数组,end每次都是与gap位后的数值进行比较。那么我们就可以把两层循环变为一层循环

void shellSort(int* arr, int n) {
	int gap = n;
	while (gap > 1) {// 再收缩gap的值,直到gap值为1时,完成插入排序
		gap = gap / 3 + 1;
		for (int end = 0; end < n  - gap; end++) { //这样设置循环可以把end在数组中的每个位置都走一边,但效率上没有变化
			int tmp = arr[end + gap];
			while (end >= 0) { //把当前tmp插入到合适的位置
				if (arr[end] > tmp) {
					arr[end + gap] = arr[end];//每次都与前gap值为比较
					end -= gap;
				}
				else {
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
    会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书籍中给出的希尔排序的时间复杂度都不固定:

《数据结构-用面相对象方法与C++描述》— 殷人昆

在这里插入图片描述
不才上面使用的是Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,暂时就按照:O(n1.25)到O(1.6*n1.25)来算,按照我们也可以粗略的归类与O(n * logn)的量级,但是真实的时间复杂度是比O (n * logn)大。


三、选择排序

基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

3.1、直接选择排序

  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
    在这里插入图片描述

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3.2、堆排序

在堆的的逻辑结构中是严格遵循有序但这并不意味着整个堆的物理存储结构是有序的堆排序的目的对堆中的元素进行排序,通过堆这种数据结构的特性来实现元素的排序。

排序中分为升序和降序,堆排序即利用堆的思想来进行排序

  • 排升序对应着建大堆
  • 排降序对应着建小堆

堆排序的方法:

  • 因为堆排序的逻辑与堆的删除逻辑是完全一致的,都是先把堆顶元素与最后一个元素进行交换之后向下调整。与删除不同的是,删除需要把数组中最后一个元素完全删除,排序只需要不再理会数组最后一个元素,不用真正删除元素。

排升序建大堆的原因把堆顶元素与最后一个元素进行交换之后,堆中的中最大的值被放置在物理结构的最右边,如此循环即可完成结构的升序。降序同理
在这里插入图片描述

把堆中元素进行升序排序

我们使用上述大堆的例子创建有序的物理结构物理结构:[95,70,8,21,5,3,4,6,9,1]
在这里插入图片描述
首先交换堆顶与最后一个元素(如下图)
在这里插入图片描述
在交换完成后逻辑结构上不再把95结点当作堆的结点,之后进行向下调整(如下图)
在这里插入图片描述
此时,物理结构为:[70,21,8,9,5,3,4,6,1,95]。这样就把最大值放置在物理结构最右边,并且忽略最后一个结点后,其他结点依旧保持着大堆结构。(与删除堆顶逻辑完全相同)

循环上述操作可得下图:
在这里插入图片描述
一定次数的循环后,会得到下图
在这里插入图片描述
观察上图可以看到此时物理结构:[8,6,3,1,5,4,9,21,70,95],只要循环次数足够,就可以把物理结构排为升序

最终可得下图:
在这里插入图片描述
此时我们就完成了:堆中元素的升序排序。物理结构为:[1,3,4,5,6,8,9,21,70,95]

3.2.1、堆排序的代码实现

void HeapSort(HPDataType* arr, int capacity, int farent) {
    assert(arr); // 确保传入的数组指针不为空

    int cp = capacity; // 存储堆的初始容量

    // 当堆中还有元素时进行排序
    while (cp != 0) {
        // 将堆顶元素(最大或最小元素)与当前堆的最后一个元素交换
        HeapSwap(arr, 0, cp - 1); 

        // 减少堆的有效大小(去除已排序的最大元素)
        --cp;

        // 调整堆结构,确保堆的性质依然保持
        AdjustDown(arr, cp, farent);
    }
}

  • 首先把堆顶元素与最后一个叶子节点的元素进行交换。
  • 之后--元素个数,把已经交换完成的最大值(最小值)忽略。
  • 完成后再向下调整。把交换完成后的顺序表,重新调整为大堆(小堆)。

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

ps:剩下的两大排序真正紧张制作中,欲知后事如何,请听下回分解~~

以上就是本章所有内容。若有勘误请私信不才。万分感激💖💖 如果对大家有帮助的话,就请多多为我点赞收藏吧~~~💖💖
请添加图片描述

ps:表情包来自网络,侵删🌹

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

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

相关文章

Flink四大基石之Time (时间语义) 的使用详解

目录 一、引言 二、Time 的分类及 EventTime 的重要性 Time 分类详述 EventTime 重要性凸显 三、Watermark 机制详解 核心原理 Watermark能解决什么问题,如何解决的? Watermark图解原理 举例 总结 多并行度的水印触发 Watermark代码演示 需求 代码演示&#xff…

虚拟机docker记录

最近看了一个up的这个视频&#xff0c;感觉docker真的挺不错的&#xff0c;遂也想来搞一下&#xff1a; https://www.bilibili.com/video/BV1QC4y1A7Xi/?spm_id_from333.337.search-card.all.click&vd_sourcef5fd730321bc0e9ca497d98869046942 这里我用的是vmware安装ubu…

[ACTF2020 新生赛]BackupFile--详细解析

信息搜集 让我们寻找源文件&#xff0c;目录扫描&#xff1a; 找到了/index.php.bak文件&#xff0c;也就是index.php的备份文件。 后缀名是.bak的文件是备份文件&#xff0c;是文件格式的扩展名。 我们访问这个路径&#xff0c;就会直接下载该备份文件。 我们把.bak后缀删掉…

AD单通道AD多通道

AD单通道接线图 滑动变阻器的内部结构 左边和右边的两个引脚接的是电阻的两个固定端&#xff0c;中间这个引脚接的是滑动抽头&#xff0c;电位器外边这里有个十字形状的槽可以拧&#xff0c;往左拧&#xff0c;抽头就往左靠&#xff0c;往右拧&#xff0c;抽头就往右靠。所以外…

解决Ubuntu DNS覆盖写入127.0.0.53

ubuntu22.04解析网址时报错如图所示&#xff1a; 因为/etc/resolve.conf中存在 nameserver 127.0.0.53回环地址造成循环引用 原因&#xff1a; ubuntu17.0之后特有&#xff0c;systemd-resolvd服务会一直覆盖 解决方法&#xff1a; 1、修改resolv.config文件中的nameserver…

hint: Updates were rejected because the tip of your current branch is behind!

问题 本地仓库往远段仓库推代码时候提示&#xff1a; error: failed to push some refs to 192.168.2.1:java-base/java-cloud.git hint: Updates were rejected because the tip of your current branch is behind! refs/heads/master:refs/heads/master [rejected] (…

[golang][MAC]Go环境搭建+VsCode配置

一、go环境搭建 1.1 安装SDK 1、下载go官方SDK 官方&#xff1a;go 官方地址 中文&#xff1a;go 中文社区 根据你的设备下载对应的安装包&#xff1a; 2、打开压缩包&#xff0c;根据引导一路下一步安装。 3、检测安装是否完成打开终端&#xff0c;输入&#xff1a; go ve…

关于VNC连接时自动断联的问题

在服务器端打开VNC Server的选项设置对话框&#xff0c;点左边的“Expert”&#xff08;专家&#xff09;&#xff0c;然后找到“IdleTimeout”&#xff0c;将数值设置为0&#xff0c;点OK关闭对话框。搞定。 注意,服务端有两个vnc服务,这俩都要设置ide timeout为0才行 附件是v…

AIGC时代 | 如何从零开始学网页设计及3D编程

文章目录 一、网页设计入门1. 基础知识2. 学习平台与资源3. 示例代码&#xff1a;简单的HTMLCSSJavaScript网页 二、3D编程入门1. 基础知识2. 学习平台与资源3. 示例代码&#xff1a;简单的Unity 3D游戏 《编程真好玩&#xff1a;从零开始学网页设计及3D编程》内容简介作者简介…

virtualbox给Ubuntu22创建共享文件夹

1.在windows上的操作&#xff0c;创建共享文件夹Share 2.Ubuntu22上的操作&#xff0c;创建共享文件夹LinuxShare 3.在virtualbox虚拟机设置里&#xff0c;设置共享文件夹 共享文件夹路径&#xff1a;选择Windows系统中你需要共享的文件夹 共享文件夹名称&#xff1a;挂载至wi…

C#窗体简单登录

创建一个Windows登录程序&#xff0c;创建两个窗体&#xff0c;一个用来登录&#xff0c;一个为欢迎窗体&#xff0c;要求输入用户名和密码&#xff08;以个人的姓名和学号分别作为用户名和密码&#xff09;&#xff0c;点击【登录】按钮登录&#xff0c;登录成功后显示欢迎窗体…

es 3期 第12节-选择合适的数据查询方式

#### 1.Elasticsearch是数据库&#xff0c;不是普通的Java应用程序&#xff0c;传统数据库需要的硬件资源同样需要&#xff0c;提升性能最有效的就是升级硬件。 #### 2.Elasticsearch是文档型数据库&#xff0c;不是关系型数据库&#xff0c;不具备严格的ACID事务特性&#xff…

Educator头歌:离散数学 - 图论

第1关&#xff1a;图的概念 任务描述 本关任务&#xff1a;学习图的基本概念&#xff0c;完成相关练习。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;图的概念。 图的概念 1.一个图G是一个有序三元组G<V,R,ϕ>&#xff0c;其中V是非空顶点集合&am…

day30|leetcode 452. 用最少数量的箭引爆气球, 435. 无重叠区间 , 763.划分字母区间

重叠区间专题 11.用最少的数量引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可以沿着 x 轴从不同…

电脑提示报错“Directx error”怎么解决?是什么原因导致的?游戏软件提示“Directx error”错误的解决方案

DirectX Error&#xff08;DX错误&#xff09;通常指的是在使用基于DirectX技术的应用程序&#xff08;尤其是游戏&#xff09;时遇到的问题。这个问题可能由多种因素导致&#xff0c;以下是一些可能的原因及相应的解决方案&#xff1a; 可能的原因 DirectX版本不匹配&#x…

K8S网络系列--Flannel网络下UDP、VXLAN模式的通信流程机制分析

文章目录 前言一、了解overlay、underlay容器网络二、网络通信1.分类2.网络虚拟设备对2.1、什么是网络虚拟设备对veth pair?2.2、如何查看容器的网卡与主机的哪个veth设备对是成对的关系? 3、vxlan和vtep3.1、vtep3.2、vxlan相关概念 三、Flannel网络模式剖析0、flannel的作用…

【OpenGL学习笔记】图形渲染管线

文章目录 渲染管线简介顶点输入顶点着色器片段着色器着色器程序链接顶点属性 VAO VBO绘制图元元素缓冲对象 EBO 渲染管线简介 在OpenGL中&#xff0c;一切都是3D的&#xff0c;但屏幕或者窗口是一个2D像素阵列&#xff0c;因此OpenGL的大部分工作是将所有3D坐标转换为适合屏幕…

Linux下的三种 IO 复用

目录 一、Select 1、函数 API 2、使用限制 3、使用 Demo 二、Poll 三、epoll 0、 实现原理 1、函数 API 2、简单代码模板 3、LT/ET 使用过程 &#xff08;1&#xff09;LT 水平触发 &#xff08;2&#xff09;ET边沿触发 4、使用 Demo 四、参考链接 一、Select 在…

视觉语言动作模型VLA的持续升级:从π0之参考基线Octo到OpenVLA、TinyVLA、DeeR-VLA、3D-VLA

第一部分 VLA模型π0之参考基线Octo 1.1 Octo的提出背景与其整体架构 1.1.1 Octo的提出背景与相关工作 许多研究使用从机器人收集的大量轨迹数据集来训练策略 从早期使用自主数据收集来扩展策略训练的工作[71,48,41,19-Robonet,27,30]到最近探索将现代基于transformer的策略…

【Python爬虫五十个小案例】爬取猫眼电影Top100

博客主页&#xff1a;小馒头学python 本文专栏: Python爬虫五十个小案例 专栏简介&#xff1a;分享五十个Python爬虫小案例 &#x1f40d;引言 猫眼电影是国内知名的电影票务与资讯平台&#xff0c;其中Top100榜单是影迷和电影产业观察者关注的重点。通过爬取猫眼电影Top10…