【数据结构】排序算法系列——快速排序(附源码+图解)

news2024/9/22 6:58:32

快速排序

接下来我们将要介绍的是排序中最为重要的算法之一——快速排序。

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环可以在大部分的架构上很有效率地达成。我们直接来分析它的算法思想。

算法思想与图解

我们首先直接来看算法步骤,再分析其原理和目的

  1. 首先确定一个基准值,基准值一般选最左边或者最右边的
  2. 然后使用左右指针对数据和基准值进行大小比较
  3. 比基准值小的放左边,比基准值大的放右边,从而使得最终基准值的左边比其小,右边比其大
  4. 递归重复此步骤,注意基准值不能重复,直到完全有序

img

具体的动画分析可以看这:快速排序算法动画演示_哔哩哔哩_bilibili

我们首先来对基准值的选择进行分析:

通常我们都会选择最左边或者最右边的基准值,这是最不需要多想的选择方法;

但是往往我们需要考虑时间效率,这样选择的话,时间效率是怎样的呢?我们知道最左边和最右边的数有可能是整个数据组中最大或者最小的数,而一轮快速排序的最终目的就是使用基准值将数据分为比其大和比其小的两部分,那么如果记住基准值本身就是一个最值,排序完之后必定也只会在最前或者最后一个位置,这样就会进行浪费的比较,从而降低效率。

在这里插入图片描述

如果我们需要规避这种最坏的情况,我们可以使用随机基准值或者三数取中法。这样能够有效规避最坏情况的发生,但并非绝对事件。

//1.随机取基准值
//随机选基准值,从而可以有效减小最坏情况的概率 
int randindex = rand() % (right - left + 1) + left;
Swap(&a[randindex], &a[left]);

//2,三数取中
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])
			return right;
		else
			return left;
	}
	else//左边和中间比较
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;

	}
}

从基准值的选择我们其实也可以看出,实际上快速排序的核心思想就是使用基准值,将数据组分成两份。这也是它分区交换排序名字的由来。分析分区原理,只要一直不断地进行分区操作,那么最后每个数都可以成为一次基准值,也就可以达到每个数的左边都比其小,右边都比其大,那么整体来看就已经实现了完全有序。

C语言代码分析

  • 霍尔快排
void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
		return;

	//随机选基准值,从而可以有效减小最坏情况的概率 
	int randindex = rand() % (right - left + 1) + left;
	Swap(&a[randindex], &a[left]);

	int key = left;//选择最左边为基准值
	while (left < right)
	{
		//右边找小的
		while (a[right] >= a[key])
			right--;
		//左边找大的
		while (a[left] <= a[key])
			left++;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);
	//二叉树的递归方式
	QuickSort1(a, key, left - 1);//递归左边
	QuickSort1(a, left + 1, right);//递归右边
}
//霍尔单趟
int PartSort1(int* a, int left, int right)
{
	if (left >= right)
		return;

	//随机选基准值,从而可以有效减小最坏情况的概率 
	int randindex = rand() % (right - left + 1) + left;
	Swap(&a[randindex], &a[left]);

	int key = left;//选择最左边为基准值
	while (left < right)
	{
		//右边找小的
		while (a[right] >= a[key])
			right--;
		//左边找大的
		while (a[left] <= a[key])
			left++;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);
	return left;

}

注意

二叉树思想

我们观察上述的代码,会发现我们的分区思想与[[二叉树]]的思想略有相似:将基准值看成根节点,那么它的左子树——也就是左边的部分绝对比其小;类似,右子树也绝对比其大(都反过来也可)——实际上霍尔当时就是根据[[二叉树]]的思想从而发明了这样一种排序的算法。

左右指针相遇的逻辑
  1. 初始化指针

    • 左指针从数组的起始位置开始向右移动,寻找一个大于基准值的元素。
    • 右指针从数组的末尾开始向左移动,寻找一个小于基准值的元素。
  2. 移动指针

    • 左指针向右移动,直到找到一个大于等于基准值的元素。
    • 右指针向左移动,直到找到一个小于等于基准值的元素。
  3. 指针相遇

    • 当左右指针相遇时,意味着左指针的位置是一个元素大于基准值的位置,而右指针已经通过其他元素找到了一个小于基准值的元素。此时可以认为,左指针的位置应该是大于或等于基准值的(可能因为左指针已经停止在一个比基准值小的元素上),而右指针的位置则是小于或等于基准值的。
为什么相遇节点永远小于基准值

在理想的情况下,通过上述移动,左右指针不会交叉的情况下,最终会在一个位置相遇,这个位置可能就是基准值的位置,也可能比基准值小。而这个位置的元素比基准值小的原因是基于以下几点:

  1. 分区约束

    • 根据右边先走,左边再走的顺序,左右指针最终需要相遇前会有以下两种情况:

      1.右指针找到小的,左指针没有找到大的,那么此时继续移动二指针就会相遇。

      2,右指针没有找到小的,继续移动直到遇到了左指针,鉴于左指针本身就比基准值要小或者相等(才会停下),所以此时的相遇位置就可以是比基准值要小。

      无独有偶,当左边先走,右边再走时就有可能遇见比基准值大的相遇位置。

  2. 基准值的定义

    • 最终将会把基准值放在左右指针交会的位置的元素上。这个位置的特性就是:在其左边的都是小于基准值的元素,而在其右边的都是大于基准值的元素。

因此,尽管左右指针可能在不等于基准值的元素上相遇,实际上通过合并数据的方式能整理出期望的排序效果。因此,它并不意味着相遇位置的元素永远小于基准值,而是说在执行分区后,基准值应该放在那个位置以满足排序的条件。

算法优化

快速排序除了霍尔发明的最初的一种算法,实际上还有改进算法。

  • 挖坑法

挖坑法的实质是不断变换坑位,这个坑位最终是用来存放基准值的位置。而在算法中我们将看到坑位始终是根据左右指针来进行定位的,因此当坑位要存放基准值也就是单趟结束的时候,左右指针会相遇在基准值的坑位。左右指针的移动也是根据同基准值的大小来决定的。

这个算法的好处是有助于我们更好地理解快排的本质,从而优化算法。

//挖坑法的实质就是不断变基准值的位置,直到找到基准值的位置
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;

	//三数取中
	int mid = GetMid(a, left, right);
	if (left != mid)
		Swap(&a[left], &a[mid]);

	int key = a[left];
	int hole = left;//挖坑位置

	while (left < right)
	{
		//右边找小的
		while (a[right] >= a[key])
			right--;
		a[hole] = a[right];//填坑
		hole = right;

		//左边找大的
		while (a[left] <= a[key])
			left++;
		a[hole] = a[left];//填坑
		hole = left;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);
	//二叉树的递归方式
	QuickSort1(a, key, left - 1);//递归左边
	QuickSort1(a, left + 1, right);//递归右边
}

//挖坑单趟
void PartSort2(int* a, int left, int right)
{
	
	//三数取中
	int mid = GetMid(a, left, right);
	if (left != mid)
		Swap(&a[left], &a[mid]);

	int key = a[left];
	int hole = left;//挖坑位置

	while (left < right)
	{
		//右边找小的
		while (a[right] >= a[key])
			right--;
		a[hole] = a[right];//填坑
		hole = right;

		//左边找大的
		while (a[left] <= a[key])
			left++;
		a[hole] = a[left];//填坑
		hole = left;
		
	}
	a[hole] = key;
	return hole;
}
  • 前后指针法

img

前后指针法使用cur和prev前后两个指针进行移动,规则如下:

  • A.cur找到比基准值小的值,prev++,再将cur与prev位置的值交换,cur++
  • B.cur找到比基准值大的值,cur++
  • 当cur越界(识别完所有的数据)时,结束所有的移动,将基准值放入此时prev的位置。

我们分析,在这两种情况下,prev要么就是紧跟cur,两个指针一直依附着对方前进,要么就是中间间隔的数都比基准值要大;同时它也实现了快排的核心思想:比基准值大的放右边,比基准值小的放左边。

//第三种是前后指针法
//前后指针法的实质是通过比较后指针和基准值的大小,
//然后满足大小条件时进行前后指针交换
//交换的原则就是把小的放在前边,大的放在后边
void QuickSort3(int* a, int left, int right)
{
	int mid = GetMid(a, left, right);
	if (mid != left)
		Swap(&a[left], &a[mid]);
	int key = left;
	
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] <a[key] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
		
	}
	Swap(&a[prev], &a[key]);
	key = prev;

	return key;
    
}

//前后指针单趟
int PartSort3(int* a, int left, int right)
{
	int mid = GetMid(a, left, right);
	if (mid != left)
		Swap(&a[left], &a[mid]);
	int key = left;

	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;

	}
	Swap(&a[prev], &a[key]);
	key = prev;

	return key;

}

  • 非递归快排

非递归版本主要通过显式栈来模拟递归调用栈。

使用非递归的原因:当数据过多的时候,递归算法就会跑不起来——递归需要建立栈帧,当建立了过多的栈帧就会出现栈溢出的情况。

  1. 初始化栈:创建一个栈来保存需要处理的数组区间。
  2. 入栈:将整个数组的左右边界(即数组的起始和结束索引)入栈。
  3. 循环处理
    1. 从栈顶弹出一对左右边界。
    2. 使用这些边界对数组进行分区,找到分区的中间点(即分区点)。
    3. 将分区点两侧的左右边界分别入栈,表示后续需要处理的子数组。
    4. 如果某个子数组的元素数量少于等于1,则不需要入栈处理。
  4. 结束:当栈为空时,所有区间都已经处理完毕,排序完成。
//非递归实现快排
void QuickSortNoR(int* a, int left, int right)
{
	ST s;
	STInit(&s);
	STPush(&s, left);//先将左右边界入栈
	STPush(&s, right);

	while (STEmpty(&s))
	{
		//取出左右边界
		int begin = STTop(&s);
		STPop(&s);
		int end = STTop(&s);
		STPop(&s);
		//使用一次单趟的快排得到第一次的基准值
		int key = PartSort3(a, begin, end);
		//将基准值的左右边界入栈
		if (key + 1 < end)
		{
			STPush(&s, end);
			STPush(&s, key + 1);
		}
		if (begin < key-1)
		{
			STPush(&s, key-1);
			STPush(&s, begin);
		}
	}

	STDestroy(&s);
}

代码解析

  1. S结构体:用来保存左右边界索引。
  2. PartSort3函数:选择数组的最右边元素为基准元素,通过交换使得基准元素的左侧都是小于等于它的元素,右侧都是大于它的元素。返回值是基准元素的最终位置。

这个算法是利用栈模拟递归过程,适用于不能使用递归的环境或递归深度较大的情况。

时间复杂度

关于快速排序为什么是最好的排序算法之一,肯定与它优秀的时间效率扯不开关系。这里我们直接看维基对于其平均时间复杂度的分析:

在这里插入图片描述

可以看到,快速排序从根本上就能够良好的减少遇见最坏情况的概率,而它的最坏情况实际上也坏不到哪去,如此优秀的排序机制也为它奠定了基础和不可动摇的地位。

最坏情况:O(n2)

最好情况:O(n logn)

稳定性

鉴于快速排序会改变前后元素的相对位置,所以:不稳定

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

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

相关文章

XXL-JOB环境搭建

2.快速入门 2.1 下载源码 a.源码下载地址: github地址 gitee地址 2.2.环境搭建&#xff1a; a.初始化调度数据库: 1.请下载项目源码并解压&#xff0c;获取 “调度数据库初始化SQL脚本” 并执行即可 b.编译源码: 1.解压源码,按照maven格式将源码导入IDE, 使用maven进行…

【Python】使用国内镜像安装conda并创建python环境

conda介绍&#xff1a; Conda 是一个开源的包管理系统和环境管理系统&#xff0c;由 Continuum Analytics 开发。它的主要作用是简化科学计算中软件包和依赖的安装和升级&#xff0c;并允许用户轻松地在不同的环境中切换。Conda 的设计初衷是为了简化 Python 环境的搭建和管理&…

海洋大地测量基准与水下导航系列之二国外海底大地测量基准和海底观测网络发展现状(上)

海底大地控制网建设构想最先由美国斯克里普斯海洋研究所(Scripps Institution of Oceanography,SIO)提出&#xff0c;目前仅有少数发达国家具备相应技术条件。美国、日本、俄罗斯和欧盟等发达国家通过布测先进的海底大地控制网&#xff0c;不断完善海洋大地测量基准基础设施&am…

go 运行报错missing go.sum entry for module providing package

运行&#xff1a; #清理go.mod中不再需要的模块&#xff0c;并且会添加缺失的模块条目到go.sum中 go mod tidy

【全网最全】2024华为杯数学建模C题高质量成品查看论文!【附带全套代码+数据】

题 目&#xff1a; ___基于数据驱动下磁性元件的磁芯损耗建模 完整版获取&#xff1a; 点击链接加入群聊【2024华为杯数学建模助攻资料】&#xff1a;http://qm.qq.com/cgi-bin/qm/qr?_wv1027&kxtS4vwn3gcv8oCYYyrqd0BvFc7tNfhV7&authKeyedQFZne%2BzvEfLEVg2v8FOm%…

线段树优化dp,CF 413E - Maze 2D

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 413E - Maze 2D 二、解题报告 1、思路分析 对于(li, l) -> (ri, r) …

nginx upstream转发连接错误情况研究

本次测试用到3台服务器&#xff1a; 192.168.10.115&#xff1a;转发服务器A 192.168.10.209&#xff1a;upstream下服务器1 192.168.10.210&#xff1a;upstream下服务器2 1台客户端&#xff1a;192.168.10.112 服务器A中nginx主要配置如下&#xff1a; log_format main…

接口加解密及数据加解密

目录 一、 加解密方式介绍 1.1 Hash算法加密 1.2. 对称加密 1.3 非对称加密 二、 我们要讲什么&#xff1f; 三、 接口加解密 四、 数据加解密 一、 加解密方式介绍 所有的加密方式我们可以分为三类&#xff1a;对称加密、非对称加密、Hash算法加密。 算法内部的具体实现…

Mysql高级篇(中)—— SQL优化之查询截取分析

SQL优化之查询截取分析 一、慢查询日志&#xff08;1&#xff09;简述&#xff08;2&#xff09;如何开启&#xff08;3&#xff09;慢查询日志分析工具介绍(了解)&#xff08;4&#xff09;官方工具 mysqldumpslow简述如何使用 二、SHOW PROCESSLIST三、&#xff08;了解&…

网络安全详解

目录 引言 一、网络安全概述 1.1 什么是网络安全 1.2 网络安全的重要性 二、网络安全面临的威胁 2.1 恶意软件&#xff08;Malware&#xff09; 2.2 网络钓鱼&#xff08;Phishing&#xff09; 2.3 中间人攻击&#xff08;Man-in-the-Middle Attack&#xff09; 2.4 拒…

让C#程序在linux环境运行

今晚花一些时间&#xff0c;总结net程序如何在linux环境运行的一些技术路线。 1、采用.Net Core框架 NET Core 使用了 .NET Core Runtime&#xff0c;它可以在 Windows、Linux 和 macOS 等多个操作系统上运行。可以采用Visual Studio生成Linux版本的dll。 在Linux系统中&…

救生圈检测系统源码分享

救生圈检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Visio…

Python基础学习(3)

目录 一&#xff0c;函数 1&#xff0c;函数的定义 2&#xff0c;函数的参数 1&#xff0c;默认值 2&#xff0c;传参 3&#xff0c;返回值 4&#xff0c;变量的作用域 5&#xff0c;函数的调用 二&#xff0c;常用数据结构 1&#xff0c;列表 列表的定义 列表的特性…

机器学习的应用领域

机器学习在许多领域有广泛的应用&#xff0c;下面列出了一些主要的应用领域及其典型应用&#xff1a; 1. 图像识别 人脸识别&#xff1a;用于解锁手机、自动标记照片、监控安全系统。物体识别&#xff1a;应用于自动驾驶汽车、机器人、医疗影像分析中&#xff0c;帮助机器理解…

vue3 TagInput 实现

效果 要实现类似于下面这种效果 大致原理 其实是很简单的,我们可以利用 element-plus 组件库里的 el-tag 组件来实现 这里我们可以将其抽离成一个公共的组件,那么现在有一个问题就是通讯问题 这里我们可以利用父子组件之间的通讯,利用 v-model 来实现,父组件传值,子组…

蓝桥杯15届C/C++B组省赛题目

问题描述 小蓝组织了一场算法交流会议&#xff0c;总共有 5050 人参加了本次会议。在会议上&#xff0c;大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手 (且仅有一次)。但有 77 个人&#xff0c;这 77 人彼此之间没有进行握手 (但这 77 人与…

Unity数据持久化4——2进制

概述 基础知识 各类型数据转字节数据 文件操作相关 文件相关 文件流相关 文件夹相关 练习题 using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine;public class Exercises1 : MonoBehaviour {/…

金融科技与银行业的数字化转型

随着科技的迅猛发展&#xff0c;金融科技已经成为推动银行业数字化转型的重要力量。从移动支付到区块链&#xff0c;再到人工智能&#xff0c;这些新兴技术正逐渐改变银行的运作方式&#xff0c;不断提高银行的服务效率、提升客户体验&#xff0c;并推动整个金融生态系统的变革…

大数据-143 - ClickHouse 集群 SQL 超详细实践记录!

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

代码编辑器 —— Notepad++ 实用技巧

目 录 NotePad常用技巧一、查找二、标记三、插件四、自动补全 NotePad常用技巧 Notepad 的吉祥物是一只变色龙。它广泛应用于编程、网页开发、文本处理、脚本编写、文档编辑等领域。 一起看看它有哪些功能和特点&#xff1a; 1、对众多编程语言提供语法高亮显示 2、可折叠函数…