(e)起泡排序

官官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·38 天前
排序算法图解优化边界

起泡排序

看完你能手推每轮比较、说清两处改进为何有效并复述反例

按 空格/→ 演示下一步

问一句
1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

排序算法图解优化边界

起泡排序

看完你能手推每轮比较、说清两处改进为何有效并复述反例

1第 1 页 · 起泡排序

核心构思:相邻交换

烧水时大气泡总比小气泡先冲出水面——轻的、位置合适的最先冒头。起泡排序借了这个画面:让每一轮扫描把最大的元素一路「顶」到末尾,一轮一个,逐个就位。

相邻比较
只比较位置相邻的两个元素,不从全序列里挑最大
逆序则交换
前一个比后一个大就换位置,两两解决顺序问题
大元素沉底
每轮扫描结束,当前未排序部分的最大值到达末尾
扫描范围收缩
已就位的尾部不再参与,每轮比较次数减一
水泡上浮对应 →起泡排序一轮扫描

气泡逐级上浮对应大元素经相邻交换逐位后移

第 i 轮比较 n−i 次,总比较 n(n−1)2\text{第 } i \text{ 轮比较 } n-i \text{ 次,总比较 } \frac{n(n-1)}{2}
2第 2 页 · 核心构思:相邻交换

一趟扫描的完整过程

跟随数组 [5,3,8,2] 走完一趟扫描,看最大值如何沉底。

1
比较首对
比较 5 和 3:5>3 逆序,交换得 [3,5,8,2]
2
比较次对
比较 5 和 8:已有序,不交换,指针右移
3
比较末对
比较 8 和 2:逆序,交换得 [3,5,2,8]
4
最大值就位
8 沉到末位并锁定,下一趟不再碰它
3第 3 页 · 一趟扫描的完整过程

多轮扫描与整体流程

竖向流程图:每一轮扫描后无序前缀缩短一格,看 n-1 轮如何把气泡全部顶到右端。

图解渲染中…
rx:第3到第n-1轮:每轮只比较到无序前缀末尾,右侧已归位区不再碰s1:无序前缀:右侧是已排好的最大元素,左侧待排部分每轮缩1
4第 4 页 · 多轮扫描与整体流程

朴素版实现

python

从完整双重循环代码出发,逐行对照前面推演的扫描过程,落实每一趟到底做了什么。

代码高亮加载中…

外层 i 控制趟数,内层 n-1-i 正是让右侧已排好的最大值不再参与比较,逐趟缩短扫描区间。

5第 5 页 · 朴素版实现

观察:提前有序的浪费

上一页写好了朴素版:外层固定跑 n-1 轮,内层每轮扫一遍。但如果某一轮里一次交换都没发生呢?说明数组已经有序了,后面几轮却还在白跑——这就是朴素版最大的浪费。

固定轮次的盲区
朴素版无条件跑满 n-1 轮,不看数据实际状态
无交换 = 已有序
某一轮冒泡一趟内没发生任何交换,说明序列已排好
提前有序的场景
如 [1,2,3,5,4],只需一轮交换即完成,剩余轮次全浪费
观察引出改进
把「本轮是否交换」记下来,可作为提前终止的信号
雨停还撑伞对应 →多余轮次

雨已停(已有序)却按原计划撑完全程伞,白费力

6第 6 页 · 观察:提前有序的浪费

加有序标志的改进版

python

用 sorted 标志检测「一趟无交换」,让已有序的数组提前收工,避免空跑。

代码高亮加载中…

每趟开始假设有序,一旦交换就推翻假设;若整趟未推翻,说明零交换即全局有序,break 提前结束。

7第 7 页 · 加有序标志的改进版

改进版的执行对比

同一输入,两条执行路径:左侧朴素版扫满轮次,右侧改进版遇有序即停,看出省掉的轮次。

图解渲染中…
c3:菱形判断:本轮一次交换都没发生,说明已整体有序c7:改进版提前终止点,朴素版没有这个出口b6:朴素版固定扫 n-1 轮,不管数据是否已有序
8第 8 页 · 改进版的执行对比

构造反例的思路

上一页我们看到:只要某一轮一次交换都没发生,改进版就提前收工。那它到底还怕什么?反过来想:什么样的输入能让每一轮都被迫交换?找到它,就找到了改进版的死穴。

盯住交换条件
改进版只问「本轮是否交换过」,交换多 = 无提前退出机会
构造逆序输入
如 5 4 3 2 1:最大值每次扫描只向前挪一步
每轮都有交换
逆序时一趟只把一个元素归位,标志始终为真
每次只准挪一步对应 →逆序起泡

龟速元素每次交换只前进一格,标志永远亮着

9第 9 页 · 构造反例的思路

反例的执行轨迹

沿箭头逐轮追踪:每轮都有交换,标志位每轮都被置真,从未提前终止。

图解渲染中…
a1:严格逆序输入,最长路径反例b1:每轮至少交换一次,标志置真g1:标志始终为真,等于没加优化
10第 10 页 · 反例的执行轨迹

再改进的关键观察

上一页的反例里,后半段早就排好了,标志位却还在白白扫描。换个角度想:一趟中最后一次交换发生的位置,意味着它之后再也没有元素需要换——那其后是不是天然有序?

关键观察
一趟中最后一次交换之后,其后的元素必然整体有序
为什么成立
之后两两比较都没发生交换,说明相邻关系全部正确
如何利用
记住最后一次交换位置,下轮扫描只到那里为止
效果
有序尾巴不再重复扫描,反例场景一次即完成
排队的最后插队者对应 →最后一次交换位置

最后插队的人身后队伍必然已站好,无需再检查

11第 11 页 · 再改进的关键观察

记录最后交换位置

python

最后一次交换之后的部分必然已有序,用 last 记下它,下一趟直接从那里截断扫描。

代码高亮加载中…

last 是本趟最靠后的交换下标:它右边的元素两两都比过且未换,天然有序,把 n 收缩到 last 即跳过整段冗余扫描。

12第 12 页 · 记录最后交换位置

跳跃版对反例的效果

用上一页的反例,看记录最后交换位置后,扫描区间如何一步步坍缩。

图解渲染中…
c1:k为最后一次交换位置,其后已确认有序e1:不再固定减1,而是直接跳到kf1:相比标志版,少扫整段已有序部分
13第 13 页 · 跳跃版对反例的效果

三个版本横向对比

加了标志后最好情况变 O(n),但有人误以为最坏也变了。逐维对齐,看清两版差别到底在哪。

改进版:有序标志
  • 全程无交换即可提前终止
  • 最好 O(n):仅一趟扫描
  • 最坏仍 O(n²):逆序时无用
跳跃版:记末次位置
  • 一趟后尾部整体成有序区
  • 最好 O(n):一趟即判有序
  • 最坏 O(n²) 不变,但每趟边界更快收缩
数据接近有序或尾部已有序时选跳跃版;纯随机或逆序数据两者差异不大,标志版实现更简单。
14第 14 页 · 三个版本横向对比

自测:标志位与边界

点击作答

起泡排序同时使用有序标志和最后交换位置两种优化,它们分别带来什么效果?

15第 15 页 · 自测:标志位与边界

要点收束

  • ✓有序性由相邻交换逐趟向右扩张
  • ✓标志位省掉无交换的多余整趟
  • ✓最后交换位置让边界一次跳到位
  • ✓改进都源于利用扫描中的免费信息
延伸主题:鸡尾酒排序双向扫描插入排序的稳定性对比O(n log n) 排序的必要性
16第 16 页 · 要点收束

课后思考

三个问题别急着看答案,先自己想十分钟,再对照参考思路。

1「鸡尾酒排序」每轮交替从两头扫描,它解决了标志位版没解决的什么问题?为什么有效?

参考答案标志位版只压缩右边界,左边的小元素(俗称「乌龟」)每轮只能挪一步。双向扫描能让乌龟一轮就往前跳一大段,小元素下沉快,整体轮数随之减少。

2如果数据接近有序但只有一对相邻元素逆序,三个版本各自要扫几轮?差距说明了什么?

参考答案朴素版仍要 n-1 轮;标志位版一趟就停;跳跃版因最后交换位置极小,边界立即收窄。差距说明改进的收益取决于「无序区域的位置」,不只是「是否有序」。

3把「相邻交换、大者右移」推广到链表结构,起泡排序还成立吗?哪个版本的边界记录会失效?

参考答案核心的相邻比较交换不变,但链表不支持随机访问,跳跃版记录的「最后交换位置」仍可用,问题在于回溯到该位置再开始新一趟需要额外指针维护,边界维护成本上升。

17第 17 页 · 课后思考