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

news2024/9/27 5:37:04

文章目录

  • 1.排序概念及运用
    • 1. 1 概念
    • 1. 2 运用
    • 1.3 常见排序算法
  • 2. 插入排序
    • 2. 1 直接插入排序
    • 2. 2 希尔排序
      • 2. 2. 1 希尔排序的时间复杂度
  • 3. 选择排序
    • 3. 1 直接选择排序
    • 3. 2 堆排序
    • 3. 3 Top-K问题


1.排序概念及运用

1. 1 概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作

1. 2 运用

各大购物平台可以按综合,销量,评论数,新品等许多要素进行排序。
大学可以按照软科,校友会等多种要素进行排序。

1.3 常见排序算法

排序

2. 插入排序

基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

实际中我们玩扑克牌时,就用了插入排序的思想:
扑克

2. 1 直接插入排序

当插入第 i(i>=1)个元素时,前面的 array[0],array[1],…,array[i-1]已经排好序,此时用 array[i]的排序码与 array[i-1],array[i-2],… 的排序码顺序进行比较,找到插入位置即将 array[i]插入,原来位置上的元素顺序后移。

我们以这个数组为例介绍一下详细步骤:

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

比如说我们要把这个数组排成升序的。

首先我们找到第二个(此时i=1)元素3,逐一向后比较,直到找到比它小的数,放到这个数的前面,而这里就是放到数组的第一个元素的位置,也就是第一步是5和3交换位置。
此时:

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

然后i++,对9进行操作,向前找比9小的,发现5就比9小,由于5前面的数字都已经有序了,所以9不在进行比较。
这一步对数组没有更改。

接着6,向前找比6小的,可以找到5,那么最终就是6与9交换了位置,

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

接着2,向前找直接找到头,放到3的前面。这里要说一下,在每次比较之后,如果还需要进行下一步的比较,应该将比较过的两个数据交换,一步步往前走,而不是找到要到的位置之后进行一次交换,这样会打乱已经排序好的数据。

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

到了这里,前4个数据就已经有序了,剩下的数据就不在赘述了,都是这个逻辑。

参考代码:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//外层循环控制排序次数,是n-1次,因为第一个数不需要排序
		int cur = i;
		//这里并没有采取每次交换的方法,因为那样的效率太低了
		//这里采取的是先把要比较的那个数存储起来,然后如果需要发生交换时就让前面的数直接向后覆盖
		//最后找到指定位置时,把存储起来的数值放入数组
		
		//把要比较的数据存储起来
		int tmp = a[cur + 1];
		while (cur >= 0)
		{
			if (a[cur] > tmp)
			{
				//向后进行一次覆盖
				a[cur + 1] = a[cur];
			}
			else	//比较符合预期的话,就说明找到合适的位置
				break;
			//注意这时的cur指向的是比要排序的数据小的数据
			cur--;
		}
		//把存储起来的数据放进去
		a[cur + 1] = tmp;
	}
}

直接插入排序的特性总结

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

时间复杂度其实很好计算,它和我们之前改进过的冒泡排序的时间复杂度是一样的,最差情况下要遍历
(n-1)(n-2)+(n-2)(n-2)+...,那么估计就是n2次。

2. 2 希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数(通常是gap=n/3+1),把待排序文件所有记录分成各组,所有的距离相等的记录分在同一组内,并对每一组内的记录进行排序,然后gap=gap/3+1得到下一个整数,再将数组分成各组,进行插入排序,当gap=1时,就相当于直接插入排序。它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的

这么说可能会不太容易理解,我们根据一个实例来分析一下步骤:
1
(图中被相同颜色的线连起来数据的是一组)
第一趟排序时,gap=5,我们可以找到2组,对这5组内的2个数据分别进行排序,使他们符合最终排序得相对有序。
第二趟排序时,gap=2,可以找到8组,对这2组内的5个数据分别进行排序,使他们符合最终排序得相对有序。
第三趟排序时,gap=1,可以找到9组,对这1组内的10个数据进行排序,使他们符合最终排序得相对有序。

最后一次排序就相当于直接插入排序,但是由于元素集合越接近有序,直接插入排序算法的时间效率越高,所以效率是远高于直接插入排序的

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//对gap进行迭代
		gap = gap / 3 + 1;
		//这个i是每轮排序中,排序的组数,是每组的第一个数据的下标
		for (int i = 0; i < n - gap; i++)	//i最大就是n-gap-1,再往后也找不到了
		{
			//分组,在每个组里使用直接插入排序
			int cur = i;	//cur是用来控制每个组内成员数据与它之前的组内成员数据进行比较的
			int tmp = a[cur + gap];	//实际要比较的数据是[cur+gap]这个数据
			//对组内成员进行操作
			while (cur >= 0)
			{
				if (a[cur] < tmp)
					//cur的上一个是cur+gap,不是cur+1
					a[cur + gap] = a[cur];
				else
					break;
				//cur迭代时要-gap来找到下一个
				cur -= gap;
			}
			a[cur + gap] = tmp;
		}
	}
}

2. 2. 1 希尔排序的时间复杂度

希尔排序的时间复杂度估算:
外层循环的时间复杂度可以直接给出,为:O(log2n)或者O(log3n),即O(log n)
内层循环的时间复杂度与gap的取值有关:
希尔篇排序

因此,希尔排序在最初和最后的排序的次数都为n,即前一阶段排序次数是逐渐上升的状态,当到达某一顶点时,排序次数逐渐下降至n,而该顶点的计算暂时无法给出具体的计算过程

希尔排序时间复杂度不好计算,因为 gap 的取值很多,导致很难去计算,因此很多书中给出的希尔排序的时间复杂度都不固定。
《数据结构(C语言版)》— 严蔚敏书中给出的时间复杂度为:
严蔚敏

3. 选择排序

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

3. 1 直接选择排序

  1. 在元素集合 array[0]--array[n-1]中选择关键码最大(小)的数据元素
  2. 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  3. 左右各缩小一个元素的范围,重复上述步骤,直到集合剩余1个元素

直接选择排序十分便于理解,就不进行步骤分析了。

void SelectSort(int* a, int n)
{
	int end = n - 1;
	int begin = 0;
	while (begin < end)
	{
		//mini和maxi存储找到的最大值和最小值的下标
		int mini = begin;
		int maxi = begin;
		for (int j = begin; j <= end; j++)
		{
			//遍历数组时寻找最大值和最小值的下标
			if (a[mini] > a[j])
				mini = j;
			if (a[maxi] < a[j])
				maxi = j;
		}
		//处理特殊情况,下文解释
		if (begin == maxi)
			maxi = mini;
		//交换,Swap函数是用于交换两个数的自定义函数,不过多赘述
		//注意在传参的同时,对begin和end进行了迭代
		Swap(&a[mini], &a[begin++]);
		Swap(&a[end--], &a[maxi]);
	}
}

解释一下这个特殊情况:如果begin==maxi,并且没有进行if语句中的操作会发生什么?
以这个数组为例:

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

此时maxi为0,mini为3,执行两个Swap语句,首先9和1互换,然后4和第一个数字1交换,很显然就出了问题,因为最大值被换走了,而maxi依然指向原来的位置。而maxi=mini之后的步骤就是:1和9交换,然后4和9交换,交换完成后就是:

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

这样就能顺利完成任务了。
直接选择排序的特性总结:

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

3. 2 堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
它是选择排序的一种,通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

注意:显然堆排序需要用到堆这个数据结构,其实现也不再赘述

步骤为:先将整个数组建堆,然后将堆顶和最后一个元素交换,此时可以保证最后一个元素就是最大(或最小)的,然后把end--,再进行调整,再重复上述过程,就能得到顺序的数组。

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 2) / 2; i >= 0; i--)	//只需要排序到根节点为倒数第二层的最右边的节点就可以了
		AdjustDwon(a, n, i);
	int end = n - 1;
	while (end)
	{
		//交换并调整
		Swap(&a[end], &a[0]);
		AdjustDwon(a, end, 0);
		end--;
	}
}
void AdjustDwon(int* a, int n, int root);

注:由于AdjustDown函数并没有要求传入的是堆,只是接受一个数组,所以可以这么写,当然创建一个堆来进行排序也是可以的。

void HeapSort(int* a, int n)
{
	HP hp;
	for (int i = 0; i < n; i++)
	{
		HPPush(&hp, a[i]);
	}
	int i = 0;
	while (!HPEmpty(&hp))
	{
		a[i++] = HPTop(&hp);
		HPPop(&hp);
	}
	HPDestroy(&hp);
}

最终排序的是升序还是降序与建的是大堆(升序)还是小堆(降序)有关。

另外,为什么采取向下调整算法建堆?因为向上调整算法建堆的时间复杂度是O(n*log2n),而向下调整算法建堆的时间复杂度是O(n)。其计算较为复杂,可以看一眼,没必要记:
向上
向下

3. 3 Top-K问题

(这个问题不属于数据结构排序的内容,但与堆排序有关,所以放到这里)

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

比如说我们要最大的K个数,基本思路就是建大小为K一个小堆,将前K个数据依次入堆并调整,然后开始向后遍历,如果遍历到的数据比堆顶大,就将堆顶换成这个数据,然后调整,循环进行这个过程,最后这个堆中的数据就是最大的K个数据了。

//这个函数调用一次就行了
//这个函数可以生成一个有10000个随机数的data.txt文件
void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = rand() % 1000000;	//给数据添加最大值
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

void PrintTopK(int k)
{
	FILE* fl = fopen("data.txt", "r");
	if (!fl)
	{
		perror("fopen");
		exit(1);
	}
	int* arr = (int*)malloc(k * sizeof(int));
	if (!arr)
	{
		perror("malloc");
		exit(2);
	}
	for (int i = 0; i < k; i++)
		fscanf(fl, "%d", &arr[i]);

	//建堆
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, k);
	}

	int tmp = 0;
	//遍历
	while (fscanf(fl, "%d", &tmp) != EOF)
	{
		if (tmp > arr[0])
		{
			arr[0] = tmp;
			AdjustDown(arr, 0, k);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
	}
}

这个函数中用到了文件操作和fscanf函数。
这里介绍一下fscanf函数:

int fscanf ( FILE * stream, const char * format, ... );

第一个为流,这里就是这个文件,剩下的和scanf函数是一样的格式化输入放到第三个参数中,只是是从文件的流中获取数据,而不是从标准输入流。

怎么测试这个算法的结果是否正确?
我们打开第一个函数生成的文件(这个文件怎么找可以在这篇博客中找到,不再赘述):
数据
随便找几个数据,为它们添上几位数:
添加
生成随机数时,我们给它添加了范围,所以最后的输出应该就是这几个改过的数字,这样就可以方便调试代码了。

时间复杂度:O(n)=k+(n-k)log2k

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

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

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

相关文章

小米2025届软件开发工程师(C/C++/Java)(编程题AK)

选择题好像也是25来个 编程题 T1 题目描述 小明喜欢解决各种数学难题。一天&#xff0c;他遇到了一道有趣的题目:他需要帮助他的朋友们完成一个排序任务。小明得到两个长度为 n 的数组a[]和b[]。他可以在两个数组对应位置进行交换&#xff0c;即选定一个位置 i &#xff0c…

PCL 求八叉树的体素中心

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 八叉树构建 2.1.2 获取体素中心 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xff08;长期更新&#xf…

注意力机制篇 | YOLOv8改进之在C2f模块引入EffectiveSE注意力模块 | 基于SE注意力

前言:Hello大家好,我是小哥谈。EffectiveSE(Effective Squeeze-Excitation) 是一种改进的通道注意力模块,其目的是在保持模型性能的同时减少计算复杂性和信息丢失。它基于原始的 Squeeze-Excitation (SE) 模块,但通过一些关键的改进来提高效率。🌈 目录 🚀1.基…

前端——flex布局

flex布局——弹性布局 传统布局: 浮动 定位 行内块等 1. flex布局 方法简单 不需要计算 能自动分配父级里面的子元素排版 对齐方式等等 >flex布局 可以适应不同屏幕布局 2. flex布局使用 - 给父级盒子 display: flex 开启弹性盒模型 - 子元素就会默…

栈的基本概念和及具体实现

今天给大家介绍一下栈的基本概念及实现&#xff01;话不多说&#xff0c;立即开始&#xff01; 1.栈的概念&#xff1a; 一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶&#xff0c;另一端称为栈底。栈中的…

计算机网络--TCP、UDP抓包分析实验

计算机网络实验 目录 实验目的 实验环境 实验原理 1、UDP协议 2、TCP协议 实验具体步骤 实验目的 1、掌握使用wireshark工具对UDP协议进行抓包分析的方法&#xff0c;掌握UDP协议的报文格式&#xff0c;掌握UDP协议校验和的计算方法&#xff0c;理解UDP协议的优缺点&am…

vant van-pull-refresh + van-list实现list列表支持搜索和下拉刷新

1 介绍 在使用 van-pull-refresh van-list实现list列表下拉刷新时遇到几个问题在这里进行一个总结。 2 出现的问题 问题一&#xff1a;当van-pull-refresh van-list组合使用时&#xff0c;下拉刷新会调用两个加载图标。 解答&#xff1a;去除van-pull-refresh加载图标&…

leetcode-189:轮转数组

给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4…

快讯:腾讯轻量服务器四周年,最低一折续费,还有免费升配

最近腾讯云轻量服务器四周年庆开始了&#xff0c;免费升级配置&#xff0c;续费服务器最低一折。 最低一折续费&#xff1a; 持有多久的轻量服务器决定续费几折&#xff0c;已经持有四年就是一折&#xff0c;三年1.5折以此类推。 免费升级配置&#xff1a; 2-4-5免费升级到…

String类常用的方法

源代码&#xff1a; 输出结果&#xff1a;

Linux 之 logrotate 【日志分割】

简介 logrotate 是一个用于管理日志文件的工具。它可以自动对日志文件进行轮转、压缩、删除等操作&#xff0c;以防止日志文件无限增长占用过多磁盘空间。logrotate 通常作为一个守护进程定期运行&#xff0c;也可以通过 cron 任务来调度执行 工作原理 按照配置文件中的规则…

【Mysql多数据源实现读写分离的几种方案】

文章目录 一.什么是MySQL 读写分离二.读写分离的几种实现方式(手动控制)1.基于Spring下的AbstractRoutingDataSource1.yml2.Controller3.Service实现4.Mapper层5.定义多数据源6.继承Spring的抽象路由数据源抽象类&#xff0c;重写相关逻辑7. 自定义注解WR&#xff0c;用于指定当…

客户端数JSON据库SQL操作功能实现代码-———未来之窗行业应用跨平台架构

一、前端json结构化查询优点 以下是前端本地化查询的一些优点&#xff1a; 1. 快速响应&#xff1a;无需通过网络请求从服务器获取数据&#xff0c;查询结果能够立即返回&#xff0c;提供了几乎零延迟的用户体验&#xff0c;使应用更加流畅和响应迅速。 2. 离线可用性&#x…

9.4 Linux_I/O_访问目录、文件属性

访问目录 1、打开关闭目录 打开目录函数声明如下&#xff1a; //1.直接打开指定路径的目录文件 DIR *opendir(const char *name); //2.先用open打开目录文件&#xff0c;再用该函数访问目录文件 DIR *fdopendir(int fd); 返回值&#xff1a;成功返回指向打开的目录文件的结…

ELK-05-skywalking监控SpringCloud服务日志

文章目录 前言一、引入依赖二、增加日志配置文件三、打印日志四、skywalking网页查询链路五、日志收集5.1 修改logback-spring.xml5.2 重启SpringCloud服务并请求test接口5.3 查看skywalking网页的Log 总结 前言 基于上一章节&#xff0c;现在使用skywalkin监控SpringCloud服务…

JWT令牌技术介绍及使用

一、JWT介绍 JWT是JSON Web Token的缩写&#xff0c;即JSON Web令牌&#xff0c;是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息&#xff0c;以便于从资源服务…

D. Determine Winning Islands in Race (cf div2,dp、图论最短路)

D. Determine Winning Islands in Race 思路: bfs找到E到达每个点的最短时间t[i]。 如果E要超过B&#xff0c;那么一定要借助辅助桥&#xff0c;从而获胜。 假设有u->v的辅助桥&#xff0c;E能通过这个桥超过B的条件是: s>u 且 t[v] < v-s 即 s的取值要为[u1,v-t[v]-…

C++核心编程和桌面应用开发 第七天(运算符重载 智能指针)

目录 1.数组类 2.运算符重载 2.1加号运算符 2.1.1成员函数实现 2.1.2全局函数实现 2.1.3加号重载 2.2左移运算符 2.3递增运算符 2.4指针运算符 2.5赋值运算符 1.数组类 //默认构造函数 MyArray::MyArray() {m_Size 0;m_Capacity 100;pAddress new int[m_Capacity]…

【有啥问啥】深度解析迁移学习(Transfer Learning)

深度解析迁移学习&#xff08;Transfer Learning&#xff09; 在机器学习领域&#xff0c;迁移学习&#xff08;Transfer Learning&#xff09;作为一种强大的技术&#xff0c;正广泛应用于各种实际场景中。本文将详细解析迁移学习的基本概念、原理、分类、应用场景以及具体实…

vue3中storeToRefs让store中的结构出来的数据也能变成响应式

1、首先需要安装pinia 具体安装和使用教程参考 2、创建 src/stores/counter.js 文件&#xff0c;其内容如下&#xff1a; import {defineStore} from "pinia"; import {ref} from "vue";export const useCounterStore defineStore(counter,()>{const…