【排序算法】 快速排序(快排)!图解+实现详解!

news2024/11/27 18:36:10

在这里插入图片描述

🎥 屿小夏 : 个人主页
🔥个人专栏 : 算法—排序篇
🌄 莫道桑榆晚,为霞尚满天!

文章目录

  • 📑前言
  • 🌤️快速排序的概念
    • ☁️快速排序的由来
    • ☁️快速排序的思想
    • ☁️快速排序的实现步骤
  • 🌤️快速排序(递归版)
    • ☁️快排主框架
    • ☁️Hoare版本快排
      • ⭐代码与图解
      • ⭐代码解析:
    • ☁️挖坑法
      • ⭐代码与图解
      • ⭐代码解析:
    • ☁️双指针法
      • ⭐代码与图解
      • ⭐代码解析
    • ☁️三数取中优化
      • ⭐为什么要三数取中?
      • ⭐三数取中代码实现
    • ☁️小区间优化
      • ⭐什么是区间优化?
      • ⭐小区间优化代码实现
      • ⭐小区间优化的好处
  • 🌤️快速排序(非递归版)
    • ☁️代码解析
  • 🌤️快速排序的特性总结
  • 🌤️全篇总结

📑前言

什么是快排?快排的速度到底有多快呢?它们的思想和实现是什么样的?

本文会对这快速排序进行详解,绝对细致入微!让你彻底搞懂快排!

🌤️快速排序的概念

☁️快速排序的由来

英国计算机科学家Tony Hoare在1960年为了解决计算机上的排序问题,提出了快速排序的算法,最初是为了在英国的英尔兰电子公司(ELLIOTT Brothers)的快速硬件上实现高效的排序算法。

☁️快速排序的思想

快速排序的主要思想是分治法,将一个大问题分割成小问题,解决小问题后再合并它们的结果。

☁️快速排序的实现步骤

  1. 从待排序的数组中选择一个元素,称之为枢纽元(pivot)。
  2. 将数组中小于枢纽元的元素移到枢纽元的左边,将大于枢纽元的元素移到枢纽元的右边,这个过程称为分区(partition)。
  3. 递归地对枢纽元左边的子数组和右边的子数组进行排序。
  4. 当所有子数组都有序时,整个数组就自然有序了。

🌤️快速排序(递归版)

☁️快排主框架

void QuickSort(int* a, int left, int right)
{
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
	if (right <= left)
		return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
	//int keyi = PartSort1(a, left, right);
	//int keyi = PartSort2(a, left, right);
	int keyi = PartSort3(a, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, keyi-1) 和 [keyi+1, right)
// 递归排[left, keyi-1)
	QuickSort(a, left, keyi - 1);
// 递归排[keyi+1, right)
	QuickSort(a, keyi + 1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

☁️Hoare版本快排

⭐代码与图解

在这里插入图片描述

int PartSort1(int* a, int left, int right)
{
	//三数取中(优化)
	//int keyi = NumBers(a, left, right);
	//Swap(&a[keyi], &a[left]);
	int key = left;

	while (left < right)
	{
		while (left < right && a[left] <= a[right])
		{
			right--;
		}
		while (left < right && a[left] <= a[right])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[key]);
	return left;
}

⭐代码解析:

  1. 首先,定义一个变量key,用于保存基准值的下标,初始值为left。
  2. 进入一个循环,循环条件是left < right,即左右指针没有相遇。
  3. 在循环中,首先从右边开始,找到第一个小于等于基准值的元素的下标,将right指针左移,直到找到符合条件的元素或者left和right相遇。
  4. 然后从左边开始,找到第一个大于基准值的元素的下标,将left指针右移,直到找到符合条件的元素或者left和right相遇。
  5. 如果left < right,说明找到了需要交换的元素,将a[left]和a[right]交换位置。
  6. 重复步骤3到步骤5,直到left和right相遇。
  7. 最后,将基准值a[key]和a[left]交换位置,将基准值放在正确的位置上。
  8. 返回分割点的下标left。

实现了一次快速排序的分割操作,将数组分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。然后再通过递归调用这个函数,这就是hoare版的快排。

☁️挖坑法

⭐代码与图解

在这里插入图片描述

int PartSort2(int* a, int left, int right)
{
	//三数取中优化
	//int keyi = NumBers(a, left, right);
	//Swap(&a[keyi], &a[left]);
	int key = a[left];
	int hole = left;//为第一个坑

	while (left < right)
	{
		while (left < right && key <= a[right])
		{
			--right;
		}
		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

⭐代码解析:

  1. 定义一个变量key,用于保存基准值,初始值为a[left]。
  2. 定义一个变量hole,用于保存空洞的位置,初始值为left。
  3. 进入一个循环,循环条件是left < right,即左右指针没有相遇。
  4. 在循环中,首先从右边开始,找到第一个小于基准值的元素的下标,将right指针左移,直到找到符合条件的元素或者left和right相遇。
  5. 将a[right]的值赋给a[hole],将空洞的位置移动到right。
  6. 然后从左边开始,找到第一个大于基准值的元素的下标,将left指针右移,直到找到符合条件的元素或者left和right相遇。
  7. 将a[left]的值赋给a[hole],将空洞的位置移动到left。
  8. 重复步骤4到步骤7,直到left和right相遇。
  9. 最后,将基准值key放入空洞的位置a[hole],将基准值放在正确的位置上。
  10. 返回空洞的位置hole。

同样实现了将数据分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。

☁️双指针法

⭐代码与图解

在这里插入图片描述

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	//三数取中优化
	//int midi = NumBers(a, left, right);
	//Swap(&a[left], &a[midi]);

	int prev = left;
	int cur = prev + 1;

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

		++cur;
	}

	Swap(&a[prev], &a[keyi]);
	return prev;
}

⭐代码解析

  1. 定义两个指针prev和cur,分别指向left和left+1。
  2. 定义一个变量keyi,用于保存基准值的下标,初始值为left。
  3. 进入一个循环,循环条件是cur <= right,即cur指针没有越界。
  4. 在循环中,如果a[cur]小于基准值a[keyi],则将prev指针右移一位,并交换a[prev]和a[cur]的值,保证prev指针之前的元素都小于基准值。
  5. 将cur指针右移一位。
  6. 重复步骤4到步骤6,直到cur指针越界。
  7. 最后,将基准值a[keyi]和a[prev]交换位置,将基准值放在正确的位置上。
  8. 返回分割点的下标prev。

同样实现了将数据分成两部分,左边的元素都小于等于基准值,右边的元素都大于基准值。

☁️三数取中优化

⭐为什么要三数取中?

  1. 三数取中是为了选择一个更好的基准值,以提高快速排序的效率。在快速排序中,选择一个合适的基准值是非常重要的,它决定了每次分割的平衡性。

  2. 快速排序是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的小,然后再对这两部分分别进行快速排序,递归地进行下去,直到整个序列有序。

  3. 如果每次选择的基准值都是最左边或最右边的元素,那么在某些情况下,快速排序的效率可能会降低。例如,当待排序序列已经有序时,如果每次选择的基准值都是最左边或最右边的元素,那么每次分割得到的两个子序列的长度差可能会非常大,导致递归深度增加,快速排序的效率降低。

  4. 而通过三数取中的优化,可以选择一个更好的基准值,使得每次分割得到的两个子序列的长度差更小,从而提高快速排序的效率。

  5. 具体来说,三数取中的优化是选择待排序序列的左端、右端和中间位置的三个元素,然后取它们的中值作为基准值。这样选择的基准值相对于最左边或最右边的元素,更接近整个序列的中间位置,可以更好地平衡分割后的两个子序列的长度,从而提高快速排序的效率。

  6. 通过三数取中的优化,可以减少递归深度,提高分割的平衡性,使得快速排序的效率更稳定,适用于各种不同的输入情况。

⭐三数取中代码实现

//三数取中
int NumBers(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	// left mid right
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])  // mid是最大值
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right]) // mid是最小
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

☁️小区间优化

⭐什么是区间优化?

小区间优化是指在快速排序中,当待排序的子序列的长度小于一定阈值时,不再继续使用快速排序,而是转而使用直接插入排序。

⭐小区间优化代码实现

void QuickSort(int* a, int left, int right)
{
	if (right <= left)
		return;
	if(right - left + 1 > 10)
	{
        int keyi = PartSort3(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a + left,right - left + 1);
	}
}

⭐小区间优化的好处

  1. 减少递归深度:使用插入排序来处理较小的子序列,可以减少递归的深度,从而减少了函数调用的开销。
  2. 提高局部性:插入排序是一种稳定的排序算法,它具有良好的局部性,可以充分利用已经有序的部分序列。对于较小的子序列,插入排序的效率更高。
  3. 减少分割次数:对于较小的子序列,使用插入排序可以减少分割的次数。快速排序的分割操作需要移动元素,而插入排序只需要进行元素的比较和交换,因此在较小的子序列中使用插入排序可以减少分割操作的次数。

小区间优化可以在一定程度上提高快速排序的性能。它通过减少递归深度、提高局部性和减少分割次数来优化算法的效率,特别适用于处理较小的子序列。

🌤️快速排序(非递归版)

这里需要借助栈的来实现非递归.关于栈详情见:数据结构剖析–栈

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, begin, end);
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);
}

☁️代码解析

  1. 将整个序列的起始和结束位置入栈。然后,进入循环,不断从栈中取出子序列的起始和结束位置。
  2. 在每次循环中,通过PartSort3函数将当前子序列分割成两部分,并得到基准值的下标keyi。如果基准值右边的子序列长度大于1,则将右边子序列的起始和结束位置入栈。如果基准值左边的子序列长度大于1,则将左边子序列的起始和结束位置入栈。
  3. 循环继续,直到栈为空,表示所有的子序列都已经排序完成。

通过使用栈来模拟递归的过程,非递归实现避免了递归调用的开销,提高了快速排序的效率。

🌤️快速排序的特性总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  2. 时间复杂度:O(N*logN)在这里插入图片描述

  3. 空间复杂度:O(logN)

  4. 稳定性:不稳定

🌤️全篇总结

​ 本章对快排从其思想到实现,一步步由浅入深的讲解,相信聪明的你看到这里已经对快排有一个明白的理解了!

看到这里希望给博主留个:👍点赞🌟收藏⭐️关注!

在这里插入图片描述

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

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

相关文章

网络运维Day02

文章目录 实验环境Linux系统简介Linux发行版RHEL与CentOS国产操作系统 安装CentOS7.9步骤一&#xff1a;虚拟硬件步骤二&#xff1a;为虚拟机安装操作系统为root设置密码首次初始化虚拟机 登录操作系统系统偏好设置 快照LINUX目录结构常见目录及其作用 LINUX磁盘表示方法LINUX中…

Linux常用命令——chage命令

在线Linux命令查询工具 chage 修改帐号和密码的有效期限 补充说明 chage命令是用来修改帐号和密码的有效期限。 语法 chage [选项] 用户名选项 -m&#xff1a;密码可更改的最小天数。为零时代表任何时候都可以更改密码。 -M&#xff1a;密码保持有效的最大天数。 -w&…

毅速丨3D打印在压铸模上大有所为

压铸模是压铸件成型不可缺少的工具。它的作用是将液态的金属或合金浇入到预先准备好的型腔中&#xff0c;并在压力的作用下凝固、成形。3D打印技术在压铸模上的使用&#xff0c;尤其是随形水路的应用&#xff0c;将大幅度提升制造效率。 在传统的压铸模制造中&#xff0c;水路的…

树结构及其算法-二叉排序树

目录 树结构及其算法-二叉排序树 C代码 树结构及其算法-二叉排序树 事实上&#xff0c;二叉树是一种很好的排序应用模式&#xff0c;因为在建立二叉树的同时&#xff0c;数据已经经过初步的比较&#xff0c;并按照二叉树的建立规则来存放数据&#xff0c;规则如下&#xff1…

Linux shell编程学习笔记21:用select in循环语句打造菜单

一、select in循环语句的功能 Linux shell脚本编程提供了select in语句&#xff0c;这是 Shell 独有的一种循环语句&#xff0c;非常适合终端&#xff08;Terminal&#xff09;这样的交互场景&#xff0c;它可以根据用户的设置显示出带编号的菜单&#xff0c;用户通过输入不同…

Draft-P802.11be-D3.2协议学习__$9-Frame-Format__$9.3.1.22-Trigger-frame-format

Draft-P802.11be-D3.2协议学习__$9-Frame-Format__$9.3.1.22-Trigger-frame-format 9.3.1.22.1 Genreal9.3.1.22.2 Common Info field9.3.1.22.3 Special User Info field9.3.1.22.4 HE variant User Info field9.3.1.22.5 EHT variant User Info field9.3.1.22.6 Basic Trigge…

关于JADX和JEB的小问题

关于JADX和JEB的小问题 很久没水过技术文啦&#xff0c;最近也刚好遇到点小问题&#xff0c;特此记录 第一个问题 在处理app加密逻辑的时候一直拿不到正确的密文&#xff0c;反复看了反编译出来的代码&#xff08;如下图&#xff09; public static string n(String str, Stri…

提升之做事方法

前言&#xff1a;在工作中&#xff0c;做事需要有一套方法&#xff0c;同样的一件事&#xff0c;不同的人去做&#xff0c;就能看到能力的差别&#xff0c;会产生不同的结果。那如何在工作中&#xff0c;提升自己做事的能力&#xff1f; 目录 闭环思维 做事方法论 事前规划 …

网络运维Day03

文章目录 基本命令使用查看文本文件内容-cat命令分页查看文本文件-less命令查看CPU信息-lscpu命令查看系统内核版本-uname命令查看机修改主机名-hostname命令查看IP地址-ifconfig命令创建目录-mkdir命令创建空文件-touch命令查看文件前几行-head命令查看文件后几行-tail命令快速…

2024年CleanMyMac更新了哪些内容?

CleanMyMac作为一款专业的苹果电脑清理软件&#xff0c;它不仅仅能单纯的卸载不用、少用的应用&#xff0c;同时还支持&#xff1a;1、清理应用程序的数据文件&#xff0c;将应用重置回初始状态&#xff0c;减少空间占用&#xff1b;2、自动检查应用更新&#xff0c;保持应用的…

Hyper-V 安装windows10 虚拟机,且能调试窗口大小、与主机之间复制文件

1. 搜索栏--打开‘启动或关闭windows功能’-- 勾选 ‘ Hyper-V ’ 然后点击确定&#xff1b; 2. 搜索栏--打开‘ Hyper-V 快速创建’ ---本地安装源---更改安装源&#xff08;选择 对应的 windows.iso 镜像&#xff09;---创建镜像--启动虚拟机--&#xff08;到达&#xff09;P…

优化|随机零阶优化算法分析

原文&#xff1a;Random Gradient-Free Minimization of Convex Functions. Found Comput Math 17, 527–566 (2017). https://doi.org/10.1007/s10208-015-9296-2 原文作者&#xff1a;Yurii Nesterov, Vladimir Spokoiny 论文解读者&#xff1a;陈宇文 本次知识分享活动挑选…

【中国知名企业高管团队】系列58:创维SKYWORTH

昨天华研荟介绍了位于深圳的彩电三巨头之一——康佳KONKA&#xff0c;今天介绍创维SKYWORTH。两家公司都是以做电视起家的&#xff0c;但是发展历程和现在的业务差别非常大&#xff0c;当然&#xff0c;过程中发生的故事也是波澜起伏。 一、关于创维 创维集团有限公司&#x…

JAVA面经整理(10)

一)MyBatis有什么优缺点&#xff1f; Mybatis是⼀种典型的半自动化的ORM 框架&#xff0c;所谓的半自动&#xff0c;因为还需要⼿动的写 SQL 语句在XML文件里面&#xff0c;再由框架根据SQL以及传入数据来进行组装成要执行的SQL&#xff0c;所谓的ORM框架&#xff0c;就是对象关…

burpsuite进行拦截请求包

当我们进行正常访问浏览器时&#xff0c;进行勾选住如上内容&#xff0c;若我们需要进行拦截某个请求时&#xff0c;首先将如上勾选去掉&#xff0c;然后打开proxy下的Intercept&#xff0c;确保intercept is on 按钮打开。 然后打开intercept is on 按钮即可进行拦截请求

温故知新-Redis01

目录 开场白-追命3连 使用场景 01缓存穿透场景与方案 02布隆过滤器 03缓存击穿场景与方案 04缓存雪崩场景与方案 开场白-追命3连 看你项目中有说用到Redis&#xff0c;都是哪些场景使用了Redis呢&#xff1f; 如果发生了缓存穿透、击穿、雪崩如何应对呢&#xff1f;缓存…

LangChain+LLM实战---ChatGPT的即时插件套件制作

英文原文&#xff1a;Instant Plugins for ChatGPT: Introducing the Wolfram ChatGPT Plugin Kit 在一分钟内构建一个新插件 几周前&#xff0c;我们与OpenAI合作发布了Wolfram插件&#xff0c;使ChatGPT可以使用Wolfram语言和Wolfram|Alpha作为工具&#xff0c;在ChatGPT内部…

Day 6 登录页以及路由(四)Vue页面处理

系列文章目录 本系列记录一下通过Abp搭建后端&#xff0c;VueElement UI Plus搭建前端&#xff0c;实现一个小型项目的过程。 Day 1 Vue 页面框架Day 2 Abp框架下&#xff0c;MySQL数据迁移时&#xff0c;添加表和字段注释Day 3 登录页以及路由 (一&#xff09;Day 4 登录页及…

我终于学会的前端技能——代码调试、打断点

在技术的世界里&#xff0c;要用魔法来打败魔法 说来惭愧我做前端已近三年了竟然还没有学会如何调试代码&#xff0c;也就是给自己的代码打上断点一步步看它的运行状态以达到理清代码运行逻辑、排查问题提升开发效率的目的。直到最近我才学会了这一技能&#xff0c;在这之前我用…

抖音小店参与双十一活动:销售增长的策略与实施

双十一是中国最大的购物狂欢节&#xff0c;对于抖音小店商家来说&#xff0c;参与双十一活动是一个极好的机会&#xff0c;可以在这个繁忙的购物季节中大幅提升销售。下面四川不若与众将介绍一些抖音小店商家如何参与双十一活动的策略和注意事项。 1. 提前准备&#xff1a;在双…