【算法】一文看懂快速排序!!!

【算法】一文看懂快速排序!!!
请添加图片描述

一文看懂快速排序

前言:在各种排序算法中,快速排序(Quick Sort)几乎是“算法界的明星”。它由 Tony Hoare 在 1962 年提出,凭借着分治思想与高效的平均性能,成为众多编程语言和标准库中的默认排序算法。
相比于冒泡排序、选择排序这样的“基础选手”,快速排序更像是一位善于策略分配的“指挥官”:它先选出一个基准值(key),再通过一趟划分,把比基准小的元素放在左边,比基准大的元素放在右边,然后递归处理子区间,直到整个序列有序。
得益于这种巧妙的思路,快速排序在大多数情况下能以 O(N log N) 的时间复杂度完成任务,且只需较少的额外空间,几乎成为了“高效排序”的代名词。不过,快速排序并非完美无缺,它在极端情况下可能退化为 O(N²),同时也不是一个稳定的排序算法。
在这篇文章中,我们将从原理到实现、从优化到应用,全面剖析快速排序,帮助你既能在面试中“手撕快排”,也能在工程实践中理解为何它依然是排序算法的首选之一。
📖专栏【算法】

目录


一、快速排序简介

1.1 快排的提出与背景

  • 1962 年,Tony Hoare 提出快速排序,并凭借这一成果获得了图灵奖。
  • 快速排序利用 分治思想,通过“划分—递归”的方式实现排序。

1.2 基本思想

1. 在数组中选择一个基准值(key);
2. 将数组划分为两部分:左边比 key 小,右边比 key 大;
3. 递归排序左右两部分;
4. 最终合并得到有序序列。

二、快速排序的实现

在这里,最重要的就是将数组划分为两部分:左边比 key 小,右边比 key 大。
对于将数组划分为两部分的方法有三种,本质思想都是一样的,但实现的方法却有点不同,我们来看一下:

三种划分方式

以下面一组数据为例,我们假设key的值是最左边的值。

*** 2.2.1 Hoare版本**

看了这个动图之后,可知,它是定义了两个下标(L与R),分别从两侧开始找,R先走,找到比key小的值,停下来;L开始走,找到比key大的值,停下来,交换对应的值即可,然后继续重复,直到两者相遇,交换相遇点与key的值即可,这就是Hoare的思想。还是很好理解的,但是这个方法会导致很多人有疑问?我也重点标记了。
答疑解惑:为什么两者相遇,交换相遇点与key的值?那万一此时的值比key大呢?
在这里我们就要明白一个点,两者会以什么方式相遇,简单,要么L在移动的时候遇到了R,要么R在移动的时候遇到了L。
L在移动的时候遇到了R的情况:
由于是R先走的缘故,所以R此时指向的是比key小的值,此时L遇到了R,相遇点即为比key小的值
R在移动的时候遇到了L的情况:
此时L停留在上次交换的位置,上次交换的是比key小的值,即相遇点为比key小的值,也有可能L停留在key的位置。结果都是一样。

为什么R先走?
其实,上面的解释已经有了答案。我们也可以看看如果L先走的话,会发生什么情况:

在这里插入图片描述


我们会发现,当L先走时,可以把key设在右边。

那现在我们来进行代码的完成。

int_QuickSort(int* a,int left,int right){int key = left;// 选择最左边为基准while(left < right){while(left < right && a[right]>= a[key])//只找比key位置小的 right--;while(left < right && a[left]<= a[key]) left++;Swap(&a[right],&a[left]);}Swap(&a[left],&a[key]);// 将基准值放到正确位置return left;// 返回基准值最终位置}

现在我们完成了快速排序中的单次划分,上面我们说过,递归排序左右两部分,怎么完成呢,来分析:

也就是说,左右两部分和首次排序的思想是一样的,然后,左右两部分的左右两部分又相同,所以对于递归已经发现清楚了,下面来完成代码吧:

voidQuickSort(int* a,int left,int right){if(left >= right)// 递归终止条件:区间只有一个元素或为空return;int mid =_QuickSort(a, left, right);// 划分操作,返回基准值的最终位置QuickSort(a, left, mid -1);// 递归排序左半部分QuickSort(a, mid +1, right);// 递归排序右半部分}

至此,快排的基本框架完成,为什么说基本框架,因为快排后序会有多种优化,后面会说。下面这两种方法我们可以来看一下思想,代码很简单。

  • 2.2.2 挖坑法
  • 2.2.3 前后指针法

这两个我们了解一下思想即可。霍尔法虽然在实现上稍复杂,但其优异的性能表现使其成为大多数标准库实现的选择。

三、快速排序的优化

对于我们实现的快排,问题在于key的取值,如果key的取值大多数接近数组最小值,极端一点,就会导致:


所以我们使得key的取值尽量靠近中间值。便有了三数取中法选择基准值

3.1三数取中法选择基准值

三数取中法,顾名思义,选择三个数的中间值,看代码

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

3.2 小区间转插入排序

对于快排来说,在数据量较小时,就没有使用的必要了,所以在数据量较小时选择插入排序即可。代码实现

voidQuickSort(int* a,int left,int right){if(left >= right)// 递归终止条件:区间只有一个元素或为空return;if(right - left +1<=10)//数据量较小时用插入排序{InsertSort(a + left, right - left +1);return;}int mid =_QuickSort(a, left, right);// 划分操作,返回基准值的最终位置QuickSort(a, left, mid -1);// 递归排序左半部分QuickSort(a, mid +1, right);// 递归排序右半部分}

3.3 其他优化方法简介

  1. 对于快排的优化远不止于此,我们会发现,在每次划分的时候,对于与key相等的值,我们选择是不动的,这样也会使得效率较低,由此便产生了三路划分,即在找比key小的值和大的值的同时,找与key相等的值。但是,三路划分又对于重复数值不多的情况,效率有所减少。
  2. 前面我们说过的三数取中法有时还会导致key的值不理想,递归的层次较大,所以,有大佬便想到了,当递归层次较大时,选择堆排,在数据量较小时,选择插入排序,这种又称为自省排序

四、非递归实现

对于快排,我们还需要会用非递归实现。
这个时候,思想还是递归的思想,只是,我们要用到栈了:对于快排的递归来说,每次转入的都是leftright。那我们对于非递归来说,每次把leftright都传入栈中,每次当leftright出栈进行操作后,把划分后的区间又入栈,即可。
停止条件:“递归终止条件:区间只有一个元素或为空”,那我们非递归对于left >= right是不入栈即可或者出栈后不进行操作即可。
代码完成:

voidQuickSortNonR(int* a,int left,int right){ Stack st;StackInit(&st);StackPush(&st, left);StackPush(&st, right);while(StackEmpty(&st)!=0){ right =StackTop(&st);StackPop(&st); left =StackTop(&st);StackPop(&st);if(right <= left )continue;int mid =_QuickSort(a, left, right);// 以基准值为分割点,形成左右两部分:[left, mid-1 ] 和 [mid+1, right]StackPush(&st, mid +1);StackPush(&st, right);StackPush(&st, left);StackPush(&st, mid -1);}StackDestroy(&st);}

对于栈的实现,详见【栈】

五、快速排序的性能分析

5.1 时间复杂度

对于快排的时间复杂度一般为O(N*logN),看下图递归树。假设基准值每次都可以取到中间。


**但是快排最坏能到O(N^2),当数组完全有序(正序或逆序),每次选择的基准值都是最小或最大元素。但是一般少发生且可以避免。**毕竟快排还有很多优化。

5.2 空间复杂度

如上图,递归为O(logN),非递归也为O(logN),非递归我们可以看做二叉树的前序遍历,一直从上图的左边下去。简单点,就是,出一个,带两个,持续logN次。

5.3 稳定性

不稳定,相同元素的相对顺序可能改变。

在这里插入图片描述

六、总结

快速排序通过“选基准、分区间、递归处理”的分治思想,实现了高效的排序。它的核心优势是平均时间复杂度接近O(N log N),并且额外空间开销较小,因此被广泛应用于标准库中。虽然在极端情况下可能退化为 O(N²),而且本身不稳定,但通过三数取中、小区间转插排、三路划分等优化手段,可以在实际场景中保持优秀表现。递归和非递归两种实现方式各有应用,递归更直观,非递归更适合工程。总体来看,快速排序因高效与实用,成为排序算法中的“明星”。


如果本文对您有启发:
点赞 -让更多人看到这篇硬核技术解析 !
收藏 -实战代码随时复现
关注 -获取数据结构系列深度更新
您的每一个[三连]都是我们持续创作的动力!✨
请添加图片描述

Read more

算法应用:2024年算法牛顿-拉夫逊算法(NRBO)无人机路径规划研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭:行百里者,半于九十。 📋📋📋本文内容如下:🎁🎁🎁  ⛳️赠与读者 👨‍💻做科研,涉及到一个深在的思想系统,需要科研者逻辑缜密,踏实认真,但是不能只是努力,很多时候借力比努力更重要,然后还要有仰望星空的创新点和启发点。建议读者按目录次序逐一浏览,免得骤然跌入幽暗的迷宫找不到来时的路,它不足为你揭示全部问题的答案,但若能解答你胸中升起的一朵朵疑云,也未尝不会酿成晚霞斑斓的别一番景致,万一它给你带来了一场精神世界的苦雨,那就借机洗刷一下原来存放在那儿的“躺平”上的尘埃吧。      或许,雨过云收,神驰的天地更清朗.......🔎🔎🔎 💥第一部分——内容介绍 2024年牛顿-拉夫逊算法(NRBO)在无人机三维路径规划中的研究进展 摘要 随着无人机技术的快速发展,三维路径规划作为其核心任务之一,面临复杂地形、动态障碍物及实时性要求等挑战。2024年提出的牛顿-拉夫逊优化算法(

By Ne0inhk
《算法题讲解指南:优选算法-位运算》--33.判断字符是否唯一,34.丢失的数字

《算法题讲解指南:优选算法-位运算》--33.判断字符是否唯一,34.丢失的数字

🔥小叶-duck:个人主页 ❄️个人专栏:《Data-Structure-Learning》 《C++入门到进阶&自我学习过程记录》《算法题讲解指南》--从优选到贪心 ✨未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游 目录 位运算基础前置知识: 位1的个数 比特位计数 汉明距离 只出现一次的数字 只出现一次的数字||| 34. 判断字符是否唯一 题目链接: 题目描述: 题目示例: 解法(位图的思想): 算法思路: C++算法代码: 算法总结及流程解析: 35. 丢失的数字 题目链接: 题目描述: 题目示例: 解法(位运算): 算法思路: C++算法代码: 算法总结及流程解析: 结束语 位运算基础前置知识:       回顾了上面位运算基础前置的知识这里有五道非常简单的题可以试试手,都是考察位运算的题目: 位1的个数 191.

By Ne0inhk
数据结构中的栈与队列:原理、实现与应用

数据结构中的栈与队列:原理、实现与应用

前言:栈和队列是计算机科学中两种最基础的线性数据结构,它们的独特操作规则和广泛的应用场景使其成为每一位开发者必须掌握的核心知识。本文将通过生活案例、代码实现和实际应用场景,带您深入理解这两种数据结构的精髓。 1.栈(Stack) 基本定义 栈是一种受限的线性表,仅允许在同一端(栈顶)进行数据插入(push)和删除(pop)操作。其核心特性遵循LIFO(后进先出)(Last In First Out)原则,即后进入的元素优先被访问。 LIFO原则后进先出(Last In First Out):最后入栈的元素总是最先出栈入栈(Push):将新元素放入栈顶出栈(Pop):移除并返回栈顶元素 核心机制 * 单端操作:所有操作集中在栈顶完成 * 动态指针:通过栈顶指针(top)实时跟踪最新元素位置 * 操作限制:禁止直接访问中间元素,必须按序操作 * 空间管理:顺序栈需预判容量,链式栈动态扩展但需额外指针空间 2.

By Ne0inhk
Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构

Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 vnlunar 适配鸿蒙 HarmonyOS 实战:高精度农历算法,构建民俗文化日期与节气治理架构 前言 在鸿蒙(OpenHarmony)生态迈向全球化部署、涉及多语言本地化(L10n)及深层文化特性适配的背景下,如何实现准确的阴阳历(农历)转换、二十四节气计算及民俗节日提醒,已成为提升应用“人文温度”与本地化竞争力的核心要素。在鸿蒙设备这类强调分布式时间同步与低功耗常驻显示(AOD)的环境下,如果应用依然依赖简单的查表法或通过网络接口获取农历信息,由于由于闰月计算的复杂性或离线环境限制,极易由于由于计算偏移导致传统节日提醒的误报。 我们需要一种能够实现天文级算法推演、支持高精度节气定位且具备纯 Dart 离线运作能力的历法治理方案。 vnlunar 为 Flutter 开发者引入了标准化的阴阳历转换协议。它不仅支持对天干地支、生肖及闰月的精确解构,更针对东南亚等地区的历法细微差异提供了专项适配。在适配到鸿蒙 HarmonyOS 流程

By Ne0inhk