【数据结构初阶】排序算法(下)冒泡排序与归并排序

news2024/11/20 12:28:20

文章目录

  • 4. 交换排序
    • 4. 1 冒泡排序
  • 5. 归并排序
  • 6. 非比较排序
    • 6. 1 计数排序
  • 5. 排序性能分析
  • 6. 排序算法复杂度及稳定度分析


4. 交换排序

交换排序基本思想:
所谓交换**,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置**。
交换排序的特点是: 将值较大的数据向序列的尾部移动,值较小的数据向序列的前部移动(也可以反过来)。

常见的交换排序有两个:

  1. 冒泡排序——这是一个除了教学意义外几乎没有任何用处的排序,因为它的时间复杂度太高
  2. 快速排序——实践中最常用的排序,在快排专题中已经介绍过。

4. 1 冒泡排序

冒泡排序是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动

冒泡排序的思路就是每一趟通过两两比较逐渐将未排序的数据中的最大值(或最小值)送到它最终应该在的地方,也就是未排序数据的最右边

举例:

int a[] = {3, 5, 9, 7, 2, 1};

通过冒泡排序把它变成降序的。

第一次排序,从最左边开始拿3和5比较,3比5小,交换

int a[] = {5, 3, 9, 7, 2, 1};

接着3又和9比较,3小,再交换

int a[] = {5, 9, 3, 7, 2, 1};

接着3又和7比较,3小,再交换

int a[] = {5, 9, 7, 3, 2, 1};

接着3又和2比较,2小,不交换,但操作的元素从3变成2
2与1比较,1小,不交换,第一轮比较结束。
从原理上看,只有1被放到正确的位置,但是我们可以发现实际上被正确放置的还有2,3,这就会导致在后面几次比较时,可能整个数组都已经有序了。比如下一次遍历之后:

int a[] = {9, 7, 5, 3, 2, 1};

可以看到此时数组已经有序了,但是循环没有停止,这会大大降低冒泡排序的效率
可以在每一轮循环中增加一个变量,当发生交换时,改变它的值,如果在一轮循环后这个变量的值没有发生改变,就说明所有的数据已经有序了,就可以提前停止循环

代码:

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int jug = 1;	//单次循环的交换检测变量
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				jug = 0;
			}
		}
		if (!jug)
			jug = 1;	
		else	//如果jug没被更改,就说明已经有序了
			break;
	}
}

时间复杂度:O(N2)
空间复杂度:O(1)

5. 归并排序

归并排序算法思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divideand Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列。即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序核心步骤:

归并
归并排序的核心就是把数组拆分再一点点地合并,并在每次合并后时合并的这部分有序,直到合并成整个数组

合并时,应该怎么让合并后的部分有序呢?调用快排吗?
当然不需要,要注意合并的这两部分已经有序了,我们可以采用双指针遍历这两个数组,把较小的数放到前面,较大的数放到后面(这里以升序为例,降序则相反)。这样做的时间复杂度是O(N),而快排是O(NlogN)

但这里又出现了一个问题:把较小的数放到哪里?直接放到这个数组里面吗?这显然不行,因为这样可能会覆盖还没排序的数据。
所以我们可以创建一个新的数组,这个数组和排序的数组的大小一致,把排序好的数据放进去,在本次合并完成时,就把数据从新数组里拷贝回来就可以了。

void _MergeSort(int* a, int* tmp, int left, int right)
{
	//递归的停止条件
	if (left >= right)
		return;
	
	//递归的思路是反着来的,要先传递下去才能回归,把数组不断二分,最终就可以开始回归,也就是上面说的步骤
	int mid = (left + right) / 2;

	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);
	//此时开始合并,合并的两个序列已经是有序的了
	int cur1 = left, end1 = mid;
	int cur2 = mid + 1, end2 = right;
	int cur = left;
	//双指针遍历两个序列,把较小的数放到tmp里,形成升序
	while (cur1 <= end1 && cur2 <= end2)
	{
		if (a[cur1] < a[cur2])
			tmp[cur++] = a[cur1++];
		else
			tmp[cur++] = a[cur2++];
	}
	//当上面的循环结束时,有且仅有一个序列还没遍历完,将剩下的放入tmp数组
	while (cur1 <= end1)
		tmp[cur++] = a[cur1++];
	while (cur2 <= end2)
		tmp[cur++] = a[cur2++];
	
	//把tmp里的数据拷贝回来,合并完成
	for (int i = left; i <= right; i++)
		a[i] = tmp[i];
}

void MergeSort(int* a, int n)
{
	//创建新数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	//开始递归
	_MergeSort(a, tmp, 0, n - 1);
	//记得释放动态申请的数组
	free(tmp);
	tmp = NULL;
}

时间复杂度:O(NlogN)
空间复杂度:O(N)

6. 非比较排序

6. 1 计数排序

计数排序又称为鸽巢原理,是对哈希直接定址法(不了解这个也没关系)的变形应用。操作步骤

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

比如

int a[]={6, 1, 2, 9, 4, 2, 4, 1, 4};

我们以这个数组为例:

  1. 首先遍历一次数组,找到数据中的最大值max和最小值min创建一个新数组,把新数组中元素都置为0,数组的大小是max
  2. 再来遍历数组,每遍历一个数据,就把新数组中对应下标的数据+1
  3. 当遍历完成后,再遍历新数组,根据新数组中数据的下标推算出对应的数据(因为放入时是根据数据对应的下标放入的,所以可以反推出数据)放入原数组个数就是新数组中存放的数据

jishu
在这个例子中,计数排序表现良好,但是如果换一个例子:
比如:

int a[]={100, 101, 109, 105, 101, 105};

这是我们再根据最大值开空间就会造成极大的浪费
2
我们可以根据范围来开空间,开一个max-min+1大小的新数组,这样每一个数据都可以在-min之后找到对应的新数组下标,还能大幅度节省空间。

void CountSort(int* a, int n)
{
	//找最大值和最小值
	int max = a[0];
	int min = a[0];

	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	//创建数组
	int N = max - min + 1;
	int* count = (int*)calloc(N, sizeof(int));
	if (!count)
	{
		perror("calloc");
		exit(1);
	}
	//遍历,并在新数组中将数据标记
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//放回原数组
	int index = 0;
	for (int i = 0; i < N; i++)
	{
		while (count[i]--)
			a[index++] = i + min;
	}
}

计数排序的特性:
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
时间复杂度:O(N+range)
空间复杂度:O(range)

5. 排序性能分析

time.h库中提供了clock()函数,可以在程序中记录运行到这里时的时间,而代码又是顺序执行的,因此只要保存一段代码前后的时间,再相减就可以得到这段代码运行的时间,由此,我们可以做出这样的测试代码:

void TestOP()
{
	srand((unsigned int)time(0));
	const int N = 100000;	//可以适当改小这个数值,提高速度
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	//制作相同的数组,避免其他变量
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
	}
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	 
	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();
	 
	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();
	 
	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();
	 
	int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();
	 
	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();
	 
	int begin7 = clock();
	BubbleSort(a7, N);
	int end7 = clock();
	 
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);
	printf("BubbleSort:%d\n", end7 - begin7);
	  
	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
}
int main()
{
	TestOP();
	return 0;
}

注:由于冒泡排序真的很慢,所以程序运行起来后一段时间的卡顿是正常的,可以修改N来减少等待时间,但N不能太小,否则时间太短看不出来差异。
结果
这样就可以很直观地看出来不同排序算法的差异了。

6. 排序算法复杂度及稳定度分析

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称为不稳定的。

排序办法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n2)O(n)O(n2)O(1)稳定
直接选择排序O(n2)O(n2)O(n2)O(1)不稳定
直接插入排序O(n2)O(n)O(n2)O(1)稳定
希尔排序O(nlogn)~O(n2)O(n1.3)O(n2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(nlogn)O(n2)O(nlogn)~O(n2)不稳定

关于稳定性,没必要死记硬背,需要时在脑海里拿一个比较简单的数组模拟着走一遍这个排序算法的步骤,看看有没有发生不判断两个数相等的交换就可以了,发生了就是不稳定的。
比如堆排序的第一个元素与最后一个元素交换,快排的基准值与prev或是其他变量的交换,这些都会导致这个算法不是稳定的。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章

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

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

相关文章

下一代性能怪兽RTX 5090最新规格更新与Blackwell架构解析

据悉&#xff0c;目前各家AIC厂商已经陆续收到NVIDIA的相关资料&#xff0c;RTX 5090、RTX 5080已经正式进入开案阶段&#xff0c;也就是厂商们开始设计各自的产品方案了。不出意外&#xff0c;年初的CES 2025上会看到RTX 5090/5080的发布。 作为NVIDIA的新一代GPU&#xff0c…

2024年健康经济与大数据研讨会(HEBD 2024)2024 Seminar on Health Economics and Big Data

在线投稿&#xff1a;学术会议-学术交流征稿-学术会议在线-艾思科蓝 2024年经济决策与人工智能国际学术会议 &#xff08;EDAI 2024&#xff09;将在2024年11月08-10日在广东省广州市隆重举行。大会邀请来自国内外高等院校、科学研究所、企事业单位的专家、教授、学者、工程师…

理解互联网链路:从本地ISP到Tier 1 ISP运营商

1. 互联网服务提供商&#xff08;ISP&#xff09; 互联网服务提供商&#xff08;ISP&#xff09;是指提供互联网接入服务的公司或组织。它们负责将用户连接到互联网&#xff0c;并提供相关的服务&#xff0c;如电子邮件、网站托管和其他在线服务。ISP可以分为不同的层级&#…

告别转换顾虑,来试试这四款pdf转换器~

各位小伙伴们&#xff0c;大家好&#xff01;今天我来给大家分享几款超级好用的PDF转换工具&#xff0c;无论是工作还是学习&#xff0c;相信这些工具都会给你带来极大的便利&#xff1b;别看PDF文件看似难搞&#xff0c;其实有了这些神器&#xff0c;一切都变得轻松又愉快&…

在线css像素Px到百分比(%)换算器

具体请前往&#xff1a;在线Px转百分比(%)工具--将绝对像素(px)长度单位转换为相对父级元素内尺寸的相对长度单位百分比(%)

PCL GridMinimum获取栅格最低点

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 GridMinimum 栅格最低点提取 2.1.2 可视化函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xff08;长…

新农人的求索:既要种菜,也要种钱

澎湃新闻记者 何惠子 灯下立着一个玻璃瓶&#xff0c;内里空无一物&#xff0c;清晰透亮。 一只手握住瓶身。“就像这个瓶子。前途一片光明&#xff0c;但其实都在瓶子里。” 解晓巍说的是音乐——他曾梦想以此维生。事实上&#xff0c;这也适合描述农业。 在没有任何收入的202…

计算机网络:计算机网络概述 —— 初识计算机网络

文章目录 计算机网络组成部分网络架构协议与标准网络设备网络类型作用实际应用案例 计算机网络 计算机网络是指将多台计算机通过通信设备和通信链路连接起来&#xff0c;以实现数据和信息的交换和共享的技术和系统。它是现代信息社会的基础设施之一&#xff0c;也是互联网的基…

工业现场干扰问题及处理方法

目前&#xff0c;各种干扰在各类工业现场中均存在&#xff0c;所以仪表及控制系统的可靠性直接影响到现代化工业生产装置安全、稳定运行&#xff0c;系统的抗干扰能力是关系到整个系统可靠运行的关键。随着DCS、现场总线技术的应用&#xff0c;被控对象和被测信号往往分布在各个…

Ubuntu启动后第一次需要很久才能启动GTK应用问题

Ubuntu启动后第一次需要很久才能启动GTK应用问题 自从升级了 Ubuntu 之后&#xff0c;设备重启&#xff0c;发现打开 Terminal 、Nautilus 以及其他的GTK 应用都很慢&#xff0c;需要至少一分钟的时间启动。 刚开始也是拿着 journalctl 的异常日志去寻找答案&#xff0c;但是没…

cheese安卓版纯本地离线文字识别插件

目的 cheese自动化平台是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。可以采用Vscode、IDEA编写&#xff0c;支持Java、Python、nodejs、GO、Rust、Lua。cheese也包含图色功能&#xff0c;识别…

山东大学操作系统学习笔记:第3.1讲程序的结构-简单的程序

第3.1讲&#xff1a;程序的结构-简单的程序 可执行文件 & 程序的装入 .rwdata(读写数据段): 存放程序中的含初值常量。这些常量在程序运行可以修改。 零初始化数据段&#xff08;.zidata/.bss - Block Started by Symbol&#xff09;&#xff1a;存放程序中的不含初值&am…

《自控原理》最小相位系统

在复平面右半平面既没有零点&#xff0c;也没有极点的系统&#xff0c;称为最小相位系统&#xff0c;其余均为非最小相位系统。 从知乎看了一篇答案&#xff1a; https://www.zhihu.com/question/24163919 证明过程大概率比较难&#xff0c;我翻了两本自控的教材&#xff0c;…

【中间件】fastDFS的相关知识

一、分布式文件系统 1.1 传统的文件系统 我们在Linux中学习的文件系统就是传统的文件系统&#xff1a; 传统的文件系统格式&#xff1a; ntfs/fat32/ext3/ext4 可以被挂载和卸载&#xff0c;就是一般一个盘可以分成多个盘&#xff0c;每一盘都可以挂载到不同的目录路径中。…

实时语音交互,打造更加智能便捷的应用

随着人工智能和自然语言处理技术的进步&#xff0c;用户对智能化和便捷化应用的需求不断增加。语音交互技术以其直观的语音指令&#xff0c;革新了传统的手动输入方式&#xff0c;简化了用户操作&#xff0c;让应用变得更加易用和高效。 通过语音交互&#xff0c;用户可以在不…

考研笔记之操作系统(三)- 存储管理

操作系统&#xff08;三&#xff09;- 存储管理 1. 内存的基础知识1.1 存储单元与内存地址1.2 按字节编址和按字编址1.3 指令1.4 物理地址和逻辑地址1.5 从写程序到程序运行1.6 链接1.6.1 静态链接1.6.2 装入时动态链接1.6.3 运行时动态链接 1.7 装入1.7.1 概念1.7.2 绝对装入1…

算法-汉诺塔问题(Hanoi tower)

介绍 汉诺塔是源于印度的一个古老传说的小游戏&#xff0c;简单来说就是有三根柱子&#xff0c;开始的时候&#xff0c;第一根柱子上圆盘由大到小&#xff0c;自下往上排列。这个小游戏要实现的目的呢&#xff0c;就是要把第一根柱子上的圆盘移到第三根的柱子上去&#xff1b;…

【重学 MySQL】四十四、相关子查询

【重学 MySQL】四十四、相关子查询 相关子查询执行流程示例使用相关子查询进行过滤使用相关子查询进行存在性检查使用相关子查询进行计算 在 select&#xff0c;from&#xff0c;where&#xff0c;having&#xff0c;order by 中使用相关子查询举例SELECT 子句中使用相关子查询…

带你0到1之QT编程:二十二、QChart类图表及折线图、直方图、饼图的三大可视化图表实战!

此为QT编程的第二十二谈&#xff01;关注我&#xff0c;带你快速学习QT编程的学习路线&#xff01; 每一篇的技术点都是很很重要&#xff01;很重要&#xff01;很重要&#xff01;但不冗余&#xff01; 我们通常采取总-分-总和生活化的讲解方式来阐述一个知识点&#xff01;…

09_OpenCV彩色图片直方图

import cv2 import numpy as np import matplotlib.pyplot as plt %matplotlib inlineimg cv2.imread(computer.jpeg, 1) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img) plt.show()plot绘制直方图 plt.hist(img.ravel(), 256) #ravel() 二维降一维 256灰度级…