嘿,咱们别整那些虚头巴脑的“起初其次最终”。真正搞懂了冒泡排序,就得把那些教科书上写得像个机器人一样的定义给抛开,咱们就当成两个要么三个刚搬完装备、有点扛不住重的伙计,在一条长线上把东西往后挪。 想象一下,你手里拿着一队排成一排的士兵,从 A 到 Z。你目前的任务是让他们从最终面排到最前面。
这时候你就得搞个两脚台阶,左边那个台阶叫“已排序区”,右边那个叫“没排序区”。刚启动的时候,这队人别看站得整规整齐,但哪位也不知道哪位是哪位,位置是乱的。你的任务就是站在右边那堆人旁边,挑一个没站好队的家伙,看看他前面有没有比他矮(大)的要么比他高的(小)的人。 这就好比你在比哪位嗓门大。你走到这帮人前面,要是是“大嗓门”的,他得往后退一步,不中的话,持续往前找那个比他嗓门还大要么嗓门还小的,直到找到比他小要么比他大的人。
要是真找到了,他就得换位置,站在你手边那堆人旁边。 走到哪一步,看哪位动了几次步,就把哪位标记为“彻底归位了”。
这一步做完之后,这帮人实际上已经排好队了,前面的人肯定都比后面的人排得靠前,要么说,刚刚那一轮下来,大家都从右边挪到了左边。 这时候,你就对右边这堆人喊一声“歇歇吧”,把刚刚那一轮跑过来的那些“小个子”都给淘汰出队,他们去右边那堆人旁边站好,重新排个序。 要是这帮人确实排好了,那这一轮就没必要再动次了,直接跳过。 要是有一帮人还没动完,还不能直接跳过,说明这一轮下来,最前面还有一群“大嗓门”要么“小个子”没把队排正,那这一轮还得持续,把没动过的再往后推。 这就跟洗盘子一样。你先把手碰到最脏的那块,使劲搓,洗到发亮,然后松开手,擦擦手,接着洗手。
要是这双手擦到一半发现还有脏的,那就得持续搓,直到这双手碰不到脏的。搓得发亮的,就把它擦干净利落;搓不发的,就不用管它了。
最终,你手里拿着的最干净利落的那双手,就是刚刚那一轮洗出来的成果,你能够把它扔进洗碗机沥干了,然后从最脏的那块启动,再洗一遍,要么干脆就不洗了。 咱们拿个具体数据来唠唠,这样你脑子里的模型就活真了。 假设你有一组数字:[35, 15, 10, 25, 20, 22, 18]。咱们规定,比哪位大哪位就得往后走,比哪位小哪位就得往前走,这叫冒泡。
那这组数实际上就是一队人,目前哪位在哪位后面呢? 35 在最前面,他是老大,没人能拿他当后茬;15 在第二,他是老二,老大在第二,老大没跟老二比,故此老二站得稳;10 在第三,他是老三,老大在第三,老二在第二,老大比老二大,故此老大得往后退,老二往前挤。 第一轮启动。 先看 35 和 15。35 比 15 大,35 得往前。但 35 本来就是老大,动不了,要不就哪位比他还大。
故此 35 不动,它还是老大。 再看 15 和 10。15 比 10 大,15 得往后。但 15 就是老二,老二的对手是哪位?要是是老大 35,那老大比 15 大,15 就得往前,但 35 在右边,路不通。
要是是后面的人,比如 10,10 比 15 小,10 就得往前,但 15 在左边,没法往前冲。
故此 15 和 10 哪位也没地方去,动不了。 这里有个细节,15 比 10 大,按理说 15 应当往后,但 10 在 15 后面,15 把 10 挤向前进,结局 15 反而到了 10 的后面,这是典型的“原地踏步”。 15 和 10 哪位也没变化,这俩就真成了“死对头”,哪位也不动。 接下来看 10 和 25。10 比 25 小,10 得往前。10 前面是 15,15 比 10 大,10 应当往后,但 15 在左边,路不通。
故此 10 也动不了。 再看 25 和 20。25 比 20 大,25 得往前。25 前面是 10,10 比 25 小,25 应当往后,但 10 在左边,也路不通。 25 和 20 哪位也没地方去,25 动了也没用,出于它前面的人比它小,它要去后面找比它大的,但后面的人都比它小。
故此 25 和 20 也成“死对头”了。 最终看 20 和 22。20 比 22 小,20 得往前。20 前面是 25,25 比 20 大,20 应当往后,但 25 在左边,路不通。 20 和 22 哪位也没地方去。 这时候,你看右边这堆人(25, 20, 22),哪位都没动。左边的人(35, 15, 10)也没动。 等一下,我是不是漏了哪位? 让我重新梳理一下顺序,从第 1 个人启动,依次和后面的人比。 数字序列:35, 15, 10, 25, 20, 22, 18 第 1 轮: 35 vs 15: 35<15? No. 35 不动。 15 vs 10: 15>10? Yes. 15 后移(变成 15, 10, 25, ...)。(这是关键一步,15 往右挤) 10 vs 25: 10<25? Yes. 10 前移?不对,10 在 25 前面。10 < 25,故此 10 往前?不对,规则是大的往后。10 比 25 小,10 应当往前。但 10 前面是 15,15 比 10 大,10 被 15 挡住去不了。 25 vs 20: 25>20? Yes. 25 往后。 20 vs 22: 20<22? Yes. 20 往前。 22 vs 18: 22>18? Yes. 22 往后。 看来我的理解还是有点乱。还是拿那个最直观的例子,一步步推。 原数组:[35, 15, 10, 25, 20, 22, 18] 初始化:排序区 [0,0] (35),未排序区 [0,1,2,3,4,5,6] -> 实际未排序区是 15 到 18。 第一轮: 从左边找最大,要么右边找最小?标准冒泡一般是找最大往右挤,要么最小往左挤,这里按“找最大往右挤”解释比较顺。 遍历 i 从 0 到 len-2: 比较 nums[i] 和 nums[i+1]。
要是 nums[i] < nums[i+1],说明 i 已经排好序了,不需求动,跳过。 要是 nums[i] > nums[i+1],说明 i 没排好,需求换。 第一轮执行: i=0, 35 vs 15.35>15。换。栈上:[35, 15, 10, 25, 20, 22, 18]。
这里 35 到了 15 后面?不对,35 比 15 大,35 应当往后。
原来 35 在 0 位置,15 在 1 位置。35>15,故此 35 应当往后退? 啊,我搞反了逻辑。
那会儿是“大的往后压”,目前要是是升序排列,大的应当排在后面。
那大的应当往右挤。 重新来: 数组:[35, 15, 10, 25, 20, 22, 18] 目标:升序。大的在右。 i=0: 35 和 15。35>15。35 应当往后。但 35 在左边,15 在右边。35 比 15 大,35 应当挤到 15 的右边去。 换 (0,1) -> [15, 35, 10, 25, 20, 22, 18]。 目前 35 在 1 位置,15 在 0 位置。15 排好了。 接下来看 35 和 10。35>10。35 应当往后。 换 (1,2) -> [15, 10, 35, 25, 20, 22, 18]。 目前 35 在 2 位置。10 在 1 位置。10 排好了吗?10 左边是 15,10<15,故此 10 应当往前。但 10 是第 2 个人,左边是第 1 个人,10 本来就在第 1 个位置了?不对,原数组 10 在第 3 个位置。换后,10 到了第 2 个位置,15 到了第 1 个位置。 逻辑有点绕,还是用大白话: 手里拿着一叠牌,35, 15, 10, 25, 20, 22, 18。 拿 35 和 15 比,35 比 15 大,35 要往后站,让 15 往前站。 35 往前站不了,就换,15 跑到 35 前面去?不对,15 应当比 35 靠前。 要是是升序,小的在前。
故此小的应当往前站。 35 和 15 比,35 大,35 往后站。 出于 35 在左边,15 在右边,换后,15 在左,35 在右。15 排好了。 接下来看 15 和 10。15 大,15 往后站。 15 在位置 1,10 在位置 3。15 往后站,换 15 和 10。 目前 10 在 1 位置,15 在 3 位置。 接下来看 10 和 25。10 小,10 往前站。 10 在 1 位置,前面是边界,不用往前站了。 25 和 20。25 大,25 往后站。 25 在 3 位置,20 在 4 位置。25 往后站,没路了,就换。 20 在 3 位置,25 在 4 位置。20 小,20 往前站。 20 在 3 位置,前面是 10,10<20,20 往前站不了。 22 和 18。22 大,22 往后站。 22 在 5 位置,18 在 6 位置。22 往后站,换。 18 在 5 位置,18<22,18 往前站。 这一轮下来,数据变成了:[15, 10, 25, 20, 22, 18, 35] ???不对,35 最终还在 6 位置吗? 最终一步:22(5) 和 18(6)。22>18。换。数组变成 [15, 10, 25, 20, 22, 18, 35]。 这时候 35 在右边最尾。15, 10, 25, 20, 22, 18 这六个数里,哪位都排好了吗? 15 在 0,前面无。 10 在 1,前面是 15? 15>10。10 应当往前。10 前面是 15,15 比 10 大,10 被挡住,10 没法往前排。 25 在 2,前面是 10,10<25。25 应当往前。25 前面是 10,10 比 25 小,25 往前站。换! 数组变成 [15, 10, 25, 20, 22, 18, 35] -> (2,1) 换 -> [15, 10, 20, 22, 18, 35, 25]。 这忒乱了。还是用最好办的逻辑: 冒泡排序就是重复的“冒泡”过程。 每一趟,找出最大(或最小)的元素,把它放到对的位置,然后重复这个过程,直到遍历整个个数组。 出于它把最大的元素“冒”到了最终,故此叫冒泡排序。 好的,咱们持续。 假设数组是 [5, 2, 4, 1, 3]。 第一趟: 5 和 2。5>2。换。[2, 5, 4, 1, 3]。5 到了 1 位置。 5 和 4。5>4。换。[2, 4, 5, 1, 3]。5 到了 2 位置。 5 和 1。5>1。换。[2, 4, 1, 5, 3]。5 到了 3 位置。 5 和 3。5>3。换。[2, 4, 1, 3, 5]。5 到了 4 位置。 第一趟终止。5 排到了最终。剩下的 [2, 4, 1, 3] 需求再排。 第二趟: 2 和 4。2<4。
不换。 4 和 1。4>1。换。[2, 1, 4, 3, 5]。 4 和 3。4>3。换。[2, 1, 3, 4, 5]。 第二趟终止。1 排到了第二位置(别看位置不对,但它是这里最小的)。剩下的 [2, 1, 3, 4] 需求再排。 第三趟: 2 和 1。2>1。换。[1, 2, 3, 4, 5]。 第二趟终止。2 排到了第一位置。剩下的 [1, 2, 3, 4] 不需求再排了,出于 1 已经是最终了(要么第一个了)。 循环终止。 最终结局:[1, 2, 3, 4, 5]。 你看,这个过程就像你在刷剧。你刷到了第 27 集,认定凑合,就停一停。
然后下一集,你发现第 27 集比第 28 集还烂,你重新刷。 要是你发现第 27 集和第 28 集差不多,你也不刷了。 最终,当你把第 28 集刷到最终,发现也没比第 27 集差,你也停一停。
这时候,你手里所有刷过的,都是好的,剩下的没刷的,都是烂的。 这时候你就把手里好的那一堆整理好,扔掉烂的那堆。 每一轮下来,你手里最好的那一块就变好了。 要是这一轮没有变化,说明这一堆里已经没有烂的了,直接跳过,持续下一轮。 一直重复下去,直到整个文件都是好的。 这就叫冒泡排序。 它不是啥魔法,就是一个傻乎乎的家伙,一遍遍地把坏家伙往后踢,直到踢出队,剩下好家伙挤在一起。踢完了再踢下一批。 你看到的数据变化,实际上就是数据在移动,不是被转变了。 要是数据没动,说明这一批人里,没有那个“坏家伙”能翻到“好家伙”前面去,那就直接终止。 要是数据动了,说明有那个坏家伙确实能翻那会儿,那它就在那边等着,下一批人那会儿的时候,它再参与一次游戏。 就是如此个道理。 你看代码,也就如此写。 下面这行代码: for i in range(n - 1): for j in range(n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] 这段代码,就是那个傻乎乎的家伙在