【排序】快速排序详解

news2025/2/25 9:28:49

✨✨欢迎大家来到Celia的博客✨✨

🎉🎉创作不易,请点赞关注,多多支持哦🎉🎉

所属专栏:排序

个人主页:Celia's blog~

一、快速排序的思想

快速排序的核心思想是:

  1. 选定一个key值作为基准值(一般是整个数组的第一个元素)。
  2. 把整个数组中比key小的元素放到key的左边,比key大的元素放到key的右边。这需要通过一次单趟排序来实现。
  3. 单趟排序结束后,可以认为先前选定的key值的位置已经排好序了。根据这个key值的位置,将数组分为左右两个子数组,再分别进行单趟排序(重复2过程)。直到左右数组不能再分为止。

二、快速排序的原理分析

想要实现快速排序,最重要的就是实现单趟排序,单趟排序主要有三个方法:霍尔法、挖坑法、前后针法。

2.1 霍尔法

霍尔法的思想是:

  1. 定义左右两个指针分别指向当前数组的首尾两边。
  2. 右指针先走,从右往左找到首个比key值小的元素。
  3. 再让左指针走,从左往右找到首个比key值大的元素。
  4. 交换左右指针所指向的两个元素。
  5. 重复2、3步骤,直到左右指针相遇
  6. 将key值所在的位置与左右指针相遇的位置的元素交换。
  7. 单趟排序结束,key值所在的位置左边都比key值小,右边都比key值大。

还有一个很重要的问题,为什么在单趟排序之后,两个指针相遇位置的元素值一定比key小呢?

  • 如果左指针遇到右指针,由于右指针是先走的,说明右指针已经找到了比key小的元素。
  • 如果右指针遇到左指针由于上一轮的交换,比key小的元素已经换到了当前左指针的位置,左指针的位置的元素一定也比key小。


结论:如果使用霍尔法进行单趟排序,只需要让与基准值(key)所在方位相反的指针先走就可以了。

2.2 挖坑法

挖坑法的思想是:

  1. 定义左右两个指针,分别指向数组的首尾位置。
  2. 选定一个基准值key(一般是数组的第一个元素),并记录。将当前基准值位置记作“坑”。
  3. 右指针先走,从右往左找到比key值小的元素。
  4. 将右指针所在的位置的元素移动到“坑”中,当前右指针所在的位置形成新的“坑”
  5. 左指针再走,从左往右找到比key值大的元素。
  6. 将左指针所在的位置的元素移动到“坑”中,当前左指针所在的位置形成新的“坑”
  7. 当左右指针相遇时,将key值填入左右指针相遇的位置。单趟排序结束。

这个方法相比于霍尔法更好理解,也不用考虑两指针相遇时的元素是否小于key的问题。 该方法效率与霍尔法相同

2.3 前后指针法

前后指针法的思想是:

  1. 选定一个基准值,用key保存起来。
  2. 定义prev指针指向数组首位置,定义cur指向prev的下一个位置
  3. 比较cur位置的元素与key的大小关系,若cur位置元素比key大,cur++。若cur位置元素比key小,先让prev++,再交换cur和prev位置的元素。
  4. 当cur大于数组大小时,结束遍历,将key所在的位置的值和prev所在位置的值交换。

三、快速排序的代码实现

3.0 核心代码逻辑

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void QSort(int* a, int left, int right)
{
	if (left >= right)//递归结束条件
		return;
	int begin = left, end = right;

    //使用三种方法的其中一种进行单趟排序
	int mid = Part3(a, begin, end);//记录每一次排好的元素下标

	QSort(a, begin, mid - 1);//递归左右子数组
	QSort(a, mid + 1, end);
}

递归调用会将整个数组不断地分为两个子数组,如果递归传入的left和right相等,不用进行排序,如果left大于right,不符合区间的逻辑,也不需要排序。所以递归的结束条件为 left >= right

3.1 霍尔法

//霍尔法
int Part1(int* a, int left, int right)
{
	int keyi = left;
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi])
		{
			end--;
		}
		while (begin < end && a[begin] <= a[keyi])
		{
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[keyi], &a[begin]);
	return begin;
}

3.2 挖坑法

//挖坑法
int Part2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[hole] = a[end];
		hole = end;
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;
	return hole;
}

3.3 前后指针法

//前后指针法
int Part3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] <= a[keyi])
		{
			prev++;
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

四、快速排序的时间复杂度和空间复杂度分析

4.1 时间复杂度

  • 快速排序最核心的步骤是对每个子数组进行遍历比较操作,所以我们用遍历的次数来近似时间复杂度。
  • 快速排序对数组的分组类似于二叉树,我们可以简易的把快速排序的层数(递归深度)设为h(类比二叉树深度),递归创建的总函数栈帧次数设为N(类比二叉树节点个数)。
  • 则存在近似关系:\log{_{2}}N = h ,则共有 log{_{2}}N 层,每一层的每一个节点都要遍历数组,整个一层加起来的遍历次数近似N,则总遍历次数为log{_{2}}N * N = Nlog{_{2}}N
  • 则快速排序的时间复杂度为log{_{2}}N * N = Nlog{_{2}}N

4.2 空间复杂度

  • 快速排序所占用的额外空间主要为递归创建的函数栈帧,则空间复杂度就是递归创建的最大栈帧数量。由于栈的空间可以重复利用,则计算递归的最大深度即可,最大深度为:log{_{2}}N
  • 快速排序的空间复杂度为:log{_{2}}N

五、快速排序的优化

  • 快速排序在最好情况下可以看作一个完全二叉树,时间复杂度为Nlog{_{2}}N。但是如果排序数组有序,那么每次把数组首位置作为基准位置的话,每次排序就相当于将数组分为 1 和 N - 1 个元素。每次排好一个元素,那么递归的深度就会大大增加。

  • 遍历的总数就会变成一个等差数列,n + n - 1 + n - 2 + ... + 2 + 1,用求和公式求出结果后,最大的次方项变成了N^{2},这不仅仅严重降低了效率,也有可能会因为递归层数太深造成栈溢出的风险。
  • 为了解决这两个问题,可以使用三数取中小区间优化来解决这些问题。

5.1 三数取中

  • 三数取中的思想是,取数组首、末、中三个位置的值,记录这三个位置上大小为中间值的下标。并将这个取中的值与数组首元素交换。这样一来,以首尾值为基准值,基准值最终排好的位置会趋近于数组中间,就会将数组尽可能分为长度大致相等的两部分进行递归,以增加效率减少递归深度
//三数取中
int FindMid(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else //a[mid] >= a[right] mid最大,选left和right中最大的
		{
			if (a[left] > a[right])
				return left;
			else
				return right;
		}
	}
	else //a[left] >= a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else //a[mid] <= a[right] mid最小,选left和right中最小的
		{
			if (a[right] < a[left])
				return right;
			else
				return left;
		}
	}
}
  • 加入了三数取中,快速排序的核心代码就变成了:
void QSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;

	int middle = FindMid(a, left, right);
	Swap(&a[left], &a[middle]);//交换

	int mid = Part3(a, begin, end);
	QSort(a, begin, mid - 1);
	QSort(a, mid + 1, end);
}

5.2 小区间优化

  • 小区间优化主要是针对排序数组的元素数量较少时,进行递归开辟函数栈帧开销太大(就是没有必要),不如使用其他的排序算法(快一些,但不额外开辟空间)来进行排序。一般情况下,小区间优化使用的排序算法为插入排序
//插入排序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
}
  • 快速排序的主要逻辑变为:
void QSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	if (right - left + 1 < 10)
	{
		InsertSort(a + left, right - left + 1);//插入排序
	}
	else
	{
		int begin = left, end = right;

		int middle = FindMid(a, left, right);
		Swap(&a[left], &a[middle]);//交换

		int mid = Part3(a, begin, end);
		QSort(a, begin, mid - 1);
		QSort(a, mid + 1, end);
	}
}
  • 这里需要注意,由于递归进行到一定深度时,数组区间元素个数较少的情况下([left, right]),排序的区间是整个数组的一小段,故插入排序传入的首地址需要传 a + left,排序元素数量需要传right - left + 1

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

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

相关文章

关于Unity转微信小程序的流程记录

1.准备工作 1.unity微信小程序转换工具,minigame插件,导入后工具栏出现“微信小游戏" 2.微信开发者工具稳定版 3.MP微信公众平台申请微信小游戏,获得游戏appid 4.unity转webgl开发平台,Player Setting->Other Settings->Color Space->Linear 5. unity工…

程序员面试的“八股文“:助力还是阻力?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

知识付费培训考试题库h5小程序开源版开发

知识付费培训考试题库h5小程序开源版开发 企业内部培训与考试课堂系统&#xff0c;支持丰富课程类型&#xff0c;还拥有全面的题型体系&#xff0c;并能自动评分。应用具备响应式设计&#xff0c;加之学习进度跟踪与评论功能&#xff0c;打造互动式学习环境&#xff0c;是现代…

不知道服务器jenkins账户密码,利用自己账户和sudo登录jenkins账户

在服务器上不知道jenkins账户密码&#xff0c;只知道自己账户密码&#xff0c;如何登录jenkins账户 sudo -u jenkins -i

“八股文”在实际工作中是助力、阻力还是空谈

目录 1.概述 1.1.对实际工作的助力 1.2.存在的问题 2.“八股文”对招聘过程的影响 2.1.“八股文”在筛选候选人时的作用 2.2.面试中的比重及其合理性 2.3.如何平衡“八股文”与实际编程能力的考察 3.“八股文”在日常工作中的实用价值 3.1.在团队协作环境中进行有效沟…

Burning In 测试

什么是老化测试&#xff1f; 芯片Burning In测试系统是一种高度集成的测试设备&#xff0c;它结合了温度控制、电源控制、环境控制以及数据采集与分析等多个子系统。该系统能够在可控的条件下对芯片进行长时间的老化测试&#xff0c;从而有效地排查潜在问题&#xff0c;提高芯片…

MySQL 8.0 新特性汇总

文章目录 前言1. 运维管理1.1 可持久化变量1.2 管理员端口1.3 资源组1.4 数据库粒度只读1.5 show processlist 实现方式1.6 加速索引创建速度1.7 控制连接的内存使用量1.8 克隆插件1.9 mysqldump 新增参数1.10 慢日志增强1.11 快速加列1.12 InnoDB 隐藏主键1.13 Redo 配置1.14 …

快速方便地下载huggingface的模型库和数据集

快速方便地下载huggingface的模型库和数据集 方法一&#xff1a;用于使用 aria2/wgetgit 下载 Huggingface 模型和数据集的 CLI 工具特点Usage 方法二&#xff1a;模型下载【个人使用记录】保持目录结构数据集下载不足之处 方法一&#xff1a;用于使用 aria2/wgetgit 下载 Hugg…

java算法day26

java算法day26 207 课程表208 实现Trie(前缀树) 207 课程表 这题对应的知识是图论里的拓扑排序的知识。从题意就可以感受出来了。题目说如果要学习某课程&#xff0c;那么就需要先完成某课程。 这里我描述比较复杂的情况&#xff1a;课程与课程之间也有可能是多对一的场景或者…

实现halcon中的erosion、connection、fill_up

在halcon中&#xff0c;区域R是用一系列行程&#xff08;run&#xff09;的集合表示的&#xff0c;run的形式为&#xff08;Row&#xff0c;ColumnBegin&#xff0c;ColumnEnd&#xff09;&#xff0c;分别对应行坐标、列开始坐标、列结束坐标&#xff0c;这种保存区域的方法被…

C#中重写tospring方法

在C#中&#xff0c;重写ToString方法允许你自定义对象的字符串表示形式。当你想要打印对象或者在调试时查看对象的状态时&#xff0c;重写ToString方法非常有用。 默认情况下&#xff0c;ToString方法返回对象的类型名称。通过重写这个方法&#xff0c;你可以返回一个更有意义…

1.5 队列概念,应用及部分实现

1.基本概念 队列&#xff08; Queue &#xff09;&#xff1a;也是运算受限的线性表。是一种先进先出&#xff08; First In First Out &#xff0c;简称 FIFO &#xff09;的线性表。只允许在表的一端进行插入&#xff0c;而在另一端进行删除。 队首&#xff08; front &am…

C/C++编程-算法学习-数字滤波器

数字滤波器 一阶低通滤波器结论推导11. 基本公式推导2. 截止频率 和 采样频率 推导 实现 二阶低通滤波器实现1实现2推导1推导2 一阶低通滤波器 结论 其基本原理基于以下公式&#xff1a; o u t p u t [ n ] α ∗ i n p u t [ n ] ( 1 − α ) ∗ o u t p u t [ n − 1 ] …

(Arxiv-2023)MobileDiffusion:移动设备上即时文本到图像生成

MobileDiffusion&#xff1a;移动设备上即时文本到图像生成 Paper Title&#xff1a;MobileDiffusion: Instant Text-to-Image Generation on Mobile Devices Paper是谷歌出品 Paper地址 图 1&#xff1a;MobileDiffusion 用于 (a) 文本到图像的生成。(b) Canny 边缘到图像、风…

认证授权概述和SpringSecurity安全框架快速入门

1. 认证授权的概述 1.1 什么是认证 进入移动互联网时代&#xff0c;大家每天都在刷手机&#xff0c;常用的软件有微信、支付宝、头条、抖音等 以微信为例说明认证的相关基本概念。在初次使用微信前需要注册成为微信用户&#xff0c;然后输入账号和密码即可登录微信&#xff0c…

完成stable将图片转换为二维码

1.创建虚拟环境 conda create -n stable python=3.10.6 2.克隆项目 git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui 或者 git clone https://kgithub.com/AUTOMATIC1111/stable-diffusion-webui 3.安装依赖(-i https://pypi.tuna.tsinghua.edu.cn/s…

C++基础编程100题-028 OpenJudge-1.4-08 判断一个数能否同时被3和5整除

更多资源请关注纽扣编程微信公众号 http://noi.openjudge.cn/ch0104/08/ 描述 判断一个数n 能否同时被3和5整除 输入 输入一行&#xff0c;包含一个整数n。&#xff08; -1,000,000 < n < 1,000,000&#xff09; 输出 输出一行&#xff0c;如果能同时被3和5整除输…

八股文-基础知识-int和Integer有什么区别?

引言 在Java编程实践中&#xff0c;基本数据类型int与包装类Integer扮演着不可或缺的角色&#xff0c;它们间的转换与使用策略深刻影响着程序的性能与内存效率。本文旨在深入探究int与Integer的区别&#xff0c;涵盖其在内存占用、线程安全、自动装箱与拆箱机制等方面的表现。…

3条非常实用的处世“潜规则”,受益终生

01 尽量不要让别人在你身上免费得到&#xff0c;哪怕是你不需要或者根本不在意的东西。 让别人免费得到&#xff0c;其实就是一种暗示&#xff0c;暗示别人可以继续免费索取&#xff0c;为什么&#xff1f;因为人性总是趋利的&#xff0c;如果可以免费得到&#xff0c;那为什…

高校是需要AIGC 实验室还是大数据人工智能实验室呢

AIGC&#xff08;人工智能与图形计算&#xff09;实验室和大数据人工智能实验室虽然都隶属于人工智能的范畴&#xff0c;但它们的关注点、研究方向和具体应用领域有所不同。 我们分别从研发方向、技术侧重、应用领域、研究工具和方法等方面去分析两者的区别&#xff0c;希…