快速排序
- 导读
- 一、基本思想
- 二、排序过程
-
- [2.1 第一次拆分](#2.1 第一次拆分)
-
- [2.1.1 基准选择](#2.1.1 基准选择)
- [2.1.2 序列拆分](#2.1.2 序列拆分)
- [2.1.3 交换排序](#2.1.3 交换排序)
- [2.2 第二次拆分](#2.2 第二次拆分)
-
- [2.2.1 基准选择](#2.2.1 基准选择)
- [2.2.2 序列拆分](#2.2.2 序列拆分)
- [2.2.3 交换排序](#2.2.3 交换排序)
- [2.3 第三次拆分](#2.3 第三次拆分)
-
- [2.3.1 基准选择](#2.3.1 基准选择)
- [2.3.2 序列拆分](#2.3.2 序列拆分)
- [2.3.3 交换排序](#2.3.3 交换排序)
- [2.4 第四次拆分](#2.4 第四次拆分)
-
- [2.4.1 基准选择](#2.4.1 基准选择)
- [2.4.2 序列拆分](#2.4.2 序列拆分)
- [2.4.3 交换排序](#2.4.3 交换排序)
- [2.5 小结](#2.5 小结)
- 结语

导读
大家好,很高兴又和大家见面啦!!!
在上一篇内容中,我们介绍了 交换排序 的基本思想,以及第一种 交换排序算法 ------冒泡排序。
交换 指的是:根据序列中两个元素关键字的比较结果,来对换这两个记录在序列中的位置。
交换排序思想 则是通过 比较 与 交换 这两步 核心操作 完成排序。
冒泡排序 正是基于 交换排序思想 ,最直观、也最简单的实现方式。
然而,正是因为它在排序过程中,需要频繁执行比较与交换 来完成每一次 冒泡 ,使得其算法效率相对低下 。
在上一篇末尾,我们留下了一个问题:
- 这是否意味着交换排序思想整体上不如插入排序思想?
现在我可以明确告诉你:不是的 。
冒泡排序 只是 交换排序家族 的 先行者 ,它揭示了该思想的 核心操作,却远未展现其真正的效率巅峰 。
我们可以这样比喻:
交换排序思想 如同一匹能够日行千里的 千里马 ,但要发挥它的全部潜力,还需要一位懂它的 伯乐。
那么,这位伯乐究竟是谁呢?
不卖关子了------正是 分治策略 。
当 交换 这匹 千里马 ,遇上 分治 这位伯乐,两者融合,便诞生了今天的主角:
- 在排序算法领域享有盛名,被誉为 20世纪十大算法之一 的------快速排序。
那么,快速排序究竟如何将 分治 与 交换 精妙结合,实现效率的飞跃?它又有哪些独到的魅力与智慧?
让我们带着这些期待,一同走进 快速排序 的精彩世界。
一、基本思想
快速排序 (quick sort )是一种 高效的排序算法 ,它采用 分治策略 ,通过 递归 地将数据分割成较小的部分来实现排序。也就是说 快排 是一个结合了 交换排序思想 与 分治策略 的 递归算法 。
快速排序 的 基本思想 是:
- 在待排序表 L [ 1 ⋯ n ] L[1\cdots n] L[1⋯n]中任取一个元素
pivot作为 枢轴 (或基准,通常取首元素) - 通过一趟排序将待排序表划分为独立的两部分 L [ 1 ⋯ k − 1 ] L[1 \cdots k-1] L[1⋯k−1] 和 L [ k + 1 ⋯ n ] L[k+1 \cdots n] L[k+1⋯n]
- L [ 1 ⋯ k − 1 ] L[1 \cdots k-1] L[1⋯k−1] 中的所有元素小于
pivot - L [ k + 1 ... ... n ] L[k+1......n] L[k+1......n] 中的所有元素大于等于
pivot
- L [ 1 ⋯ k − 1 ] L[1 \cdots k-1] L[1⋯k−1] 中的所有元素小于
pivot放在了其最终位置 L ( k ) L(k) L(k) 上,这个过程称为 一次划分。- 分别 递归 地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
快排 的 核心思想 可以概括为:选取基准、分区、递归。
二、排序过程
这里我们具体的实例更好的来说明其排序过程:
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 | 3 | 2 | 1 | 0 |
就比如上表这个具有 7 7 7 个元素的 降序序列 ,我们希望通过 快排 实现 升序排列 ,那么我们会将 关键字序列 逐步进行 拆分 。那它究竟是如何通过 拆分 完成最终排序的呢?下面就让我们一起来感受一下其内在的魅力;
2.1 第一次拆分
2.1.1 基准选择
快排 在每一次进行 拆分 前都需要选择一个 基准 ,通常选取 首元素 ,这里我们就直接简单点,以 首元素 作为 基准;
确定好 基准 后,接下来我们就需要以该 基准 将序列拆分为两部分:
左侧小元素
基准元素
右侧大元素
2.1.2 序列拆分
这里我们所说的拆分,实际上指的是 逻辑拆分 而不是真正意义上的拆分。具体的拆分过程我们是借助的 折半思想 ,通过 左右指针 来实现 逻辑拆分:
- 左指针 指向 左侧小元素部分
- 右指针 指向 右侧大元素部分
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{black}{0} 0 |
| 左右指针: | 基准、L | R |
2.1.3 交换排序
完成了 逻辑拆分 后,接下来我们就需要开始通过 交换排序思想 进行初步的 交换排序,具体过程如下:
- 移动右指针,当右指针找到 小于基准值的元素 时停止查找
- 移动左指针,当左指针找到 大于基准值的元素 时停止查找
- 交换左右指针所指向的元素,使其继续保持:左指针指向小元素,右指针指向大元素
- 重复上述过程,直到左右指针相遇
- 交换指针所指向的元素与基准元素
下面我们就来开始实操:
- 第一次查找: 0 < 4 0 < 4 0<4, R R R 指针找到小元素,再移动 L L L 指针
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{blue}{0} 0 |
| 左右指针: | 基准、 L | R |
- 第二次查找: 4 = = 4 4 == 4 4==4, L L L 指针右移
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{black}{0} 0 |
| 左右指针: | 基准 | L | R |
- 第三次查找: 3 < 4 3 < 4 3<4, L L L 指针右移
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{blue}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{black}{0} 0 |
| 左右指针: | 基准 | L | R |
- 第四次查找: 2 < 4 2 < 4 2<4, L L L 指针右移
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{blue}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{black}{0} 0 |
| 左右指针: | 基准 | L | R |
- 第五次查找: 1 < 4 1 < 4 1<4, L L L 指针右移
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 初始状态: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{blue}{1} 1 | 0 \textcolor{black}{0} 0 |
| 左右指针: | 基准 | L | R |
- 第六次查找: 0 < 4 0 < 4 0<4, L 、 R L、R L、R 指针相遇,结束查找并完成交换
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 交换前: | 4 \textcolor{red}{4} 4 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 0 \textcolor{blue}{0} 0 |
| 左右指针: | 基准 | L、R | |||
| 交换后: | 0 \textcolor{blue}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 | 4 \textcolor{red}{4} 4 |
| 左右指针: | 基准、L、R |
此时第一次的拆分就已经完成,这时的 关键字序列 就被我们以 基准元素 4 4 4 为分界线拆分成了两部分:
左侧 < 4
基准元素4
右侧 > 4
完成了第一次的拆分后,此时 左侧元素 的个数 n l ≥ 1 n_l \geq 1 nl≥1 ,右侧元素 的个数 n r = = 0 n_r == 0 nr==0,这就表示 右侧元素 已经不需要继续拆分,左侧元素 还需要进一步拆分;
2.2 第二次拆分
在进行第二次拆分时,根据 分治策略 的思想,我们是需要分别对 左侧元素 以及 右侧元素 完成 拆分操作 ,但是由于此时 基准 4 4 4 的右侧不存在任何元素,因此,我们只需要完成对 左侧的拆分;
2.2.1 基准选择
同样的,由于第一次选择的 基准 是 首元素 ,那么本次的 基准 我们同样要选择 首元素:
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 初始状态: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准 |
2.2.2 序列拆分
同样我们还是以 左右指针 来实现 逻辑拆分:
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 初始状态: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L | R |
2.2.3 交换排序
- 第一次查找: 1 > 0 1 > 0 1>0, R R R 指针左移
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 初始状态: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{blue}{1} 1 |
| 左右指针: | 基准、L | R |
- 第二次查找: 2 > 0 2 > 0 2>0, R R R 指针左移
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 初始状态: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{blue}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L | R |
- 第三次查找: 3 > 0 3 > 0 3>0, R R R 指针左移
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 初始状态: | 0 \textcolor{red}{0} 0 | 3 \textcolor{blue}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L | R |
- 第四次查找: 0 = = 0 0 == 0 0==0, R 、 L R、L R、L 指针相遇,结束查找并完成交换
| 下标 | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 交换前: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L、R | |||
| 交换后: | 0 \textcolor{red}{0} 0 | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L、R |
此时序列就以 0 0 0 作为分界线分成了两部分:
左侧 < 0
基准元素0
右侧 > 0
由于 0 0 0 的左侧不存在任何元素,因此不需要继续 拆分 ;而 0 0 0 的右侧还存在 3 3 3 个元素,因此还要继续对右侧进行 拆分;
2.3 第三次拆分
上一轮拆分中,基准元素 0 0 0 的右侧序列为:
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{black}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
2.3.1 基准选择
这里我们同样还是选择 首元素 作为基准:
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{red}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准 |
2.3.2 序列拆分
我们同样还是以 左右指针 来实现 逻辑拆分:
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{red}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L | R |
2.3.3 交换排序
- 第一次查找: 1 < 3 1 < 3 1<3 , R R R 指针找到了小元素,再移动 L L L 指针
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{red}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{blue}{1} 1 |
| 左右指针: | 基准、L | R |
- 第二次查找: 3 = = 3 3 == 3 3==3 , L L L 指针右移
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{red}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准、L | R |
- 第三次查找: 2 < 3 2 < 3 2<3 , L L L 指针右移
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 初始状态: | 3 \textcolor{red}{3} 3 | 2 \textcolor{blue}{2} 2 | 1 \textcolor{black}{1} 1 |
| 左右指针: | 基准 | L | R |
- 第四次查找: 1 < 3 1 < 3 1<3 , L 、 R L、R L、R 指针相遇,结束查找并完成交换
| 下标 | 1 | 2 | 3 |
|---|---|---|---|
| 交换前: | 3 \textcolor{red}{3} 3 | 2 \textcolor{black}{2} 2 | 1 \textcolor{blue}{1} 1 |
| 左右指针: | 基准 | L 、R | |
| 交换后: | 1 \textcolor{blue}{1} 1 | 2 \textcolor{black}{2} 2 | 3 \textcolor{red}{3} 3 |
| 左右指针: | 基准、 L 、R |
可以看到,这一次之后,元素已经有序,但是,因为 3 3 3 的左侧还是有两个元素,因此还是会进行第四次拆分;
2.4 第四次拆分
这一次拆分也是和前面的步骤一样,这里我就不再赘述,直接看过程;
2.4.1 基准选择
| 下标 | 1 | 2 |
|---|---|---|
| 交换后: | 1 \textcolor{red}{1} 1 | 2 \textcolor{black}{2} 2 |
| 左右指针: | 基准 |
2.4.2 序列拆分
| 下标 | 1 | 2 |
|---|---|---|
| 初始状态: | 1 \textcolor{red}{1} 1 | 2 \textcolor{black}{2} 2 |
| 左右指针: | 基准、L | R |
2.4.3 交换排序
- 第一次查找: 2 > 1 2 > 1 2>1 , R R R 指针左移
| 下标 | 1 | 2 |
|---|---|---|
| 初始状态: | 1 \textcolor{red}{1} 1 | 2 \textcolor{blue}{2} 2 |
| 左右指针: | 基准、L | R |
- 第二次查找: 1 = = 1 1 == 1 1==1 , L 、 R L、R L、R 指针相遇,结束查找并完成交换
| 下标 | 1 | 2 |
|---|---|---|
| 交换前: | 1 \textcolor{red}{1} 1 | 2 \textcolor{black}{2} 2 |
| 左右指针: | 基准、L、R | |
| 交换后: | 1 \textcolor{red}{1} 1 | 2 \textcolor{black}{2} 2 |
| 左右指针: | 基准、L、R |
此时由于 基准元素 1 1 1 的左侧不存在任何元素,右侧只有一个元素,因此不再继续拆分,并且完成了最终的排序;
2.5 小结
整个排序过程实际上就是一个 递归 的过程,每一次的 拆分 就是在执行一轮 递进 ,当完成第四轮 拆分 后,算法就会开始回归;
正是因为 快排 是一个 递归算法 ,所以我们在整个排序过程中才能看到 快排 在每一次的 递进 中就做了三件事:
- 基准选择 :选择一个用于分割序列的 基准元素 作为序列的 分界线
- 序列拆分 :通过 左右指针 来完成 逻辑拆分
- 交换排序 :通过 左右指针 的 交替比较 与 交换 ,将元素调整到其正确的分区中。
- 具体来说,在每一步中,指针通过比较 基准元素 与 当前元素 的大小关系,决定是否进行交换,这种 交替扫描 的方式是提高查找效率的关键。
通过每一次递归中的这三步操作,快速排序 成功地 将原始问题分解为规模更小的子问题 ,并 通过解决这些子问题最终合并(由于是原地排序,合并是自动完成的)得到有序序列。
结语
通过今天的学习,我们深入探讨了 快速排序 这一高效算法的核心思想 与 排序过程 。快速排序 凭借其 分而治之 的策略,通过 选取基准 、分区操作 和 递归排序 ,将复杂问题层层分解,展现出了简洁而强大的排序逻辑。
回顾全文,我们揭示了快速排序的三大核心步骤:
- 基准选择:通常选取首元素作为基准,将序列逻辑分割为左右两部分。
- 分区操作:通过左右指针的交替扫描与元素交换,将小于基准的元素调整至其左侧,大于基准的元素调整至其右侧。
- 递归排序:将分区后的左右子序列作为新的待排序序列,重复上述过程,直至序列完全有序。
值得注意的是,分区操作中指针的交替移动与条件交换是保证算法正确性的关键。
在下一篇内容中,我们将告别理论描述,亲手使用 C语言 来实现经典的 Hoare快速排序算法。我们将不仅实现代码,还会在此基础上深入分析其性能,探讨其平均情况下卓越效率的原因。具体内容包括:
- 详细解析如何用代码控制左右指针的精准移动。
- 深入探讨元素交换的具体实现与边界条件处理。
- 分析算法的时间与空间复杂度,理解其高效背后的原理。
- 讨论递归调用的优化以避免栈溢出风险。
通过代码的实现与性能分析,我们将更加深刻地体会快速排序的巧妙之处。敬请期待,我们下一篇实战篇再见!
互动与分享
-
点赞👍 - 您的认可是我持续创作的最大动力
-
收藏⭐ - 方便随时回顾这些重要的基础概念
-
转发↗️ - 分享给更多可能需要的朋友
-
评论💬 - 欢迎留下您的宝贵意见或想讨论的话题
感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!