1. 什么是冒泡排序?

Bubble Sort is the simplest sorting algorithm that works by repeatedly swapping the adjacent elements if they are in wrong order.

冒泡排序是最简单的排序算法,它可以通过重复交换相邻元素来进行排序。

什么是冒泡排序,定义自己网上找,就是不断的跟冒泡似的向上去而已。 都说什么比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。 但一写代码就不对,单纯这样两句话Code写是写不出来的。

比如一般人可能就这样写了。 写完之后还觉得没毛病。→ 我自己 ✌('ω'✌ )

def error_bubble_sort(arr):
    for i in range(len(arr) - 1):
        if arr[i] > arr[i+1]:
            arr[i], arr[i+1] = arr[i+1], arr[i]
    return arr

arr = [8, 7, 6, 4, 1]
print(error_bubble_sort(arr))

# result
# [7, 6, 4, 1, 8]

这样的结果就是只排序了1个,就是最后1个。 所以下面直接上正确代码。市面上AI给初学者最常见的写法。

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if (arr[j] > arr[j + 1]):
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

# 测试一下
data = [64, 34, 25, 12, 22, 11, 90]
sorted_data = bubble_sort(data)
print(f"排序后的数组: {sorted_data}")

请把上面的代码,一步步的,按照推演方法在纸上自己写一遍具体步骤。

然后再继续向下看,不需要你写出来代码,只需要拿出纸和笔在草稿纸上写完完整的步骤。包括每一步,n是多少,i是多少,j是多少,位置交换完之后是多少?

2. 代码难点

如果你推演一遍之后,继续就开始理解下面的难点。其实冒泡的难点就是围绕着一个问题展开的

有几个循环?到底要循环多少次?这个是你在网上看到 n 种写法的万恶之源

外循环

首先你要知道分为外循环和内循环?

外循环控制比较多少轮?

内循环控制才是真正的比较大小。

先说外循环,外循环总共比较数组长度 - 1次。

当你通过冒泡确定了 n - 1 个大元素的位置时(即它们都已经排在数组末尾了),最后剩下的那一个数(最小的那个)必然已经在正确的第一位了。换句话说,如果你有 5 个苹果要按大小排序,你选出了 4 个最大的放在右边,那么剩下的那个一定是最小的,不需要再跑第 5 轮循环去确认。

# 第一次外循环
for i in range(n): # 网上这种写法很多,但其实是多余比较了最后一次
# 第一次外循环
for i in range(n - 1): # 由于比较这种东西 剩下一个肯定是要比较的 所以这样写才是对的

总结一下就是你比较 数组长度回 还是 数组长度-1 回 结果都是一样的,因为倒数第二次比较的时候已经成型了。所以如果你比较长度回就是多余的比较。于是你可以看到网上很多写法都是-1的操作。

所以下面的几个写法,在第一次 for 的时候都是可以成立的,他们的区别只在于起始值和结束不同而已。下面假设长度是 5。 下面这一段用的是java,这样可以看的清晰点。

for(int i = 0; i < array.length - 1; i++) → 本质就是数组长度-1 起始值 0,1,2,3

for(int i = 1; i < array.length; i++) → 本质就是数组长度-1 起始值 1,2,3,4

for(int i = 0; i + 1 < array.length; i++) → 本质就是数组长度-1 起始值 0,1,2,3

for(int i = 1; i <= array.length - 1; i++) → 本质就是数组长度-1 起始值 1,2,3,4

反正无论怎么开始,怎么结束,都是比了4次。

这其实是编程中常见的 边界处理 优化。虽然在现代计算机上,多跑这一次O(1)循环几乎感知不到性能差异,但在算法逻辑的严谨性上,n - 1 才是最完美的解法。

内循环

这个因为有一个变量是需要跟外循环进行一起看的,所以有时候初学者会很懵逼。

每回要比较次数 = 去掉已经排序好的 + 去掉自身。为什么要去掉自身?因为自身是对比的对象,自己当然要去掉。

这个一定要用纸笔自己写一次。 可以模拟一下就是这样

假设数组 {5,3,6,2,7,1,9} size:7
i j j < n - i - 1
0 0 0 < 7-0-1=6
1
2
3
4
5
6
1 0 0 < 7-1-1=5
1
2
3
4
5
2 0 0 < 7-2-1=4
1
2
3
4
3 0 0 < 7-3-1=3
1
2
3
4 0 0<7-4-1=2
1
2
5 0 0<7-5-1=1
1
6 0 0<7-6-1=0

j的起始点为什么是0

因为你每一次遍历完对比之后,最大的那一个都会确定,也就是都放在最后了。所以你每一次都从第一个开始比较啊。

// before
{5, 2, 7, 3, 1, 6, 4, 2, 4, 4, 55, 8, 7, 2};
// 第一次比较 55 其实是有序的了。还是要从头开始
{5, 2, 7, 3, 1, 6, 4, 2, 4, 4, 2, 8, 7,| 55};

3. 优化1 Early Exit ➡️ 只要记这个

现在写一个小小的优化的思路。(冒泡本来效率就低,所以优化与否意义不太大? 现在我们都知道冒泡是拿着1个数字然后左右循环对比直到最后1次然后进行排列。 如果我们在第一次进行和左右对比的时候这全程都没有交换的话,那么还要继续下去吗?

[1,2,3,4,5]
# 我们拿出来1,和后面的对比的时候发现从来没有交换过,那么2还要看吗?
# 肯定是不要的。那么怎么知道有没有交换呢,用一个count来计数就可以完成。
def bubble_sort_optimized(arr):
    n = len(arr)
    for i in range(n - 1):
      	# 每一次外循环的时候 写在里面
      	# 不要写在外面代码 每一次内循环之前要重置一下
        swapped = False
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        	# 如果没有交换 直接就跳出循环 说明整个数组已经是排序好的
        	# 要注意这个判断要写在第1个for里面 相当于第i次没有交换 整个程序目前都要break
          # 如果你写在了里面的for里面,你只能跳过里面的for
        if not swapped:
            break
    return arr

初学者的代码难点主要在swapped = Falseif not swapped:写在哪里。

要注意就是写在第一层的for那里。因为只有在内循环的时候没有任何循环,你才可以判定这一次的外循环也可以结束了。整体没有什么要循环的了,我们已经排序好了。

补充知识

我想知道在py里if is_swapped is False: 和 if not is_swapped : 哪个写法比较好

在 Python 里这两种写法都会得到相同的结果,但社区通常更推荐后一种,更加「Pythonic」。

两种写法比较

语句 含义 风格评价
if is_swapped is False: 明确比较布尔值 冗长,不推荐
if not is_swapped: 利用真假值 简洁、自然、符合惯例 ✔️

为什么 if not … 更好?

当然可以,我们来更细致地看这两个方面 👇

可读性(Readability)

  • 自然语言式逻辑 if not is_swapped: 读起来像“如果没有交换过”,贴近人类思考方式。 而 if is_swapped is False: 则显得像在做“布尔比较”,多了一层解释。
  • 视觉负担 多余的关键字(is False)打断了流畅性,特别是在复杂的条件表达式里,读者需要多一步理解“到底在比较什么?”
  • 社区惯例 PEP 8 中有类似建议:“不要与布尔值做等值比较…使用 if cond:if not cond:”。遵循惯例让代码对所有 Python 开发者都更友好。

减少错误(Avoiding bugs)

  • 真假值的多样性 Python 中除了 False,还有 0""[]None 等也被判定为假。如果变量意外地不是布尔值,if not is_swapped: 仍会正确触发,而 if is_swapped is False: 只有在变量恰好是 False 布尔对象时才成立。
  • 身份运算符的陷阱 is 检查的是对象身份,不是值相等。用在布尔上有时会出现奇怪的行为,尤其在涉及到自定义类返回布尔结果时。
  • 未来维护 当代码被他人或未来的自己阅读、修改时,简单的 not 形式更直观,降低理解错误导致的回归风险。

总结if not … 既清楚又健壮,对布尔和假值行为都能正确处理。 显式比较 is False 只有在你确实需要判断“这个变量必须是字面 False”时才有价值,否则更容易引入误判。

写 Python 时,让代码像“写给人看的”通常能避免意外,not 正是这类范式的典型例子。

4. 优化2 Last Exchange Index

这个理解起来蛮困难的。先直接上代码。

def bubble_sort_smart_boundary(arr):
    n = len(arr)
    last_exchange_index = n - 1
    while last_exchange_index > 0:
        boundary = last_exchange_index
        last_exchange_index = 0 # 假设本轮没有交换 不写这个的话 while永远loop
        for j in range(boundary):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                last_exchange_index = j # 更新最后一次交换的位置
    return arr

Smart Boundary其实非常聪明,它不仅能发现数组是否有序,还能精准判断数组后面有多少位已经排好了

我们用一个稍微有点“乱”但尾部有序的数组作为例子:arr = [3, 4, 1, 5, 6]

初始状态

  • n = 5
  • last_exchange_index = 4 (数组最后一个下标)

第一轮:while 第一次循环

此时 boundary = 4,我们要检查 j03

  1. j = 0: 比较 34。 $3 < 4$,不交换。
  2. j = 1: 比较 41。 $4 > 1$,交换!数组变为 [3, 1, 4, 5, 6]
    • 关键点:last_exchange_index 更新为 1
  3. j = 2: 比较 45。 $4 < 5$,不交换。
  4. j = 3: 比较 56。 $5 < 6$,不交换。

第一轮结束结果

  • 数组变为 [3, 1, 4, 5, 6]
  • last_exchange_index 停留在 1
  • 深层含义:算法发现,下标 1 之后的所有元素(4, 5, 6)在这一轮都没动过。这意味着下标 1 之后的所有东西已经全部归位了

第二轮:while 第二次循环

此时 boundary = 1(由上一轮的 last_exchange_index 赋值而来)。

这意味着 for j in range(1),内层循环只会运行一次

  1. j = 0: 比较 31。 $3 > 1$,交换!数组变为 [1, 3, 4, 5, 6]
    • 关键点:last_exchange_index 更新为 0

第二轮结束结果

  • 数组变为 [1, 3, 4, 5, 6]
  • last_exchange_index0

结束:while 第三次尝试

  • while last_exchange_index > 0 判定为 False
  • 跳出循环,返回结果。

抽丝剥茧:为什么它比普通版强?

普通冒泡排序在处理 [3, 4, 1, 5, 6] 时,即使后面 5, 6 已经是排好的,它依然会死板地执行以下步骤:

  • 第一轮:比较到最后(4次比较)。
  • 第二轮:比较到倒数第二位(3次比较)。
  • 第三轮:比较到倒数第三位(2次比较)。

而你的Smart Boundary版本:

  1. 跳跃扫描:第一轮跑完,它敏锐地发现 last_exchange_index1,说明后面那一大截(4, 5, 6)全是好的。
  2. 大幅缩减:第二轮它直接把搜索范围从 4 砍到了 1
  3. 提前收工:总共只跑了两轮 while 就确定全都有序了。

一句话总结

普通冒泡是“按部就班”地缩减范围,而 Smart Boundary 是“哪里最后动过,我就检查到哪里”。

这里代码上要注意到就是 while last_exchange_index > 0: 这一句话的理解,当你最后一次交换是0的时候,说明全部结束了,所以while这里的条件就是大于0。

5. 优化3 Cocktail Shaker Sort

冒泡排序通常只从左往右。鸡尾酒排序(双向冒泡)则是左右来回摆动。其实本质还是对撞指针下的相邻比较,只是每一次都确定一个最大,一个最小。这样开始对撞。

def cocktail_sort(arr):
    n = len(arr)
    low, high = 0, n - 1
    while low < high:
        # 从左向右冒泡(找最大值)
        for j in range(low, high):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
        # 这里记得要写在全部排序找到最后在--,我第一次写在if里面了
        high -= 1 #找最大值 找到之后--

        # 从右向左冒泡(找最小值)
        for j in range(high, low, -1):
            if arr[j - 1] > arr[j]:
                arr[j - 1], arr[j] = arr[j], arr[j - 1]
        low += 1
    return arr

这里有一个困扰我的难点,就是 while (low < high)可以不可以写成 while (low <= high)。答案就是是可以的,如果 low=right 的时候,说明指向同一个元素,这时候即使你执行 while 里面的循环,你发现里面的两个 for 都不会执行。因为不符合条件,但是会执行 high--和 low++。但是即使执行了对结果依然没有任何影响。此时的 left 和 right 无论是变大还是变小已经没用了,排序也结束了。

但是如果你使用 while (low < high)会比 while (low <= high)少一次比较,代码效率会少一丢丢而已。

🔔 直接看提示词

如果你觉得上面的你都没看懂,或者太长不看。可以直接到这里。复制一下提示词。

什么是冒泡排序?请给我一个最常见的实现。
冒泡排序有什么优化写法?请浅显易懂抽丝剥茧的给我讲述一下优化写法。
xxxx这个写法能否有具体的例子给我一步步的讲解一下,包括代码的难点。
我想知道这个代码为什么不可以是xxxx