快速排序及归并排序的实现与排序的稳定性

news2024/11/15 15:53:23

目录

快速排序

一. 快速排序递归的实现方法

1. 左右指针法

步骤思路

为什么要让end先走?

2. 挖坑法

步骤思路

3. 前后指针法

步骤思路

二. 快速排序的时间和空间复杂度

1. 时间复杂度

2. 空间复杂度

三. 快速排序的优化方法

1. 三数取中优化

2. 小区间优化

四. 使用栈来实现非递归快排

步骤思路

归并排序

​编辑

一. 归并排序的递归实现

步骤思路

二. 时间复杂度与空间复杂度

1. 时间复杂度

2. 空间复杂度

三. 非递归实现归并排序

步骤思路

排序算法的稳定性


快速排序

一. 快速排序递归的实现方法

1. 左右指针法
步骤思路

(假设排升序)将数组a最左边的下标用begin记录下来,最右边用end记录下来,定义一个key为begin或end

(假设key定义为begin)end向左查找找到<a[key]的数停下begin再向右查找找到>a[key]的值停下,此时将begin指向的值与end指向的值交换,以此类推直到end的值<=begin,将此时的a[key]与begin与end相遇坐标的值交换,我们发现此时的a[key],左边的值都比其小,右边的值都比其大,那就说明key所指向的值在数组中已经排好位置了

如以下代码,即完成了单趟

		int key = left;
		int begin = left, end = right;
		while (begin < end)
		{
			while (a[end] >= a[key] && begin < end)
			{
				end--;
			}
			while (a[begin] <= a[key] && begin < end)
			{
				begin++;
			}

			Swap(&a[begin], &a[end]);
		}
		Swap(&a[key], &a[begin]);

我们在end和begin寻找比a[key]大或小的值的时候不要忘记也要判断循环成立的条件

既然key已经在数组排好位置,我们接下来递归就不需要加上key了,只需要递归key的左右区间即可,直到递归的区间左边与右边相等即只有一个数

完整代码如下

void QuickSort1(int* a, int left,int right)
{

		if (left >= right)
			return;
		int mid = GetMid(a, left, right);
		Swap(&a[mid], &a[left]);

		int key = left;
		int begin = left, end = right;
		while (begin < end)
		{
			while (a[end] >= a[key] && begin < end)
			{
				end--;
			}
			while (a[begin] <= a[key] && begin < end)
			{
				begin++;
			}

			Swap(&a[begin], &a[end]);
		}
		Swap(&a[key], &a[begin]);
		QuickSort1(a, left, begin - 1);
		QuickSort1(a, begin + 1, right);
}
为什么要让end先走?

左边做key右边先走,可以保证相遇位置比key小
相遇场景分析

begin遇end:end先走,停下来,end停下条件是遇到比key小的值,end停下来的位置一定比key小,begin没有找到大的遇到end停下了
end遇begin:end先走,找小,没有找到比key更小的,直接跟begin相遇了。begin停留的位置是上一轮交换的位置(即,上一轮交换,把比key小的值,换到begin的位置了)
同样道理让右边做key,左边先走,可以保证相遇位置比key要大  

2. 挖坑法

步骤思路

(假设排升序,给数组a)将最左边的值定义key存储起来,最左边的下标用bigen记录,最右边的下标用end记录,定义pivot记录为最左边的下标,即将最左边视为坑位

然后end向左寻找比key小的值放到pivot所指向的位置即坑位中,并将这个地方(end所找到的)视作新的坑(更新pivot的值)。

begin向右寻找比key大的值,放到坑位中,并将这个地方视作新的坑(更新pivot的值)

重复以上步骤直到end<=begin

然后将key填进pivot中,再通过递归,即可完成排序

由于与左右指针法类似就不写单趟,直接上完整代码

void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;

	int key = a[left];
	int begin = left, end = right;
	int pivot = left;

	while (begin < end)
	{
		while (a[end] >= key && begin < end)
		{
			end--;
		}
		a[pivot]=a[end];
		pivot = end;
		while (a[begin] <= key && begin < end)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	a[pivot] = key;
	QuickSort2(a, left, pivot - 1);
	QuickSort2(a, pivot + 1, right);
}
3. 前后指针法

步骤思路

(假设排升序)定义key为数组最左边的下标,并定义,prev=key与after=key+1

after在找到比key指向的值小的值时,prev++,并将after指向的值与现在的prev(即prev++后的值)交换

以此往复,直到after>数组的值

然后将prev所指向的值与key所指向的值交换

代码如下

我们要注意,当prev++后的值==after就会发生与自身交换

完成一次后,效果依然是a[key]左区间的值比其小,右区间的值比其大

	int key = left;
	int prev = left, after = left + 1;

	while (after<=right)
	{
		while (a[after] < a[key]&&++prev!=after)
		{
			Swap(&a[prev], &a[after]);
		}
		after++;
	}
	Swap(&a[prev], &a[key]);

递归是和上面两种方法同样的道理

完整代码如下

void QuickSort3(int* a,int left,int right)
{
	if (left >= right)
		return;

	int key = left;
	int prev = left, after = left + 1;

	while (after<=right)
	{
		while (a[after] < a[key]&&++prev!=after)
		{
			Swap(&a[prev], &a[after]);
		}
		after++;
	}
	Swap(&a[prev], &a[key]);
	QuickSort3(a, left, prev - 1);
	QuickSort3(a, prev + 1, right);
}

二. 快速排序的时间和空间复杂度

1. 时间复杂度

①最好情况

每次的划分都使得划分后的子序列长度大致相等,一般在数据已经部分有序或者随机分布的情况下发生。此时时间复杂度为O(Nlog₂N)

②最坏情况

待排序序列有序的情况下,每一次划分的两个区间都有一个为0,此时快速排序的时间复杂度退化为O(N²)

③平均情况

实际应用中快速排序的平均情况大概会接近于最好情况,因为待排序序列通常不是有序的,我们还可以通过三数取中来优化,减少最坏情况的可能性,所以快速排序的时间复杂度为O(Nlog₂N)

2. 空间复杂度

由于需要递归调用,相当于求递归树的深度,

①最坏情况

当数组接近有序时,递归深度很深,空间复杂度为O(N)

②最好情况

当数组无序时,递归树基本相当与完全二叉树,空间复杂度为O(log₂N)

③平均情况

实际应用中,平均情况大概会接近最好情况,同样可以用三数取中优化

所以快速排序空间复杂的为O(log₂N)

三. 快速排序的优化方法

1. 三数取中优化

为了让每次左右区间长度接近,我们可以使用三数取中,即最左边最右边与中间的值取不大也不小的一个值并返回

int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] < a[right])//上面if条件不成立可得a[right]<a[mid]
			return right;
		else//又可得 a[left] > a[right]
			return left;
	}
	else//a[left]>=a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left]<  a[right])//上面if条件不成立可得a[right]>a[mid]
			return left;
		else//又可得 a[left] < a[right]
			return right;
	}

}

将返回值接收并将其指向位置与最左边的值交换,代码如下

		if (left >= right)
			return;
		int mid = GetMid(a, left, right);
		Swap(&a[mid], &a[left]);
		int key = left;
2. 小区间优化

当快速排序要排的数据很长时,越递归到后面区间越小递归的层数越多,我们可以考虑,当要递归区间小于10的时候用别的排序来代替,这样就可以省去80%到90%的递归

代码如下

void QuickSort1(int* a, int left,int right)
{
	if ( (right-left+1)<10)//小区间优化
	{
		InsertSort(a+left, right - left + 1);
		//a+left 有可能是后半段区间
		//减少递归层数
	}
	else
	{
		if (left >= right)
			return;
		int mid = GetMid(a, left, right);
		Swap(&a[mid], &a[left]);

		int key = left;
		int begin = left, end = right;
		while (begin < end)
		{
			while (a[end] >= a[key] && begin < end)
			{
				end--;
			}
			while (a[begin] <= a[key] && begin < end)
			{
				begin++;
			}

			Swap(&a[begin], &a[end]);
		}
		Swap(&a[key], &a[begin]);
		QuickSort1(a, left, begin - 1);
		QuickSort1(a, begin + 1, right);
	}
}

四. 使用栈来实现非递归快排

栈的实现可以看一下我以前的博客

栈的实现详解-CSDN博客

步骤思路

初始化栈后,将数组的最右边与最左边分别放入栈(即将一个区间放入栈中)

进入循环(当栈为空时循环结束),用begin和begin1接收栈顶端的值,再删除栈的值,再用end和end1接收栈顶端的值,再删除栈的值,使用左右指针法(挖坑法,前后指针法皆可)(用begin与end来寻找值,begin1与end1不变)进行一趟排序,

如果right1>=begin+1 就往栈里存 right1(当前排序区间的最右边) 和 begin+1 反之不存

如果left1<=begin-1 就往栈里存  begin-1 和 left1(当前排序区间的最左边)  反之不存

最后不要忘记销毁栈

代码如下

void StackQuickSort(int* a, int left, int right)
{
	ST s;
	StackInit(&s);
	StackPush(&s, right);
	StackPush(&s, left);

	while (!StackEmpty(&s))
	{
		int begin = StackTop(&s);
		int left1 = begin;
		StackPop(&s);
		int end = StackTop(&s);
		int right1= end;

		StackPop(&s);
		int key = begin;

		//int mid = GetMid(a, begin, end);
		//Swap(&a[mid], &a[begin]);


		while (begin < end)
		{
			while (a[end] >= a[key] && begin < end)
			{
				end--;
			}
			while (a[begin] <= a[key] && begin < end)
			{
				begin++;
			}
			Swap(&a[begin], &a[end]);
		}
		Swap(&a[key], &a[begin]);
		if(right1>=begin+1)
		{
			StackPush(&s,right1);
			StackPush(&s, begin + 1);
		}
		if(left1<=begin-1)
		{
			StackPush(&s, begin - 1);
			StackPush(&s, left1);
		}
	}
	StackDestroy(&s);
}

归并排序

一. 归并排序的递归实现

步骤思路

malloc一个临时数组进入子函数(创建子函数递归会更方便),进行递归,子函数利用分治思想一直递归直到left>=right 开始执行下面操作

k赋初值为当前区间最左边begin1 , end1来记录左数组最左边和最右边,定义begin2 ,end2 来记录右数组的最左边和最右边,将两个数组从头比较,较小的赋值给临时数组,直到有一方赋完值,再将没赋完值的数组给临时数组赋值。最后给要排序数组left到right赋值为临时数组left到right

代码如下

//递归
void _MergeSort(int* a,int* tmp, int left, int right)
{
	if(left>=right)
	{
		return;
	}
	int mid = (left + right) / 2;
	//如果[left,mid][mid+1,right]有序就可以归并了
	_MergeSort(a,tmp, left, mid);
	_MergeSort(a,tmp, mid + 1, right);
	int begin1 = left;
	int end1 = mid;

	int begin2 = mid + 1;
	int end2 = right;

	int k=left;
	while (begin1 <= end1&&begin2<=end2)
	{
		if(a[begin1]<a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
		{
			tmp[k++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[k++] = a[begin1++];
	}
	while (begin2 <= end2)
	{ 
		tmp[k++] = a[begin2++];
	}
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}

}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//_MergeSort(a, tmp, 0, n - 1);
	_MergeSort2(a,tmp,  n);

	free(tmp); 
	tmp = NULL;
}

二. 时间复杂度与空间复杂度

1. 时间复杂度

归并排序的时间复杂度是稳定的,不受输入数组的初始顺序影响

 将数组分成两个子数组的时间复杂度为O(1),递归对子数组进行排序,假设每个子数组长度为n

则两个子数组排序的总时间复杂度为O(NlogN),将两个有序数组合并为一个有序数组时间复杂度为O(N),所以归并排序时间复杂度为O(NlogN)

2. 空间复杂度

调用栈所需要的额外空间为O(logN),因为我们需要一个额外数组来存储数据所以又额外消耗O(N)的空间,我们将较小的O(logN)忽略可以得到归并排序的空间复杂度为O(N)

三. 非递归实现归并排序

步骤思路

开辟动态空间后定义一个数gap=1来控制区间(gap相当于每组数据个数),(每一次gap*2,使每次区间扩大)gap<数组长度

设计一个for循环i+=gap*=2

每次分两组[i][i+gap-1]和[i+gap][i+2*gap-1]  (i每次+=正好跳过这些数据)

将两个区间的值比较放入新开辟的数组,再拷贝到原数组

代码如下

//非递归
void _MergeSort2(int* a,int* tmp,int n)
{
	int gap = 1;
	while(gap<n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;;

			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			int k = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = a[begin2++];
			}
			//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
			for (int j = i; j < k; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}
}

但是我们发现,这样如果会发生越界的现象

一共三种可能

1. [begin1,end1][begin2,end2]  end2越界
2. [begin1,end1][begin2,end2]  begin2,end2越界
3. [begin1,end1][begin2,end2]  end1,begin2,end2越界

 第2,3种我们可以直接不递归了,因为后面区间的不存在前面区间的在上一次已经递归好了,

第一种呢我们需要把区间(即end)给修正一下

修正代码如下

//非递归
void _MergeSort2(int* a,int* tmp,int n)
{
	int gap = 1;
	while(gap<n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;;

			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			int k = i;

			if (begin2 >= n)//第二种情况,第二组不存在,不需要归并
				break;

			if (end2 >= n)//第一种情况,需要修正一下
				end2 = n - 1;


			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[k++] = a[begin1++];
				}
				else
				{
					tmp[k++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = a[begin2++];
			}
			//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
			for (int j = i; j < k; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}
}

排序算法的稳定性

假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变

原序列中 r[i]=r[j],且r[i]在r[j]之前而在排序后的序列中r[i]仍在r[j]前,则称这种排序算法是稳定的,否则是不稳定的

冒泡选择稳定
选择排序不稳定***只会考虑自身,假如找到最小值1下标为3,将其与下标为0(假设此处为6)处交换若下标为1处也是6,就改变了
直接插入排序稳定
希尔排序不稳定(分组)预排序时相同的值可能分到不同的组
堆排序不稳定建堆时可能就乱了
归并排序稳定当两个数相等,让第一个下来就是稳定的(可以控制)
快速排序不稳定end先找到 j 和begin交换了,在找到 i 和bigin交换,显然改变了

这篇文章就到这里了,感谢大家阅读

(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤ 

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

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

相关文章

【C++】拷贝构造函数及析构函数

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

超详细信息收集篇

1 域名信息收集 1.1 域名是什么 域名&#xff08;英语&#xff1a;Domain Name&#xff09;&#xff0c;又称网域&#xff0c;是由一串用点分隔的名字组成的 Internet 上某一台 计算机 或计算机组的名称&#xff0c;用于在数据传输时对计算机的定位标识&#xff08;有时也指地…

学习分布式事务遇到的小bug

一、介绍Seata 在处理分布式事务时我用到是Seata&#xff0c;Seata的事务管理中有三个重要的角色&#xff1a; TC (Transaction Coordinator) - 事务协调者&#xff1a;维护全局和分支事务的状态&#xff0c;协调全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器…

DockerHub无法拉取镜像怎么办

快速构建企业级AIGC项目 LangChat是Java生态下企业级AIGC项目解决方案&#xff0c;在RBAC权限体系的基础上&#xff0c;集成AIGC大模型功能&#xff0c;帮助企业快速定制知识库、企业机器人。 网站文档&#xff1a;Index – LangChat 后台地址&#xff1a;LangChain Chat 前台…

3.5、matlab打开显示保存点云文件(.ply/.pcd)以及经典点云模型数据

1、点云数据简介 点云数据是三维空间中由大量二维点坐标组成的数据集合。每个点代表空间中的一个坐标点&#xff0c;可以包含有关该点的颜色、法向量、强度值等额外信息。点云数据可以通过激光扫描、结构光扫描、摄像机捕捉等方式获取&#xff0c;广泛应用于计算机视觉、机器人…

Redis之List列表

目录 一.列表讲解 二.列表命令 三.内部编码 四.应用场景 Redis的学习专栏&#xff1a;http://t.csdnimg.cn/a8cvV 一.列表讲解 列表类型是用来存储多个有序的字符串&#xff0c;如下所示&#xff0c;a、b、c、d、e五个元素从左到右组成了一个有序的列表&#xff0c;列表中的…

【连续四届EI检索|稳定ACM出版、EI检索|线上线下结合】2024年第五届医学人工智能国际学术会议(ISAIMS 2024,8月13-17)

第五届医学人工智能国际学术会议&#xff08;ISAIMS2024&#xff09;将于2024年8月13-17日于荷兰阿姆斯特丹自由大学召开&#xff0c;国内分会场将于2024年10月25-27日于中国武汉召开。 会议自2020年至今已经成功举办四届&#xff0c;吸引了来自海内外相关领域学者600余名。本届…

贪心算法(2024/7/16)

1合并区间 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1&#xff1a; 输入&#xff1a;inter…

MongoDB教程(七):mongoDB分片

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言MongoDB 分…

Mysql数据库的概念及sql语法和规范+数据库的用户管理

一、数据库的概念 1.数据库&#xff1a;组织、管理、存储数据的仓库 2.数据库的管理系统&#xff08;DBMS&#xff09;&#xff1a;实现对数据有效组织&#xff0c;管理和存储的系统软件。 3.关系型数据库和非关系性数据库&#xff1a; 关系型数据库&#xff1a;mysql or…

神经网络概述

目录 1. 前馈神经网络(Feedforward Neural Networks, FNNs) 2. 卷积神经网络(Convolutional Neural Networks, CNNs) 3. 循环神经网络(Recurrent Neural Networks, RNNs) 4. 长短期记忆网络(Long Short-Term Memory, LSTM) 5. 门控循环单元(Gated Recurrent Unit…

MSPM0G3507——时钟主频拉到80MHZ

先点开使用时钟树 在配置时钟界面这样配置

【送书活动十期】从零开始node.js制作CLI工具

这篇博客的由来是源于工作中一个java项目的配置项是加密后的私钥&#xff0c;私钥是由其他项目中调用web3生成随机账号得到的&#xff0c;而加密方法只是简单在java项目中执行代码得到。这便导致两步操作有点割裂&#xff0c;需要有一个脚本来完成生成私钥和加密私钥&#xff0…

[Tensor学习]你不得不知道的知识点-切点-反转

wait...突然发现了一个错误&#xff1a; 在tensor里面只有size相同才允许相加&#xff0c; 如果想要相连接&#xff1a; PS: 如果tensor是多维的&#xff0c;比如说 a Tensor([1,2,3], [4,5,6]) 那么有 a[ : , :] a a[0,0] 1 a[ 第一维 &#xff0c;第二维] ...…

乘积量化pq:将高维向量压缩 97%

向量相似性搜索在处理大规模数据集时&#xff0c;往往面临着内存消耗的挑战。例如&#xff0c;即使是一个包含100万个密集向量的小数据集&#xff0c;其索引也可能需要数GB的内存。随着数据集规模的增长&#xff0c;尤其是高维数据&#xff0c;内存使用量会迅速增加&#xff0c…

MAC地址电脑的“身份证”

在电脑的世界里&#xff0c;每个设备都有一个独一无二的标识&#xff0c;就像我们的身份证号码一样。 这个标识被称为MAC地址&#xff08;Media Access Control Address&#xff09;&#xff0c;它是网络设备硬件的唯一识别码。 MAC地址由12个十六进制数字组成&#xff0c;通…

区块链资料

Quantstamp - Public Security Assessments smart-contract-sanctuary-bsc/contracts/mainnet at master tintinweb/smart-contract-sanctuary-bsc GitHub https://github.com/slowmist/Cryptocurrency-Security-Audit-Guide/blob/main/README_CN.md sFuzz: 高效自适应的智…

ArkUI组件——循环控制/List

循环控制 class Item{name: stringprice:number}private items:Array<Item> [new Item("A0",2399),new Item("BE",1999),new Item("Ro",2799)] ForEach(this.items,(item:Item) > {})List组件 列表List是一种复杂的容器&#xff0c;…

小程序里面使用vant ui中的vant-field组件,如何使得输入框自动获取焦点

//.wxml <van-fieldmodel:value"{{ userName }}"placeholder"请输入学号"focus"{{focusUserName}}"/>// .js this.setData({focusUserName: true});vant-field

postgresql删除用户

背景 **角色与用户**&#xff1a;在 PostgreSQL 中&#xff0c;用户和组的概念是通过“角色”来统一实现的。角色可以有登录权限&#xff08;在这种情况下&#xff0c;它们通常被称为“用户”&#xff09;&#xff0c;也可以没有&#xff08;在这种情况下&#xff0c;它们通常用…