数据结构-排序(2)

news2024/11/17 16:51:35

前言:

上一章节介绍了 排序中的插入排序和选择排序, 分别复盘了插入排序中的直接插入排序和希尔排序以及选择排序中的选择排序和堆排序。今天继续复盘交换排序。


目录

2.3交换排序

2.3.1冒泡排序

2.3.2快速排序

2.3.2快速排序非递归 



2.3交换排序

 基本思想:

     所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.3.1冒泡排序
 

冒泡排序,已经很熟悉了,是我们接触的第一个排序,初学的时候,我单独对其进行了复盘,这里就不多介绍,可以根据下面动图和程序进行知识和程序的对比回忆:

 

程序代码: 

void BubbleSort(int* a, int n)
{
	
	for (int i = 0; i < n; i++)
	{
		bool exchange = false;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])//遍历到n - 1 会产生越界 所以这个地方要么 j = 1 前一个减一 要么遍历条件需要注意
			{
				swap(&a[j], &a[j + 1]);
				exchange = true;
			}
		}
		if (exchange == false)
		{
			break;
		}
	}
}

 分析总结:

冒泡排序,通过两层遍历,第一层遍历数组中的每个元素,第二层遍历负责每个元素都要跟相邻的元素进行比较,找到自己的位置;所以当给的元素无序的时候,时间复杂度为o(N^2) 如果是有序的数组的话,我们可以通过设置标志位判断,减少时间复杂度,让其变为O(N)。

2.3.2快速排序

 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

 1.hoare版本

霍尔大佬思想就是快排最初版本,图解如下:

 

霍尔大佬

  • 寻找合适的基准值key

基准值可以是数组中的任意元素,但是为了程序方便,我们一般会选择两端。

  • 2.定义两个下标L和R,我们让R先走,R从数组的右边往左边遍历,寻找比基准值小的数,L从左往右遍历,寻找比基准值大的元素,找到之后交换两个位置的元素。

开始出发进行寻找:

 找到元素后进行交换:

 

  • 3.当两个下标相遇的时候,交换该下标的元素和基准值,基准值所在的位置就是它应该在的位置

 

 至此,霍尔大佬的单趟排序就完成,单趟排序我们确定了基准值key的最终位置,左边都比基准值小,右边都比基准值大,所以该位置为其最终位置,接下来我们使用二分的思想以。此基准值为分界点,将数组分成两部分,基准值的左区间和右区间,然后使用递归思想左区间,使用单趟排序,找到基准值,右区间同样,以此类推,结束条件就是区间不能划分,也就确定了最后一个key的位置,然后层层归回来。如下图所示:

代码如下: 

int PartSort1(int* a, int left, int right)
{

	int keyi = left;

	while (left < right)
	{
		while (left<right && a[right] >= a[keyi]) // 要包含等于的条件 从右往左遍历
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi]) 从左往右遍历
		{
			left++;
		}
		swap(&a[right], &a[left]);  //交换两个位置的数
	}
	swap(&a[left], &a[keyi]); 相遇后,相遇的位置就是key应该在的位置
	keyi = left;
	
	return keyi;
}

 复杂度分析:

因为是对数组进行二分,所以层数不会很多,只要分就会确定key,如下图示:

层数可以确定的是 logn,每一层都要进行寻找大小值;

这里需要注意的是,第一次,一个key 第二次就两个 以此 那就是 2^2,2^3.......每一次都要减掉确定的key 可以用具体数字来看,100w个数据 有 20层,第一层 1个k,第二十层 2^19 ,层数很少,所以寻找大小的次数累和加起来 可以近似看成 100w  上升到n个数据 时间复杂度也就是N*LOGN

通过分析我们可以注意到,如果 该数组是个有序数组,就会出现每一次确定的key只能在边上,这样时间复杂度就会上升到 O(N^2),为了避免这样的问题,我们可以将key的选取进行优化,选取一个合适的key,具体代码如下;

//三个数,通过比较得到中间的数
int  GetmidNumi(int* a, int left, int right)
{
	int mid = (right - left) / 2;

	if (a[left] > a[mid])
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

 刚开始学的时候,我一直有个疑问,为什么左边为key的时候,右边要先走?为什么相遇的时候,一定是比key小的数或者和key相等,通过敲代码以及,画图分析,我明白了。具体分析如下:

为了演示,我只用四个数进行分析

因为左边是寻找大的值,右边是寻找小的值,如果L先走,相遇的时候,有可能会出现前面完成过交换,左遇右这样交换就会造成比key小的值虽然交换到左边,但是相遇后,key的值有可能交换到大的值得右边,如下图所示:

 右边先走的情况:只有两种情况

情况1:

r找到小,l没有找到大,相遇在r处 交换 满足

情况2:

r找小没找到,直接遇到l,要么就是bikey小的位置,要么就是key 。

2.挖坑法

   挖坑法就是在霍尔大佬的基础上,进行的改版,把基准值拿出来,让他的位置为坑,然后右边向左遍历寻找较小值,放进坑里然后自己成为新的坑,左边向右寻找较大值,交换后成为新的坑,两个坐标一定会在坑里相遇,然后将基准值填入。 示意图如下:

 

坑位法

没有特别大的变动,代码如下:

// 挖坑法
int PartSort2(int* a, int left, int right)
{
	// 三数取中
	int mid = GetmidNumi(a, left, right);
	if (mid != left)
		swap(&a[left], &a[mid]);

	int keyi = a[left];//保存left内部数值,并将其当做坑位
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= keyi)
		{
			right--;
		}
		swap(&a[right], & a[hole]);
		hole = right;
		while (left < right && a[left] <= keyi)
		{
			left++;
		}
		swap(&a[left], &a[hole]);
		hole = left;
	}
	a[hole] = keyi;
	//swap(&a[left], &keyi);
	return hole;
}

3.前后指针法

前后指针法,是针对前面提到的,左基值,右先走,右基值,左先走,混淆的情况做的改进,利用prev和cur两个前后指针,cur指针往前遍历,寻找比基值小的数 往前堆和prev交换,遍历完毕后,所有比基值小的数都堆在前面,这时候prev所在的位置是最后一个比基值小的数,将其和基值替换,就确定了基值的最终位置,动态图解如下:

前后指针法

 

如果cur的位置元素比基值小,prev和cur相邻的话,prev右移和cur下标相同的话,就不需要交换,同位置,如下图所示:

 

整体变化,交换如下: 

 

代码如下: 

// 前后指针法
int PartSort3(int* a, int left, int right)
{
	// 三数取中
	int mid = GetmidNumi(a, left, right);
	if (mid != left)
		swap(&a[left], &a[mid]);

	int keyi = left; // 方便返回下标
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur) // 需要注意的是 即使他俩相邻的时候,不交换prv也是要++的 这点主要不要再次犯错
		{
			swap(&a[cur], &a[prev]);
		}
		++cur;
	}
	swap(&a[prev], &a[keyi]);
	keyi = prev;

	return keyi;

}

 

 快速排序,单趟排序确定一个key位置,但是他就像堆一样,二分二分之后,最后一层节点个数占了整体节点的一半,如果还去递归调用的话,会增加时间复杂度,所以我们可以小区间优化一下,在最后基层的时候,采用插入排序,优化如下:

void QuickSort1(int* a, int left, int right)
{
	// 结束递归条件
	if (left >= right) 
		return;
	// 单趟确定 keyi位置
	
	//	// 小区间优化--小区间直接使用插入排序
	if ((right - left + 1) > 10)
	{
		int keyi = PartSort3(a, left, right);
		//int keyi = PartSort1(a, left, right);
       //int keyi = PartSort2(a, left, right);
		QuickSort1(a, left, keyi - 1);
		QuickSort1(a, keyi + 1, right);
	}

	else
	{
		InsertSort(a + left, right - left + 1);
	}

}

2.3.2快速排序非递归 

递归的思想是层层调用,但内存空间是有限的,层层调用 ,就会面对栈溢出的问题,所以解决大数据快排,引入了快排非递归,通过模拟栈,将需要递归的区间,存进栈内,然后取出确定key的位置,然后将其左右区间存入栈,再次取出确定下个区间的key位置,直到剩一个元素不用存入栈内。数组也就有序了。整体思想还是跟快排递归一样,只是将开辟数组,变成入栈的时候,开辟区间左右端下标的空间,大大减少空间的开辟。

图解如下:

 

 

代码如下:

void QuickNorSort(int* a, int left, int right)
{
	//定义一个栈, 并初始化
	ST st;
	StackInit(&st);
	// 将数组左右下标入栈
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, begin, end);

		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);

		}
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}

	StackDestroy(&st);

}

 

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

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

相关文章

HTML5 <figure> 标签、HTML5 <footer> 标签

HTML5 <figure> 标签 实例 使用 <figure> 元素标记文档中的一个图像&#xff1a; <figure><img src"img_pulpit.jpg" alt"The Pulpit Rock" width"304" height"228"> </figure>尝试一下 浏览器支持 …

在proteus中仿真arduino实现矩阵键盘程序

矩阵键盘是可以解决我们端口缺乏的问题&#xff0c;当然&#xff0c;如果我们使用芯片来实现矩阵键盘的输入端口缺乏的问题将更加划算了&#xff0c;本文暂时不使用芯片来解决问题&#xff0c;而使用纯朴的8根线来实现矩阵键盘&#xff0c;目的是使初学者掌握原理。想了解使用芯…

Lua脚本

目录说明什么是Lua脚本为什么要使用Lua脚本Lua脚本的安装Lua脚本的使用Lua的变量Lua脚本的算术运算符Lua脚本的关系运算符Lua脚本的逻辑运算符Lua脚本不同的操作Lua脚本的函数和标准库Redis整合Lua脚本&#xff08;重点&#xff09;在Java集成Lua在SpringBoot项目中使用Redis集…

前端PC端适配,网页端适配

问题背景 由于我司是使用的大屏&#xff0c;且设计稿尺寸为19201080。但是需要适配各种分辨率&#xff0c; 比如12801024(5:4)、1366768(16&#xff1a;10)、16801050&#xff08;16&#xff1a;10&#xff09;。在尝试了多种方法之后&#xff0c;最终确定主要的适配方法为rem…

【vue3】04-vue基础语法补充及阶段案例

文章目录vue基础语法补充vue的computedvue的watch侦听书籍购物车案例vue基础语法补充 vue的computed computed&#xff1a;用于声明要在组件实例上暴露的计算属性。&#xff08;官方文档描述&#xff09; 我们已经知道&#xff0c;在模板中可以直接通过插值语法显示一些data中…

科学的演变:从笛卡尔到生成式人工智能

编者按&#xff1a;本文主要介绍了科学的演变历史&#xff0c;从笛卡尔到生成式人工智能。文章探讨了数学在验证科学原理中的作用&#xff0c;并介绍了新机器学习工具如何验证新的科学。 文中提到&#xff0c;将生成式人工智能与Excel或iPhone进行比较是低估了这一新技术的潜在…

【AI】PaddlePaddle实现自动语音识别

文章目录文档背景安装环境Python版本pip环境安装模型需要的环境项目目录结构数据准备生成数据字典数据预处理训练模型创建模型构建模型的目的模型黑盒在模型中充当什么角色解码方法总结文档背景 学习AI的过程中&#xff0c;难免会出现各种各样的问题。比如&#xff0c;什么样的…

制造业生产管理系统(500强制造企业数字化实践)

前言 制造业是国民经济的支柱产业之一&#xff0c;随着科技和数字化的发展&#xff0c;制造业正在经历着一场新的变革。传统的制造模式已经无法满足市场的快速变化和客户的多样化需求&#xff0c;制造企业急需通过数字化和智能化转型升级&#xff0c;提高生产效率和质量水平&a…

第十四届蓝桥杯嵌入式详解

目录 第一部分 客观试题&#xff08;15 分&#xff09; 不定项选择&#xff08;1.5 分/题&#xff09; 第二部分 程序设计试题&#xff08;85 分&#xff09; 2.1 STM32CubeMX初始化配置 2.1.1 配置GPIO 2.1.2 配置ADC 2.1.3 配置RCC 2.1.4 配置定时器TIM 2.1.5 配置ADC1、AD…

【从零开始学Skynet】基础篇(二):了解Skynet

1、节点和服务 在下图所示的服务端系统中&#xff0c;每个Skynet进程&#xff08;操作系统进程&#xff09;都称为一个节点&#xff0c;每个节点都可以开启数千个Lua服务&#xff0c;每个服务都是一个Actor。不同节点可以部署在不同的物理机上&#xff0c;提供分布式集群的能力…

Velocity入门到精通(上篇)

最近自己所做的项目使用到这个Velocity模板引擎&#xff0c;分享一下在互联网找的学习资料。 目录 一. velocity简介 1. velocity简介 2. 应用场景 3. velocity 组成结构 二. 快速入门 1. 需求分析 2. 步骤分析 3. 代码实现 3.1 创建maven工程 3.2 引入坐标 3.3 编…

Redis锁的租约问题

目录Redis的租约问题Redis租约问题的想法Redis租约问题的解决方案Redis的租约问题 首先我们先来说一说什么是Redis的租约问题。   在我们实现Redis分布式锁的时候&#xff0c;我们会出现Redis锁的时间<业务执行执行时间&#xff0c;这其实就是一个典型的租约问题&#xf…

【C++】你了解命名空间吗?

C语言之父&#xff1a;Bjarne Stroustrup博士(本贾尼) 当我们在编写代码的时候&#xff0c;可能会产生一些命名冲突&#xff0c;为了解决这一冲突我们引出命名空间的概念 (ps:命名冲突的产生主要包括两个方面原因&#xff1a;1、与库函数名冲突&#xff1b;2、相互之间的冲突&…

【LeetCode】剑指 Offer 51. 数组中的逆序对 p249 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/ 1. 题目介绍&#xff08;51. 数组中的逆序对&#xff09; 在数组中的两个数字&#xff0c;如果前面一个数字大于后面的数字&#xff0c;则这两个数字组成一个逆序对。输入一个数组&#xf…

python3 DataFrame一些好玩且高效的操作

pandas在处理Excel/DBs中读取出来&#xff0c;处理为DataFrame格式的数据时&#xff0c;处理方式和性能上有很大差异&#xff0c;下面是一些高效&#xff0c;方便处理数据的方法。 map/apply/applymaptransformagg遍历求和/求平均shift/diff透视表切片&#xff0c;索引&#x…

VS Code 将推出更多 AI 功能给 Java 开发者

大家好&#xff0c;欢迎来到我们的二月更新&#xff01;我们将为您带来与 JUnit 5 并行测试相关的新功能以及用于 Spring Boot Dashboard 的过滤功能。另外&#xff0c;OpenAI 和 ChatGPT 是最近的热点&#xff0c;所以在 GitHub Copilot 方面也有一些令人激动的消息&#xff0…

【郭东白架构课 模块二:创造价值】19|节点二:架构活动的目标为什么常常被忽略?

你好&#xff0c;我是郭东白。从这节课开始&#xff0c;我们就进入到架构活动第二个环节的学习&#xff0c;那就是目标确认。 为架构活动确认一个正确目标&#xff0c;是架构师能为架构活动做出最大贡献的环节。从我的个人经验来看&#xff0c;一大半架构活动的目标都不具备正…

类文件具有错误的版本 55.0, 应为 52.0

最近在编译时报如下错误 java: 无法访问com.xx错误的类文件: /xxx.jar!/aa.class类文件具有错误的版本 55.0, 应为 52.0请删除该文件或确保该文件位于正确的类路径子目录中。 原来我依赖的jar包的编译版本是jdk11,而我本地代码编译的版本的jdk1.8,两个版本不一致&#xff0c;所…

C++类和对象终章——友元函数 | 友元类 | 内部类 | 匿名对象 | 关于拷贝对象时一些编译器优化

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;友元&#x1f33a;概念&#x1f33a;友元函数&#x1f341;友元函数的重要性质&#x1f33a;友元类&#x1f341;友元类的重要性质&#x1f337;内部类&#xff08;不常用&#xff09;&#x1f33a;内部类的性…

Ubuntu 下载并切换Python默认版本(无痛顺畅版)

Ubuntu 下载并切换Python默认版本的方法 文章目录Ubuntu 下载并切换Python默认版本的方法一&#xff0c;前言二&#xff0c;在ubantu中下载指定python版本1&#xff0c;更新apt版本为最新2&#xff0c;安装software-properties-common3&#xff0c;将 deadsnakes PPA 添加到你的…