
(快速排序)
大家好,很高兴又和大家见面啦!!! 在上一篇内容中,我们介绍了 交换排序 的基本思想,以及第一种 交换排序算法——冒泡排序。
交换 指的是:根据序列中两个元素关键字的比较结果,来对换这两个记录在序列中的位置。 交换排序思想 则是通过 比较 与 交换 这两步 核心操作 完成排序。
冒泡排序 正是基于 交换排序思想,最直观、也最简单的实现方式。 然而,正是因为它在排序过程中,需要频繁执行比较与交换来完成每一次 冒泡 ,使得其算法效率相对低下。 在上一篇末尾,我们留下了一个问题:
现在我可以明确告诉你:不是的。 冒泡排序只是 交换排序家族 的 先行者,它揭示了该思想的 核心操作,却远未展现其真正的效率巅峰。 我们可以这样比喻:
交换排序思想 如同一匹能够日行千里的 千里马,但要发挥它的全部潜力,还需要一位懂它的 伯乐。
那么,这位伯乐究竟是谁呢? 不卖关子了——正是 分治策略。 当 交换 这匹 千里马,遇上 分治 这位伯乐,两者融合,便诞生了今天的主角:
那么,快速排序究竟如何将 分治 与 交换 精妙结合,实现效率的飞跃?它又有哪些独到的魅力与智慧? 让我们带着这些期待,一同走进 快速排序 的精彩世界。
快速排序(quick sort)是一种 高效的排序算法,它采用 分治策略,通过 递归 地将数据分割成较小的部分来实现排序。也就是说 快排 是一个结合了 交换排序思想 与 分治策略 的 递归算法。 快速排序 的 基本思想 是:
快排 的 核心思想 可以概括为:选取基准、分区、递归。
这里我们具体的实例更好的来说明其排序过程:
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | 4 | 3 | 2 | 1 |
就比如上表这个具有 7 个元素的 降序序列 ,我们希望通过 快排 实现 升序排列,那么我们会将 关键字序列 逐步进行 拆分 。那它究竟是如何通过 拆分 完成最终排序的呢?下面就让我们一起来感受一下其内在的魅力;
快排 在每一次进行 拆分 前都需要选择一个 基准,通常选取 首元素 ,这里我们就直接简单点,以 首元素 作为 基准;
确定好 基准 后,接下来我们就需要以该 基准 将序列拆分为两部分:
flowchart LR
a[左侧小元素] ---> b[基准元素] ---> c[右侧大元素]这里我们所说的拆分,实际上指的是 逻辑拆分 而不是真正意义上的拆分。具体的拆分过程我们是借助的 折半思想 ,通过 左右指针 来实现 逻辑拆分:
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{black}{0}$ |
左右指针: | 基准、L | R |
完成了 逻辑拆分 后,接下来我们就需要开始通过 交换排序思想 进行初步的 交换排序,具体过程如下:
下面我们就来开始实操:
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{blue}{0}$ |
左右指针: | 基准、 L | R |
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{black}{0}$ |
左右指针: | 基准 | L | R |
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{blue}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{black}{0}$ |
左右指针: | 基准 | L | R |
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{blue}{2}$ | $\textcolor{black}{1}$ | $\textcolor{black}{0}$ |
左右指针: | 基准 | L | R |
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
初始状态: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{blue}{1}$ | $\textcolor{black}{0}$ |
左右指针: | 基准 | L | R |
下标 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
交换前: | $\textcolor{red}{4}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{blue}{0}$ |
左右指针: | 基准 | L、R | |||
交换后: | $\textcolor{blue}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ | $\textcolor{red}{4}$ |
左右指针: | 基准、L、R |
此时第一次的拆分就已经完成,这时的 关键字序列 就被我们以 基准元素 4 为分界线拆分成了两部分:
flowchart LR
a[左侧小于4] ---> b[基准元素4] ---> c[右侧大于4]完成了第一次的拆分后,此时 左侧元素 的个数 n_l \geq 1 ,右侧元素 的个数 n_r == 0,这就表示 右侧元素 已经不需要继续拆分,左侧元素 还需要进一步拆分;
在进行第二次拆分时,根据 分治策略 的思想,我们是需要分别对 左侧元素 以及 右侧元素 完成 拆分操作,但是由于此时 基准 4 的右侧不存在任何元素,因此,我们只需要完成对 左侧的拆分;
同样的,由于第一次选择的 基准 是 首元素 ,那么本次的 基准 我们同样要选择 首元素:
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
初始状态: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准 |
同样我们还是以 左右指针 来实现 逻辑拆分:
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
初始状态: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
初始状态: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{blue}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
初始状态: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{blue}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
初始状态: | $\textcolor{red}{0}$ | $\textcolor{blue}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 | |
|---|---|---|---|---|
交换前: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L、R | |||
交换后: | $\textcolor{red}{0}$ | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L、R |
此时序列就以 0 作为分界线分成了两部分:
flowchart LR
a[左侧小于0] ---> b[基准元素0] ---> c[右侧大于0]由于 0 的左侧不存在任何元素,因此不需要继续 拆分;而 0 的右侧还存在 3 个元素,因此还要继续对右侧进行 拆分;
上一轮拆分中,基准元素 0 的右侧序列为:
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{black}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
这里我们同样还是选择 首元素 作为基准:
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{red}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准 |
我们同样还是以 左右指针 来实现 逻辑拆分:
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{red}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{red}{3}$ | $\textcolor{black}{2}$ | $\textcolor{blue}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{red}{3}$ | $\textcolor{black}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 | 3 |
|---|---|---|---|
初始状态: | $\textcolor{red}{3}$ | $\textcolor{blue}{2}$ | $\textcolor{black}{1}$ |
左右指针: | 基准 | L | R |
下标 | 1 | 2 | 3 |
|---|---|---|---|
交换前: | $\textcolor{red}{3}$ | $\textcolor{black}{2}$ | $\textcolor{blue}{1}$ |
左右指针: | 基准 | L 、R | |
交换后: | $\textcolor{blue}{1}$ | $\textcolor{black}{2}$ | $\textcolor{red}{3}$ |
左右指针: | 基准、 L 、R |
可以看到,这一次之后,元素已经有序,但是,因为 3 的左侧还是有两个元素,因此还是会进行第四次拆分;
这一次拆分也是和前面的步骤一样,这里我就不再赘述,直接看过程;
下标 | 1 | 2 |
|---|---|---|
交换后: | $\textcolor{red}{1}$ | $\textcolor{black}{2}$ |
左右指针: | 基准 |
下标 | 1 | 2 |
|---|---|---|
初始状态: | $\textcolor{red}{1}$ | $\textcolor{black}{2}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 |
|---|---|---|
初始状态: | $\textcolor{red}{1}$ | $\textcolor{blue}{2}$ |
左右指针: | 基准、L | R |
下标 | 1 | 2 |
|---|---|---|
交换前: | $\textcolor{red}{1}$ | $\textcolor{black}{2}$ |
左右指针: | 基准、L、R | |
交换后: | $\textcolor{red}{1}$ | $\textcolor{black}{2}$ |
左右指针: | 基准、L、R |
此时由于 基准元素 1 的左侧不存在任何元素,右侧只有一个元素,因此不再继续拆分,并且完成了最终的排序;
整个排序过程实际上就是一个 递归 的过程,每一次的 拆分 就是在执行一轮 递进,当完成第四轮 拆分 后,算法就会开始回归; 正是因为 快排 是一个 递归算法 ,所以我们在整个排序过程中才能看到 快排 在每一次的 递进 中就做了三件事:
通过每一次递归中的这三步操作,快速排序 成功地 将原始问题分解为规模更小的子问题,并 通过解决这些子问题最终合并(由于是原地排序,合并是自动完成的)得到有序序列。
通过今天的学习,我们深入探讨了 快速排序 这一高效算法的核心思想 与 排序过程。快速排序 凭借其 分而治之 的策略,通过 选取基准、分区操作 和 递归排序,将复杂问题层层分解,展现出了简洁而强大的排序逻辑。 回顾全文,我们揭示了快速排序的三大核心步骤:
值得注意的是,分区操作中指针的交替移动与条件交换是保证算法正确性的关键。 在下一篇内容中,我们将告别理论描述,亲手使用 C语言 来实现经典的 Hoare快速排序算法。我们将不仅实现代码,还会在此基础上深入分析其性能,探讨其平均情况下卓越效率的原因。具体内容包括:
通过代码的实现与性能分析,我们将更加深刻地体会快速排序的巧妙之处。敬请期待,我们下一篇实战篇再见! 互动与分享
感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!