【数据结构】排序算法精讲 | 快速排序全解:分治思想、核心步骤与示例演示

快速排序

  • 导读
  • 一、基本思想
  • 二、排序过程
    • [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
  • 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快速排序算法。我们将不仅实现代码,还会在此基础上深入分析其性能,探讨其平均情况下卓越效率的原因。具体内容包括:

  • 详细解析如何用代码控制左右指针的精准移动。
  • 深入探讨元素交换的具体实现与边界条件处理。
  • 分析算法的时间与空间复杂度,理解其高效背后的原理。
  • 讨论递归调用的优化以避免栈溢出风险。

通过代码的实现与性能分析,我们将更加深刻地体会快速排序的巧妙之处。敬请期待,我们下一篇实战篇再见!

互动与分享

  • 点赞👍 - 您的认可是我持续创作的最大动力

  • 收藏⭐ - 方便随时回顾这些重要的基础概念

  • 转发↗️ - 分享给更多可能需要的朋友

  • 评论💬 - 欢迎留下您的宝贵意见或想讨论的话题

感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!

相关推荐
程序员小白条2 小时前
提前实习的好处有哪些?有坏处吗?
java·开发语言·数据结构·数据库·链表
七夜zippoe2 小时前
Python高级数据结构深度解析:从collections模块到内存优化实战
开发语言·数据结构·python·collections·内存视图
iconball2 小时前
个人用云计算学习笔记 --29 华为云网络云服务
运维·笔记·学习·华为云·云计算
YGGP2 小时前
【Golang】LeetCode 55. 跳跃游戏
算法·leetcode
YJlio3 小时前
Contig 学习笔记(13.4):单文件碎片整理工具的原理与基本用法
笔记·学习·stable diffusion
练习时长一年4 小时前
Leetcode热题100(跳跃游戏 II)
算法·leetcode·游戏
小白菜又菜9 小时前
Leetcode 3432. Count Partitions with Even Sum Difference
算法·leetcode
wuhen_n10 小时前
LeetCode -- 15. 三数之和(中等)
前端·javascript·算法·leetcode
sin_hielo10 小时前
leetcode 2483
数据结构·算法·leetcode