编码 · 算法
冒泡排序详解:原理、代码与时间复杂度
简短回答
冒泡排序是一种简单的排序算法:反复遍历列表,交换每一对顺序错误的相邻元素。每一趟之后,剩余元素中最大的那个会移动(像气泡一样浮起)到末尾的最终位置。它在平均和最坏情况下需要 O(n²) 时间,对已经有序的列表是 O(n),使用 O(1) 额外空间,并且是稳定的。它主要用于教学;实际程序使用内置的排序函数。
打开行排序通俗地说,思路是什么
比较前两个元素,如果第一个更大就交换。然后比较第二个和第三个,再比较第三个和第四个,依此类推直到列表末尾。这就是一趟。第一趟结束后,最大的元素一定在最后,因为每次比较都把它向右带了一步。
对未排序的部分重复这一趟,每次未排序部分缩短一个元素。如果某一趟没有发生任何交换,说明列表已经有序,可以提前停止。
逐趟演示
将 [5, 1, 4, 2, 8] 按升序排序。竖线之后的元素已在最终位置。
| 趟数 | 比较与交换 | 该趟之后的列表 |
|---|---|---|
| 1 | 5↔1 交换,5↔4 交换,5↔2 交换,5 与 8 保持不变 | [1, 4, 2, 5, 8] (8 已就位) |
| 2 | 1 与 4 保持,4↔2 交换,4 与 5 保持 | [1, 2, 4, 5, 8] (5 已就位) |
| 3 | 1 与 2 保持,2 与 4 保持,没有交换 | [1, 2, 4, 5, 8] 已有序,停止 |
如果没有提前退出,算法还会再跑第四趟;有了提前退出,这里三趟就够了。
Python 实现
def bubble_sort(items):
"""Sort a list in place, ascending. Returns the same list."""
n = len(items)
for end in range(n - 1, 0, -1): # last index of the unsorted part
swapped = False
for i in range(end):
if items[i] > items[i + 1]: # strict >, keeps equal items in order
items[i], items[i + 1] = items[i + 1], items[i]
swapped = True
if not swapped: # no swaps: already sorted
break
return items
print(bubble_sort([5, 1, 4, 2, 8])) # [1, 2, 4, 5, 8]
print(bubble_sort([3, 3, 1])) # [1, 3, 3]
print(bubble_sort([])) # []内层循环在 end 处停止,因为它之后的元素都已就位。swapped 标志使有序输入下达到 O(n) 的最好情况。
时间与空间复杂度
| 情况 | 比较次数 | 时间 | 原因 |
|---|---|---|---|
| 最好(已有序) | n − 1 | O(n) | 一趟没有交换,然后停止 |
| 平均(随机顺序) | 约 n²/2 | O(n²) | 平均约 n²/4 次交换 |
| 最坏(逆序) | n(n − 1)/2 | O(n²) | 每次比较都要交换 |
| 额外空间 | O(1) | 原地排序,只用几个变量 |
对于逆序的 10,000 个元素,这将近有 5000 万次比较和交换。交换次数始终等于逆序对的数量,所谓逆序对,就是顺序颠倒的元素对。
冒泡排序与其他排序算法的比较
| 算法 | 最好 | 平均 | 最坏 | 额外空间 | 稳定 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 否 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 否 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 |
Timsort(Python 的 sorted) | O(n) | O(n log n) | O(n log n) | O(n) | 是 |
冒泡排序是稳定的,因为它只交换相邻元素,并且只在严格逆序时才交换,所以相等的元素永远不会越过彼此。插入排序的复杂度和稳定性与它相同,但在实践中写入次数更少、速度更快,因此像 Timsort 这样的混合算法内部用的是插入排序,而不是冒泡排序。
变体与常见错误
- 鸡尾酒排序在交替的趟中先从左到右、再从右到左,能更快地把末尾的小元素(俗称「乌龟」)移到前面。它仍然是 O(n²)。
- 梳排序比较相隔一定间距的元素,并逐步缩小间距,能提早消除大量「乌龟」;间距为 1 时就退化成冒泡排序。
- 奇偶排序先比较所有奇数对,再比较所有偶数对,常用于讲解并行排序。
何时使用冒泡排序
在生产代码中几乎不用。在 Python 中,请调用 sorted(items) 或 items.sort(),它们使用 Timsort,时间 O(n log n),并且是稳定的。冒泡排序仍然值得了解:它是最容易讲清楚的排序,正确性证明也很好懂,而且是考察循环、交换和复杂度时常见的入门面试题或考试题。
它唯一的实际优势是只需一趟 O(n) 就能检测出列表已有序,但只比较相邻元素的普通循环无需排序也能做到这一点。
常见问题
冒泡排序稳定吗?
稳定。它只在左边元素严格更大时才交换相邻元素,所以相等的元素保持原有顺序。
冒泡排序的时间复杂度是多少?
平均和最坏情况下是 O(n²)。如果某一趟没有交换就提前退出,已有序输入下的最好情况是 O(n)。
为什么叫冒泡排序?
大的元素每次一个交换地向列表末尾移动,就像气泡浮向水面。第 k 趟之后,最大的 k 个元素已就位。
冒泡排序比插入排序快吗?
不快。两者都是 O(n²),但插入排序通常操作更少,实际运行更快,对接近有序的数据尤其如此。
冒泡排序会用额外内存吗?
不会。它原地排序,只需要 O(1) 的额外空间来存放循环下标和交换时的临时值。