排序算法:快速排序(三种排序方式、递归和非递归)

news2025/1/15 16:28:05

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关排序算法的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

目录

前言:

1.快速排序

1.1递归版本

1.1.1hoare版本

代码演示:

1.1.2挖坑法

 代码演示:

1.1.3前后指针(下标)版本

 代码演示:

1.1.4时间复杂度 

1.1.5快速排序的优化

优化完整代码: 

 1.2非递归版本

代码演示: 


前言:

在前面的文章我们分别介绍了插入排序和选择排序,那么在本期的学习中我们来了解一下快速排序,以及快速排序的三种实现方式以及递归和非递归的实现,话不多说,正文开始:

快速排序和冒泡排序是属于交换排序这个范畴内的,冒泡排序在前面的文章中非常细致的讲解过,那么在这里就不做赘述,直接开始快速排序即可:

1.快速排序

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

1.1递归版本

首先我们使用递归的方式来实现快速排序,后面再使用非递归来完成,因为递归太深是会有风险的。在递归版本的快速排序下对于排序区间的划分有三种方法:1.hoare版本、2.挖坑法、3.前后指针法。那么我们来一一学习。

1.1.1hoare版本

我们要对一段数据进行排升序的处理:首先我们取一个关键值key(基准值),那么我们一般取的是最左边或者最右边的数据,然后设置两个下标:左下标和右下标,左下标,进行一趟排序:左下标从左向右开始找比key大的,右下标从右向左找比key小的,然后左右下标的值交换,然后再重复此过程,当左右下标重叠时,将key与重叠位置的数据进行交换。一趟排序之后得到的结果就是key左边的都比key小,右边的都比key大,但是并不一定是有序,然后使用左右下标重合的位置将数据分为两个区间,然后再重复上述步骤,将这两个区间变为有序,那么左右区间都有序,数据的整体就有序了。

那么在这就存在几个问题:

1.怎么保证重叠位置的值就一定比key小呢?

2.怎么让左右区间变为有序?

 1.怎么保证重叠位置的值就一定比key小呢?

我们可以先让右下标开始走,然后让左下标开始走,因为右下标找的是比key小的值,先让右下标找到比key小的值,当左下标找到比key大的值时,两个值交换,当左下标找不到比key大的值时,这时左下标肯定和右下标重合,这时重合的就是右下标找到的比key小的,这时交换即可达到重叠时的数据比key小。

 2.怎么让左右区间变为有序?

通过一次排序让数据变为两个区间,那么这两个区间再分别进行一次排序,再将它分为若干个小区间,这若干个小区间再进行排序,直到分出的区间只剩一个值或者分出的区间不存在,这时就达到了有序,那么就需要使用递归来进行左右区间的排序,递归截止的条件就是区间不存在或者区间里面只有一个数据。

代码演示:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

// 1. hoare版本
int PartSort1(int* a, int left, int right)
{
	//设置关键字
	int keyi = left;

	while (left < right)
	{
		//右边先走
		//找比key小的
		while (left < right && a[keyi] <= a[right]) //先得判断下标是否合理
		{
			right--;
		}
		//找比key大的
		while (left < right && a[keyi] >= a[left])
		{
			left++;
		}

		//交换
		Swap(&a[left], &a[right]);
	}
	//交换重叠位置的数据和key
	Swap(&a[keyi], &a[right]);

	//将重叠位置返回
	return right;
}


void QuickSort(int* a, int begin, int end)
{
	//递归截止条件
	if (begin >= end)
	{
		return;
	}
	
	//区间的划分
	int keyi = PartSort1(a, begin, end);
	//将整个数据划分为[begin, keyi-1] keyi [keyi+1, end]
	
	//递归右区间
	QuickSort(a, begin, keyi - 1);
	//递归左区间
	QuickSort(a, keyi + 1, end);

}

void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestQuickSort()
{
	int a[] = { 3,2,5,10,6,8,9,7,1,4 };
	QuickSort(a, 0, sizeof(a) / sizeof(int) - 1);
	PrintArry(a, sizeof(a) / sizeof(int));
}

int main()
{
	TestQuickSort();
	return 0;
}

 1.1.2挖坑法

由于hoare版本的方法需要注意左右下标出发的先后顺序,那么就针对这一问题,有了一种新的方法:挖坑法

同样的还是将最左边的数据作为关键数据,并且将它先保存,然后将这个位置设置为坑,然后设置左右两个下标,由于左边有坑,所以右下标先走,向前找比key小的值,找到了之后将这个值放在坑中,然后这个值的位置就形成了新的坑,然后左下标开始向后找比key大的,找到了之后将这个值放在新的坑中,此时又形成了一个坑,继续重复这个过程,当左右下标重叠时,再将刚开始保存的key放在这个坑中,即可完成一次排序,然后关键数据的这个位置又将数据分为两个区间,然后使用递归继续排序左右区间

 代码演示:

// 2. 挖坑法
int PartSort2(int* a, int left, int right)
{
	//保存关键值
	int key = a[left];
	//设置坑
	int hole = left;

	//一次排序
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] > key)
		{
			right--;
		}
		//找到了将其放入坑中,然后更新坑的位置
		a[hole] = a[right];
		hole = right;
		//左边找大
		while (left < right && a[left] < key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}

	//最后把关键数据再放到坑中
	a[hole] = key;
	key = right;
	return key;
}

void QuickSort(int* a, int begin, int end)
{
	//递归截止条件
	if (begin >= end)
	{
		return;
	}
	
	//区间的划分
	int keyi = PartSort2(a, begin, end);
	//将整个数据划分为[begin, keyi-1] keyi [keyi+1, end]
	
	//递归右区间
	QuickSort(a, begin, keyi - 1);
	//递归左区间
	QuickSort(a, keyi + 1, end);

}

1.1.3前后指针(下标)版本

同样的还是取最左边作为关键数据,然后设置两个下标,一个指向起始位置(prev),一个指向起始位置的后一个位置(cur),然后比较cur指向的数据与key的大小,若cur小于key,则prev++,并且prev指向的数据和cur指向的数据进行交换(如果prev和cur指向的时同一个数据那么便不用交换),然后cur++,如果cur指向的数据大于key,则cur++,当cur越界时,将prev指向的数据和key交换,这时prev指向的位置就可以作为新的key将整个数据分为两个区间,这时就可使用递归继续来排序它的左右区间。

 代码演示:

// 3.前后指针版本
int PartSort3(int* a, int left, int right)
{
	//设置关键数据
	int keyi = left;
	//前后指针(下标)
	int prev = left;
	int cur = left + 1;
	
	//判断cur的合法性
	while (cur <= right)
	{
		//如果cur指的数据小于关键数据且prev不等于cur即可完成交换
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	//cur越界之后再次交换prev与key
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	//返回新的关键位置
	return keyi;
}

void QuickSort(int* a, int begin, int end)
{
	//递归截止条件
	if (begin >= end)
	{
		return;
	}
	
	//区间的划分
	int keyi = PartSort3(a, begin, end);
	//将整个数据划分为[begin, keyi-1] keyi [keyi+1, end]
	
	//递归右区间
	QuickSort(a, begin, keyi - 1);
	//递归左区间
	QuickSort(a, keyi + 1, end);

}

1.1.4时间复杂度 

快速排序的递归版本与二叉树学习中的前序遍历逻辑相似,只需要注意区间的划分,那么根据二叉树的递归,一共需要递归\log N次,每一次进行遍历排序需要N次,那么快速排序的时间复杂度是:O(N* \log N) ,但是计算时间复杂度是按照最坏情况来考虑,当一组数据是有序的情况再来使用快速排序来排序这时它的时间复杂度就会变为O(N^2),这时递归就需要N次,每一次的遍历也需要N次,所以就是N^2,因为我们每一次取到的关键数据都是左边的数据,那么就需要对如何取关键数据进行改进。

1.1.5快速排序的优化

为了优化快速排序最坏情况下的时间复杂度,那么我们需要对快速排序如何选择key做出改进:

我们采用的是三数取中选key:

选出最左边的数(left)、最右边的数(right)、中间数据(mid)这三个数据中大小顺序位于中间的数据作为key,然后与最左边的数进行交换,这样就达到了优化。

优化完整代码: 

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//三数取中选key
int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;

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

// 1. hoare版本
int PartSort1(int* a, int left, int right)
{
	//设置关键字
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;

	while (left < right)
	{
		//右边先走
		//找比key小的
		while (left < right && a[keyi] <= a[right]) //先得判断下标是否合理
		{
			right--;
		}
		//找比key大的
		while (left < right && a[keyi] >= a[left])
		{
			left++;
		}

		//交换
		Swap(&a[left], &a[right]);
	}
	//交换重叠位置的数据和key
	Swap(&a[keyi], &a[right]);

	//将重叠位置返回
	return right;
}

// 2. 挖坑法
int PartSort2(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	//保存关键值
	int key = a[left];
	//设置坑
	int hole = left;

	//一次排序
	while (left < right)
	{
		//右边找小
		while (left < right && a[right] > key)
		{
			right--;
		}
		//找到了将其放入坑中,然后更新坑的位置
		a[hole] = a[right];
		hole = right;
		//左边找大
		while (left < right && a[left] < key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}

	//最后把关键数据再放到坑中
	a[hole] = key;
	key = right;
	return key;
}

// 3.前后指针版本
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	//设置关键数据
	int keyi = left;
	//前后指针(下标)
	int prev = left;
	int cur = left + 1;
	
	//判断cur的合法性
	while (cur <= right)
	{
		//如果cur指的数据小于关键数据且prev不等于cur即可完成交换
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	//cur越界之后再次交换prev与key
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	//返回新的关键位置
	return keyi;
}

void QuickSort(int* a, int begin, int end)
{
	//递归截止条件
	if (begin >= end)
	{
		return;
	}
	
	//区间的划分
	int keyi = PartSort3(a, begin, end);
	//将整个数据划分为[begin, keyi-1] keyi [keyi+1, end]
	
	//递归右区间
	QuickSort(a, begin, keyi - 1);
	//递归左区间
	QuickSort(a, keyi + 1, end);

}

优化过后快速排序的时间复杂度最坏的情况下也是O(N*\log N

 1.2非递归版本

由于递归存在风险,递归太深会出现问题,所以我们需要写出快速排序的非递归版本,快速排序的递归版本跟二叉树的前序遍历逻辑相似,那么在非递归版本中我们需要借助一个数据结构来完成非递归版本的快速排序。

在之前的数据结构中我们学到了一个叫栈的数据结构,它的特点就是先进后出,因此我们可以借助于栈来实现快速排序的非递归版本。​​​​​​​

在递归的版本中我们通过第一次排序整个区间然后取得一个关键值的位置,通过这个关键值将数据再次分为两个区间,然后依次递归继续排序这两个区间,并且在这两个区间内会再次划分区间,直到全部排序完毕。根据这个特点我们也可以根据栈的特性来进行实现,首先我们创建一个栈,然后先将0存放在栈中,再将9存放在栈中,那么就可以通过0和9来访问数据了,如果栈不为空,我们就取栈顶的元素9作为右下标,然后将其移除,再取栈顶元素0作为左下标,然后经过一次排序,得到了新的关键值,那么这个关键值会将区间分为两个部分,前面的部分是[left,keyi-1],后面的部分是[keyi+1,right],这时就需要对区间的合法性做出判断,如果left小于keyi-1,那么区间合理,可以继续将left作为左下标,将keyi-1作为右下标继续排序,如果right>keyi+1,那么区间合理,可以继续将keyi+1作为左下标,将right作为右下标继续排序,如果两个条件都不满足那么表示排序完成。

代码演示: 

// 1. hoare版本
int PartSort1(int* a, int left, int right)
{
	//设置关键字
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;

	while (left < right)
	{
		//右边先走
		//找比key小的
		while (left < right && a[keyi] <= a[right]) //先得判断下标是否合理
		{
			right--;
		}
		//找比key大的
		while (left < right && a[keyi] >= a[left])
		{
			left++;
		}

		//交换
		Swap(&a[left], &a[right]);
	}
	//交换重叠位置的数据和key
	Swap(&a[keyi], &a[right]);

	//将重叠位置返回
	return right;
}

//快速排序的非递归版本
void QuickSortNonR(int* a, int begin, int end)
{
	//创建栈
	Stack st;
	StackInit(&st);

	//先将整个区间放入栈
	StackPush(&st, begin);
	StackPush(&st, end);
	
	//进行排序
	while (!StackEmpty(&st))
	{
		//取栈顶元素作为右下标
		int right = StackTop(&st);
		//删除栈顶元素
		StackPop(&st);

		//取栈顶元素作为左下标
		int left = StackTop(&st);
		//删除栈顶元素
		StackPop(&st);

		//进行一次排序
		int keyi = PartSort1(a, left, right);

		//对获得的keyi是否合理进行判断
		if (left < keyi - 1)
		{
			//重新将新的区间放入栈中
			//注意:顺序不能交换
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
		if (right > keyi + 1)
		{
			//重新将新的区间放入栈中
			//注意:顺序不能交换
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}

	StackDestroy(&st);
}

注意:这里的先放左区间和先放右区间跟后面的代码是要呼应的,顺序不能随便更改。

在这里我就不展示栈接口的代码了,大家可以去栈和队列 这一篇博客找源码。

快速排序的特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定

 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持! 

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

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

相关文章

PMP中十大知识领域及敏捷部分

今天给大家分享一篇PMP考试中十大知识领域及敏捷部分。希望正在备考11月PMP的宝子们能够清楚地拿捏重难点&#xff0c;稳稳上岸&#xff01; 我是胖圆~欢迎大家关注留言&#xff01; 或者移步公众号【胖圆说PM】找我

SpringBoot-可视化监控

一、添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--采集应用的指标信息&#xff0c;prometheus--> <dependency…

牛客练习赛115 A Mountain sequence - 乘法原理

a 到 b有3条路&#xff0c;b到c有4条路&#xff0c;那a到c共有12种选择。 最大值作为顶点&#xff0c;统计每个数的数量&#xff0c;比如说最大值为9&#xff0c;且只有一个&#xff0c;7的数量为4 有的可能为 77779 77797 77977 79777 97777 共计5种可能性&#xff0c;设一…

windows 下载安装 mysql

windows 下载安装 mysql 官网地址&#xff1a;https://dev.mysql.com/ 下载地址&#xff1a;https://cdn.mysql.com//Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi 点击 Downloads 点击 MySQL Community (GPL) Downloads 点击 MySQL Installer for Window…

C高级day5

实现一个对数组求和的函数&#xff0c;数组通过实参传递给函数 #!/bin/bashfunction sum() {i${#*}j0m0while [ $j -lt $i ]do((m${arr[j]}))((j))doneecho "数组和为$m"} read -a arr sum ${arr[*]} 写一个函数&#xff0c;输出当前用户的uid和gid&…

Android悬浮窗实现源码-悬浮球转盘悬浮加速小火箭效果悬浮播放视频图片

一、实现思路 悬浮窗是一种比较常见的需求&#xff0c;就是把需要展示的内容界面缩小成一个悬浮窗&#xff0c;然后用户可以在其他界面上处理事情。 基本实现原理&#xff1a; 主要是通过WindowManager这个类来实现 addView方法用于添加一个悬浮窗&#xff0c; updateViewLay…

如何拆卸滚珠螺杆的螺母?

现在滚珠螺杆的使用中&#xff0c;相信很多人都遇到过因外力原因需要拆卸螺母这种状况&#xff0c;对螺母不熟悉&#xff0c;在拆卸的过程中很容易会损坏螺母&#xff01;因此&#xff0c;掌握正确的拆卸方法是非常必要的。 拆卸滚珠螺杆螺母需要准备一些工具&#xff0c;包括扳…

提升运营效率:仓储可视化的实时监控与优化

当今&#xff0c;仓储管理已经不再是简单的储存和分发商品的过程。随着供应链的复杂性增加&#xff0c;企业需要更高效的方式来管理和优化其仓储运营。在这个背景下&#xff0c;仓储可视化成为了一项关键的技术&#xff0c;它利用先进的数字化工具和数据分析来提升仓储管理的效…

【Linux基础】权限管理

​&#x1f47b;内容专栏&#xff1a; Linux操作系统基础 &#x1f428;本文概括&#xff1a; 用户之间的切换、sudo提权、Linux权限管理、文件访问权限的相关方法、目录权限、粘滞位等 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1a;2023.9.11 …

D1. Candy Party (Easy Version) Codeforces Round 896 (Div. 2)

Problem - D1 - Codeforces 题目大意&#xff1a;有一个n个数的数组a&#xff0c;要求令每一个数减去一个任意数&#xff0c;然后任选一个数加上&#xff0c;问能否使所有数相等 1<n<2e5 思路&#xff1a;因为要让每个数相等&#xff0c;首先检查他们的和能否平分成n份…

Linux文件属性操作函数

access函数 chmod函数 chown函数 修改文件的所在组或者所有者 truncate函数

RabbitMQ - 如保证消息的可靠性?

目录 一、消息可靠性 1.1、生产者消息确认&#xff08;生产者角度&#xff09; 1.1.1、理论 1.1.2、实践 1.2、消息持久化&#xff08;消息角度&#xff09; 1.2.1、理论 1.3、消费者消息确认&#xff08;消费者角度&#xff09; 1.3.1、理论 1.3.2、实践 1.4、失败重…

母婴用品小程序开发

母婴用品小程序商城开发 商品分类与搜索&#xff1a; 提供母婴用品的分类&#xff0c;如奶粉、尿片、婴儿服装等。 用户可以根据需求进行搜索&#xff0c;快速找到所需的母婴用品。 商品详情与评价&#xff1a; 展示母婴用品的详细信息&#xff0c;包括商品图片、价格、规…

使用Cpolar和极简主义文件管理器构建个人云储存平台并进行公网访问

文章目录 1. 前言2.Tiny File Manager网站搭建2.1.Tiny file manager下载和安装2.2 Tiny file manager网页测试2.2 Tiny file manager网页测试3. 本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试总结 1. 前言 文件共享和查阅是现在网络最常见的应用场景&am…

再见ChatGPT 无需代码能力创建自定义ChatGPT

【无需代码技能】您可以使用3个不同的LLM创建自己的自定义ChatGPT GPT-4 Llama 2Falcon LLM 这是如何与Dante AI创建自己的聊天机器人 Dante AI是一个去中心化的人工智能平台&#xff0c;允许用户创建和训练自己的AI模型。它使用基于区块链的系统来管理数据和交易&#xff…

UG\NX二次开发 获取曲面uv中心点 UF_MODL_ask_face_props

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 简介: UG\NX二次开发 获取曲面uv中心点 UF_MODL_ask_face_props。 效果: 代码: #include "me.hpp"void AskFaceMidpoint() {//选择面tag_t face …

C语言猜数字游戏详解及代码优化

目录 引言&#xff1a; 1.程序思路&#xff1a; 2.代码实现&#xff1a; 2.1.生成游戏菜单&#xff1a; 2.2.构建主函数框架&#xff1a; 2.3.构建游戏函数&#xff1a; 3.游戏源码&#xff1a; 4.程序优化&#xff1a; &#x1f388;优化后源码&#xff1a; 结论…

Power Series and Laplace Transforms

See https://math.libretexts.org/Bookshelves/Analysis/Supplemental_Modules_(Analysis)/Ordinary_Differential_Equations/6%3A_Power_Series_and_Laplace_Transforms

计算机毕设之基于Hadoop+springboot的物品租赁系统的设计与实现(前后端分离,内含源码+文档+教程)

该系统基于Hadoop平台&#xff0c;利用Java语言、MySQL数据库&#xff0c;结合目前流行的 B/S架构&#xff0c;将物品租赁管理的各个方面都集中到数据库中&#xff0c;以便于用户的需要。在确保系统稳定的前提下&#xff0c;能够实现多功能模块的设计和应用。该系统由管理员功能…