起泡排序
看完你能手推每轮比较、说清两处改进为何有效并复述反例
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
起泡排序
看完你能手推每轮比较、说清两处改进为何有效并复述反例
核心构思:相邻交换
烧水时大气泡总比小气泡先冲出水面——轻的、位置合适的最先冒头。起泡排序借了这个画面:让每一轮扫描把最大的元素一路「顶」到末尾,一轮一个,逐个就位。
气泡逐级上浮对应大元素经相邻交换逐位后移
一趟扫描的完整过程
跟随数组 [5,3,8,2] 走完一趟扫描,看最大值如何沉底。
多轮扫描与整体流程
竖向流程图:每一轮扫描后无序前缀缩短一格,看 n-1 轮如何把气泡全部顶到右端。
朴素版实现
从完整双重循环代码出发,逐行对照前面推演的扫描过程,落实每一趟到底做了什么。
外层 i 控制趟数,内层 n-1-i 正是让右侧已排好的最大值不再参与比较,逐趟缩短扫描区间。
观察:提前有序的浪费
上一页写好了朴素版:外层固定跑 n-1 轮,内层每轮扫一遍。但如果某一轮里一次交换都没发生呢?说明数组已经有序了,后面几轮却还在白跑——这就是朴素版最大的浪费。
雨已停(已有序)却按原计划撑完全程伞,白费力
加有序标志的改进版
用 sorted 标志检测「一趟无交换」,让已有序的数组提前收工,避免空跑。
每趟开始假设有序,一旦交换就推翻假设;若整趟未推翻,说明零交换即全局有序,break 提前结束。
改进版的执行对比
同一输入,两条执行路径:左侧朴素版扫满轮次,右侧改进版遇有序即停,看出省掉的轮次。
构造反例的思路
上一页我们看到:只要某一轮一次交换都没发生,改进版就提前收工。那它到底还怕什么?反过来想:什么样的输入能让每一轮都被迫交换?找到它,就找到了改进版的死穴。
龟速元素每次交换只前进一格,标志永远亮着
反例的执行轨迹
沿箭头逐轮追踪:每轮都有交换,标志位每轮都被置真,从未提前终止。
再改进的关键观察
上一页的反例里,后半段早就排好了,标志位却还在白白扫描。换个角度想:一趟中最后一次交换发生的位置,意味着它之后再也没有元素需要换——那其后是不是天然有序?
最后插队的人身后队伍必然已站好,无需再检查
记录最后交换位置
最后一次交换之后的部分必然已有序,用 last 记下它,下一趟直接从那里截断扫描。
last 是本趟最靠后的交换下标:它右边的元素两两都比过且未换,天然有序,把 n 收缩到 last 即跳过整段冗余扫描。
跳跃版对反例的效果
用上一页的反例,看记录最后交换位置后,扫描区间如何一步步坍缩。
三个版本横向对比
加了标志后最好情况变 O(n),但有人误以为最坏也变了。逐维对齐,看清两版差别到底在哪。
- 全程无交换即可提前终止
- 最好 O(n):仅一趟扫描
- 最坏仍 O(n²):逆序时无用
- 一趟后尾部整体成有序区
- 最好 O(n):一趟即判有序
- 最坏 O(n²) 不变,但每趟边界更快收缩
自测:标志位与边界
起泡排序同时使用有序标志和最后交换位置两种优化,它们分别带来什么效果?
要点收束
- ✓有序性由相邻交换逐趟向右扩张
- ✓标志位省掉无交换的多余整趟
- ✓最后交换位置让边界一次跳到位
- ✓改进都源于利用扫描中的免费信息
课后思考
三个问题别急着看答案,先自己想十分钟,再对照参考思路。
参考答案标志位版只压缩右边界,左边的小元素(俗称「乌龟」)每轮只能挪一步。双向扫描能让乌龟一轮就往前跳一大段,小元素下沉快,整体轮数随之减少。
参考答案朴素版仍要 n-1 轮;标志位版一趟就停;跳跃版因最后交换位置极小,边界立即收窄。差距说明改进的收益取决于「无序区域的位置」,不只是「是否有序」。
参考答案核心的相邻比较交换不变,但链表不支持随机访问,跳跃版记录的「最后交换位置」仍可用,问题在于回溯到该位置再开始新一趟需要额外指针维护,边界维护成本上升。