排序(二)——快速排序(QuickSort)

news2024/11/15 4:59:41

        欢迎来到繁星的CSDN,本期内容包括快速排序(QuickSort)的递归版本和非递归版本以及优化。

一、快速排序的来历

        快速排序又称Hoare排序,由霍尔 (Sir Charles Antony Richard Hoare) ,一位英国计算机科学家发明。霍尔本人是在发现冒泡排序不够快的前提下,发明了快速排序。不像希尔排序等用名字命名排序方法,霍尔直接将其命名为快速排序,但时至今日,快速排序仍然是使用最广泛最稳定的排序算法。

二、快速排序主代码

        快速排序是如何实现的呢?私以为思想和二叉树的前序遍历类似。

     (没有看过二叉树的看这里:一文带你入门二叉树!-CSDN博客)

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

	int keyi = PartSort1(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

        快速排序采用了分治的思想,之前在排序(一)提到过,如果将数组拆成一个个小块再排序,会比一整个数组集体排序来的快。快排也正是利用这一点,从零到整地排序整个数组,并确定好分拆端点的最终位置,使得每一组的排序结束都是最终位置,无需再调整。而由于不知道数组需要被拆分成几块,类似于二叉树的递归遍历,快排也需要递归来帮助拆分和子数组排序。

        下面阐述PartSort1这个函数是如何进行单趟排序的了。

三、快速排序(递归版单趟排序)

      Part1 Hoare法

        先上动图:

        

        可以注意到,PartSort的参数是数组a,数组最左端元素的下标left,最右端元素的下标right。

        key就是子数组(即left~right范围内)的第一个元素。

        整个流程如下:

        1、right向左遍历,直到找到一个小于key的值,停下。

        2、left向右遍历,直到找到一个大于key的值,停下。

        3、两者交换。

        4、重复上述流程,直到left与right相遇,交换key和最后相遇处的下标mid。

        5、随后返回mid,并利用mid继续将[left,right]区间分割为[left,mid-1]和[mid+1,right]区间,当不能分割的时候,该数组排序完毕。

        问题讲解

        如何确保key交换后所在的位置就是最终位置?

        首先,从一个升序数组来看,对于其中任何一个元素(除首尾两个),该元素左侧均更小,右侧均更大。换句话说,保证左侧比key元素更小,右侧比key元素更大,即得到了这一元素的位置。

        不难发现,被left遍历过的地址都是比key小的,被right遍历过的地址都是比key大的,因为如有left发现比key大,或者right发现比key小,都已进行过交换。

         唯一需要判定的,就是最后一个元素是否比key小。

        (1)left遇到right       

        当right停下,说明right所处位置的元素应该比key小。

        那么left撞到right的时候,一定是right所处的位置,保证了该位置比key小。

        (2)right遇到left

        当left停下,说明已经交换完毕,此时left所处位置的元素比key小。

   两种情况本质相同,都是保证了停下的那一方所处位置比key小。

int PartSort1(int* arr, int begin, int end)
{
	int left = begin, right = end;
	int key = begin;
	while (left < right)
	{
		//left<right防止越界
		//使用>=而不是>防止数据出现死循环
		while (left < right && arr[right] >= arr[key])
			//寻找比key小的值
		{
			right--;
		}
		while (left < right && arr[left] <= arr[key])
			//寻找比key大的值
		{
			left++;
		}
		swap(&arr[left], &arr[right]);
	}
	int mid = left;
	swap(&arr[key], &arr[mid]);
	return mid;
}

      Part2 挖坑法

        先上动图:        

        挖坑法和hoare法的差别不大,思路都是将key的位置安排在最终位置后,再分段快排。

        但挖坑法的确会比hoare法更加容易理解。

        坑的位置就代表相遇位置,每“交换”一次,就将坑位换到被交换的位置,最后坑落在哪里,就将key值填入。

        和hoare法不同的是,挖坑法需要多一个临时变量储存key值,在进行操作的时候只需要赋值,而不是swap交换。

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--;
		}
		swap(&a[right], &a[hole]);
		hole = right;
		while (left < right && a[left] <= key) {
			left++;
		}
		swap(&a[left], &a[hole]);
		hole = left;
	}
	int mid = hole;
	a[hole] = key;
	return mid;
}

       Part3 双指针法

        怎么能够让双指针缺席呢!

        

        本质和前面两种没有区别,只是遍历的方向不同,前两种都是左右两侧开始遍历,而双指针法从一端开始遍历。prev代表比key小的值,cur去遍历,并确保prev的下一个位置上比key大,将较大的元素集体向后驱赶。

int PartSort3(int* a, int left, int right) {
	int fast = left;
	int slow = left;
	int key = a[left];
	while (fast <= right) {
		if(a[fast] < key)
			swap(&a[++slow], &a[fast]);
		fast++;
	}
	swap(&a[slow], &a[left]);
	return slow;
}

      总结

        Part123三种办法没有高下之分,都可以作为QuickSort的一部分使用。

        可以说QuickSort的代码实现和理解复杂程度并不如之前说的堆排那么抽象,但是性能确实优秀。

四、快速排序(非递归版)

        我们知道,递归占据了大量的栈帧空间,尽管市面上大部分编译器已经增强了递归的性能,致使用递归的快排和不用递归的快排时间上差别不大,但递归空间上的占据却不可忽视。快排非递归版应运而生。

   这一部分需要用到栈的知识,如果还没看,可以查看:栈和队列的介绍与实现-CSDN博客

        非递归版本最重要的是,如何实现数组的分割,与进行多次排序。

        利用好之前学过的数据结构,我们可以想起栈与队列,我们用栈来演示。

void QuickSortno(int* a, int left, int right) {
	Stack ps;
	StackInit(&ps);
	StackPush(&ps, left);
	StackPush(&ps, right);
	while (!StackEmpty(&ps)) {
		int end = StackTop(&ps);
		StackPop(&ps);
		int begin = StackTop(&ps);
		StackPop(&ps);
		int key=PartSort1(a, begin, end);
		if (key + 1 < end) {
			StackPush(&ps, key+1);
			StackPush(&ps, end);
		}
		if (key - 1 > begin) {
			StackPush(&ps, begin);
			StackPush(&ps, key-1);
		}
	}
	StackDestroy(&ps);
}

       有点像层序遍历,我们将待排序的子数组的两端,放入栈中,每两个为一组弹出并进行单趟排序,排序完毕后,将新增的两个子数组的两端放入栈中。循环往复,便可以得到结果。

       本质思想还是一致的。

五、快速排序的优化

  快速排序的最坏情况是升序/降序的数组,复杂度都达到了O(n^2)

       经研究表明,问题出在基准值上,也就是每次排序时最左端的数据。如果将基准值改为数组内的随机值,平均情况将比最好情况只糟39%,比只取最左端数据为基准值要好很多。

       所以优化的第一个方向在于调整基准值。

     三数取中法

int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	// left midi right
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

        代码很长,实际作用就是在left,(left+right)/2,right三个数中间取中间数,很好地做了分割。正如之前所述,越能将数组分割为两半,便越可以以更高的效率去排序。

      小区间优化

        优化的第二个方向在于快排的最后几层递归上。

        无论递归多少次,我们都要在最后几层递归上耗费大量的时间与空间(只需要看区间数量,便可以知道需排序的区间呈指数级增长)。

        递归是大招,但是更适合大场面,所以我们有以下优化:

if (left >= right)
		return;

                                                                          ↓

if (right-left<=10)
{
	InsertSort(a+left, right - left+1);
	return;
}

        没错,就如排序(一)中提到的,如果数组元素比较少,插入排序的复杂度体现不出来。

        这意味着我们可以在这个时候直接使用插入排序(当然希尔排序也是可以的,可为什么不全部都直接用希尔排序呢?)

        感受一下优化过的和未优化过的:

        优化前:

        优化后:

在10w量级的数据里,优化前后效率提升显著,相信量级更大的时候会更体现出优势。

        本篇内容到此结束,谢谢大家的观看!

        觉得写的还不错的可以点点关注,收藏和赞,一键三连。

        我们下期再见~

        往期栏目: 排序(一)——冒泡排序、直接插入排序、希尔排序(BubbleSOrt,InsertSort,ShellSort)-CSDN博客

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

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

相关文章

中伟视界:提升矿山安全管理效率,罐笼超员AI算法的应用实践

随着矿山智能化的发展&#xff0c;安全管理成为关键领域之一。罐笼作为矿山运输的重要设备&#xff0c;其安全使用直接关系到矿工的生命安全。为防止罐笼超员带来的安全隐患&#xff0c;AI算法在罐笼超员检测中发挥了重要作用。本文将详细介绍罐笼超员AI算法的工作原理及其应用…

oracle dba常用脚本2

11、表空间实有、现有、使用情况查询对比 SELECT TABLESPACE_NAME 表空间,TO_CHAR(ROUND(BYTES / 1024, 2), 99990.00) || 实有,TO_CHAR(ROUND(FREE / 1024, 2), 99990.00) || G 现有,TO_CHAR(ROUND((BYTES - FREE) / 1024, 2), 99990.00) || G 使用,TO_CHAR(ROUND(10000 * US…

数据结构(3.9_1)——特殊矩阵的压缩存储

总览 一维数组的存储结构 如果下标从1开始&#xff0c;则a[i]的存放地址LOC (i-1)*sizeof(ElemType); 二维数组的存储 二维数组也具有随机存储的特性 设起始地址为LOC 在M行N列的二维数组b[M][N]中&#xff0c;若按行优先存储&#xff0c; 则b[i][j]的存储地址的LOC (i*…

安装VMwareLinux

文章目录 主流操作系统VMware安装Linux 主流操作系统 windows 分为个⼈pc操作系统&#xff0c;服务端操作系统 windows98 win10 win11 windows server 年份 windows server 2019 使⽤简单&#xff0c;提供了图形化界⾯ ⽤户群体⾼ 可适⽤的软件是⽐较丰富 windows和windo…

深度学习中激活函数的演变与应用:一个综述

摘要 本文全面回顾了深度学习中激活函数的发展历程,从早期的Sigmoid和Tanh函数,到广泛应用的ReLU系列,再到近期提出的Swish、Mish和GeLU等新型激活函数。深入分析了各类激活函数的数学表达、特点优势、局限性以及在典型模型中的应用情况。通过系统的对比分析,本文探讨了激…

从 Icelake 到 Iceberg Rust

本文作者丁皓是Databend 研发工程师&#xff0c;也是开源项目 OpenDAL 作者&#xff0c;主要研究领域包括存储、自动化与开源。 太长不看 Icelake 已经停止更新&#xff0c;请改用 iceberg-rust。 Iceberg-rust 是一个由社区驱动的项目&#xff0c;所有 Icelake 的贡献者都已转…

Android 自动更新时间的数字时钟 TextClock

TextClock 继承 TextView &#xff0c;使用方法和 TextView 一样。 它专门用于显示数字时钟&#xff0c;可以自定义显示格式。 只要在布局文件里添加&#xff0c;它会自动更新时间&#xff0c;不需要添加刷新逻辑。 布局文件&#xff0c; <?xml version"1.0"…

计算机网络施工方案(非常详细)零基础入门到精通,收藏这一篇就够了

虽然弱电人平时接触网络工程项目比较多&#xff0c;但要系统的实施网络工程项目从施工、设备安装、验收这一套流程&#xff0c;很多朋友却感觉无从下手&#xff0c;计算机网络系统主要是一些网络设备&#xff0c;这样的系统施工方案如何写呢&#xff1f;在我们弱电vip技术中也经…

【Linux】任务管理

这个任务管理&#xff08;job control&#xff09;是用在bash环境下的&#xff0c;也就是说&#xff1a;【当我们登录系统获取bashshell之后&#xff0c;在单一终端下同时执行多个任务的操作管理】。 举例来说&#xff0c;我们在登录bash后&#xff0c;可以一边复制文件、一边查…

电脑硬盘里的文件能保存多久?电脑硬盘文件突然没了怎么办

在数字化时代&#xff0c;电脑硬盘作为我们存储和访问数据的重要设备&#xff0c;承载着无数珍贵的回忆、工作成果和创意灵感。然而&#xff0c;硬盘里的文件能保存多久&#xff1f;当这些文件突然消失时&#xff0c;我们又该如何应对&#xff1f;本文将深入探讨这两个问题&…

【JavaScript 报错】未捕获的模块错误:Uncaught ModuleError

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、错误原因分析1. 模块路径错误2. 模块不存在3. 模块命名错误 二、解决方案1. 检查模块路径2. 确保模块存在3. 检查模块名称 三、实例讲解四、总结 在JavaScript模块系统中&#xff0c;未捕获的模块错误&#xff08;Uncau…

前端面试题55(JavaScript前缀中缀后缀表达式)

在编程中&#xff0c;前缀、中缀和后缀表达式是用于描述算术表达式的不同表示方法&#xff0c;主要用于解析和计算数学公式。这些概念在算法设计&#xff08;尤其是表达式树和栈的应用&#xff09;中非常重要。下面我将分别解释这三种表达式&#xff0c;并用JavaScript代码示例…

html网页设计关于家乡-泰州

代码地址&#xff1a;https://pan.quark.cn/s/0f8659c712d0

逻辑运算及其基本概念,定理,算法,规律,卡诺图

逻辑运算及其基本概念&#xff0c;定理&#xff0c;算法&#xff0c;规律&#xff0c;卡诺图 文章目录 逻辑运算及其基本概念&#xff0c;定理&#xff0c;算法&#xff0c;规律&#xff0c;卡诺图开胃小菜运算1、与运算2、或运算3、非运算4、与非&#xff08;都1时为0&#xf…

解决GET请求中文乱码问题

解决GET请求中文乱码问题 1、乱码的根本原因2、解决方法方法一&#xff1a;修改Tomcat配置&#xff08;推荐&#xff09;方法二&#xff1a;使用URLEncoder和URLDecoder&#xff08;不推荐用于GET请求乱码&#xff09;方法三&#xff1a;String类编解码&#xff08;不直接解决乱…

Qt中实现让静态图片动起来,创建动画效果

在现代应用程序开发中&#xff0c;动画效果是提升用户体验的重要元素之一。Qt作为一个强大的跨平台应用程序框架&#xff0c;提供了丰富的工具和库来创建各种动画效果。本文将介绍如何在Qt中使用静态图片创建动画效果。 实现方法一 使用QTimer和QPixmap 1.准备图片资源&#…

【初阶数据结构】2.顺序表

文章目录 1.线性表2.顺序表2.1 概念与结构2.2 分类2.2.1 静态顺序表2.2.2 动态顺序表 2.3 动态顺序表的实现2.4 顺序表算法题2.4.1 移除元素2.4.2 删除有序数组中的重复项2.4.3 合并两个有序数组 2.5 顺序表问题与思考 1.线性表 线性表&#xff08;linear list&#xff09;是n…

【Python】Python Flask 和 gRPC 简单项目

Python Flask 和 gRPC 示例项目 本文将介绍如何在 Python 中使用 Flask 和 gRPC 创建一个简单的示例应用程序&#xff0c;并使用 requests 库进行测试。 环境设置 首先&#xff0c;确保您已经安装了 Python。然后&#xff0c;创建一个虚拟环境以管理您的依赖项。 python -m…

想要爬取第一条网页的数据但是失败了?如何解决??

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

使用Python的Turtle模块绘制玫瑰

在本文中&#xff0c;我们将通过使用Python中的turtle模块来绘制一个精美的花朵图案&#xff0c;包括花蕊、花瓣和叶子。turtle模块是Python标准库的一部分&#xff0c;用于创建图形和动画&#xff0c;非常适合初学者学习编程基础和图形绘制。 初始设置 import turtle# 设置初…