这次我们来聊一个 Python 基础但面试经常被问到的排序问题:用冒泡排序实现列表的升序排列。内容不涉及第三方库,不依赖 CUDA,不挑显卡,只要有 Python 解释器就能跑。对于刚学 Python 的朋友来说,冒泡排序是理解循环、列表索引和元素交换最好的入门案例之一;对于正在准备笔试、面试的朋友来说,手写一个可运行的冒泡排序,并且能讲清楚时间复杂度和优化思路,也是基本要求。
先说这篇文章要解决的问题:给一个包含数字的 Python 列表,比如[64, 34, 25, 12, 22, 11, 90],通过冒泡排序算法,把元素从小到大排列好。文章会从冒泡排序的原理讲起,给出基础版、优化版、支持自定义排序规则等几种写法,然后带你完成功能测试、性能观察和常见错误排查。读完你不仅能写出正确的冒泡排序,还能知道什么时候该自己写,什么时候直接调sorted()更合适。
文章适合这几类读者:刚开始学 Python 列表操作的人;正在复习数据结构与算法的人;想让代码从“能运行”变成“更规范、更可复用”的人。如果你只是想快速把一个列表排成升序,可以只看第 5 节的内置方法对比;如果你想彻底搞懂冒泡排序,建议从头开始看。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 目标 | 使用 Python 手写冒泡排序,实现列表升序排列 |
| 输入 | Pythonlist,元素通常为数字或可比较对象 |
| 输出 | 原地排序后的原列表,从小到大排列 |
| 内置排序对比 | 可使用list.sort()或sorted()一行完成升序 |
| 是否依赖第三方库 | 否,仅使用 Python 标准库 |
| 算法时间复杂度 | 平均和最坏情况 O(n²) |
| 算法空间复杂度 | O(1),原地排序,只使用常数级别额外空间 |
| 稳定性 | 稳定排序,相等元素相对顺序不变 |
| 是否支持降序 | 可以,修改比较符号或增加reverse参数 |
| 是否支持复杂对象 | 可以通过key参数或自定义比较逻辑扩展 |
| 适合场景 | 教学、面试、小规模列表排序 |
| 不适合场景 | 大数据量排序,建议使用内置 Timsort |
这张表需要特别说明一点:冒泡排序的 O(n²) 复杂度是针对比较和交换次数说的。如果列表长度从 1000 变成 10000,运行时间可能增加约 100 倍。所以真实业务里的海量数据排序,几乎不会手写冒泡排序,这也是文章后面会用较大篇幅讲“什么时候不要自己写”的原因。
2. 适用场景与使用边界
冒泡排序的适用场景非常明确:小数据量、教学演示、面试手写算法。
先说适合谁。对初学 Python 的人来说,冒泡排序是一个非常典型的双重循环结构:外层控制排序的趟数,内层控制每一趟中相邻元素的比较次数。通过这个案例,你可以把range、列表索引、条件判断、元素交换这几个知识点串起来。对准备算法面试的人来说,冒泡排序虽然简单,但考官可能会让你现场写一个“提前终止的优化版”,考察你对循环边界和标志位的理解。
再说不适合什么。如果你的列表里有几万、几十万个元素,或者你的程序需要频繁排序,用冒泡排序会明显变慢。Python 内置的list.sort()和sorted()底层是基于 Timsort 实现的,平均时间复杂度是 O(n log n),而且经过大量优化,远远优于手写冒泡排序。不要因为学会了冒泡排序,就在生产代码里强行用它处理大数据。
还有一个边界需要提醒:Python 的列表不要求元素类型完全一致,但冒泡排序通过>或<比较元素大小。如果列表中混入无法比较的类型,比如数字和字符串放在一起,运行时会出现TypeError。这在学习和测试阶段需要特别注意,别把“列表能装不同类型”理解成“排序时可以随便混装”。
3. 环境准备与前置条件
冒泡排序不需要任何第三方库,所以环境准备很简单。一个能运行 Python 的环境就够。
3.1 确认 Python 版本
打开终端或命令行,输入以下命令,确认 Python 环境可用:
python --version如果你安装了多个 Python 版本,可能需要使用python3:
python3 --version从输出可以看到当前版本。本文的代码在 Python 3 环境下都可以运行,建议至少使用 Python 3.6 以上的版本,因为代码中会用到 f-string 等语法时会更方便。需要说明的是,不同 Python 小版本之间表现基本一致,实际运行请以你本机环境为准。
3.2 选择一个运行方式
你可以用以下几种方式执行文章里的代码:
- 直接在 Python 交互式解释器里逐行输入;
- 把代码保存成
.py文件,比如bubble_sort.py,再用python bubble_sort.py运行; - 在 VSCode、PyCharm、Jupyter Notebook 等编辑器里运行。
新手比较推荐第二种方式。把代码保存在文件里,方便修改和重复运行。
3.3 准备测试列表
为了演示效果,可以先准备几个有代表性的列表:
nums = [64, 34, 25, 12, 22, 11, 90] empty_list = [] single_list = [42] duplicate_list = [5, 3, 8, 5, 2, 5]后续所有代码都可以围绕这些用例测试。验证排序是否正确时,最好包含空列表、单元素列表、重复元素列表和乱序列表,这样能更全面地发现问题。
4. 冒泡排序原理与基础实现
4.1 冒泡排序原理
冒泡排序的核心思想一句话就能概括:每一轮从头到尾比较相邻元素,如果前面的元素比后面的大,就交换它们。这样每一轮结束后,当前未排序部分的最大值就会像气泡一样“浮”到最后面。
举个例子。假设列表是[5, 1, 4, 2, 8],第一轮比较过程如下:
- 比较 5 和 1,5 比 1 大,交换,变成
[1, 5, 4, 2, 8]; - 比较当前相邻的 5 和 4,5 比 4 大,交换,变成
[1, 4, 5, 2, 8]; - 比较 5 和 2,交换,变成
[1, 4, 2, 5, 8]; - 比较 5 和 8,5 小于 8,不交换。
第一轮结束后,列表最右边的 8 已经是最大值。第二轮只需要比较前 4 个元素,也就是[1, 4, 2, 5]的部分。每一轮都会把当前范围内最大的元素送到右端,因此已经到位的元素就不再参与后续比较。
4.2 基础版代码实现
根据上面的原理,可以写出第一个版本:
def bubble_sort(arr): n = len(arr) # 外层循环控制排序趟数 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] return arr这段代码里有几个关键细节:
- 外层
range(n - 1):n 个元素最多需要 n-1 趟排序。 - 内层
range(n - 1 - i):每完成一趟,末尾就会多一个已经排好的最大值,所以下一轮可以减少一次比较。 - 交换操作
arr[j], arr[j + 1] = arr[j + 1], arr[j]是 Python 特有的元组解包写法,它等价于其他语言中借助临时变量的交换。这样写更简洁,也不会产生多余变量。
4.3 运行与预期结果
把代码保存成bubble_sort_demo.py,内容如下:
def bubble_sort(arr): n = len(arr) 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] return arr if __name__ == "__main__": nums = [64, 34, 25, 12, 22, 11, 90] print("排序前:", nums) bubble_sort(nums) print("排序后:", nums)运行结果如下:
排序前: [64, 34, 25, 12, 22, 11, 90] 排序后: [11, 12, 22, 25, 34, 64, 90]注意,bubble_sort(nums)会直接修改传入的列表。看到排序后列表变成升序,就说明基础版已经跑通。
4.4 原地排序与原列表被修改
Python 的列表是可变对象,函数内通过arr修改元素,会直接影响外部传入的列表。这既是优点也是风险。
如果你希望调用函数后保留原列表不变,可以先复制一份,再对副本排序:
nums = [64, 34, 25, 12, 22, 11, 90] sorted_nums = bubble_sort(nums[:]) print("原列表:", nums) print("新列表:", sorted_nums)这里nums[:]创建了原列表的浅拷贝,排序发生在副本上。对于元素是不可变对象的列表来说,这种拷贝方式足够安全。如果列表里的元素本身是可变对象,排序时不会修改这些对象的内部状态,通常也安全;但如果元素是字典并想按字段排序,则需要更复杂的处理方式。
5. 冒泡排序优化与 Python 内置方法对比
基础版冒泡排序的缺点很明显:即使列表已经是升序排列,它仍然会执行完所有比较。针对这一点,可以加入提前终止机制。
5.1 优化版:提前终止已经有序的列表
思路是:如果某一轮比较中一次交换都没有发生,说明列表已经有序,可以直接跳出外层循环。
def bubble_sort_optimized(arr): n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break return arr这个版本对基本有序的列表效果很好。例如输入[1, 2, 3, 4, 5],第一轮从头到尾比较一遍,没有发生交换,立刻结束。基础版仍然需要跑完 n-1 轮,而优化版的比较次数显著减少。面试时如果能写出这个优化版本,通常会被认为理解了冒泡排序的本质。
5.2 优化版:记录最后一次交换位置
另一个优化思路是记录每轮最后一次发生交换的位置。该位置之后的元素已经有序,下一轮比较不需要再访问它们。
def bubble_sort_last_swap(arr): n = len(arr) last_swap_index = n - 1 while last_swap_index > 0: border = last_swap_index last_swap_index = 0 for j in range(border): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] last_swap_index = j return arr这个版本比只加标志位的版本更精细。实际效果受数据分布影响,但思路本身值得掌握。你不一定在项目里用到它,但遇到“手写优化版冒泡排序”的面试题时,能多给出一个方案。
5.3 使用内置方法一行完成升序
回到标题本身:任务是“列表升序排列”。站在工程角度,Python 已经提供了最直接的工具:
nums = [64, 34, 25, 12, 22, 11, 90] nums.sort() print(nums)也可以用sorted():
nums = [64, 34, 25, 12, 22, 11, 90] sorted_nums = sorted(nums) print(sorted_nums)两者都能实现升序。它们的区别很关键:
| 方法 | 是否修改原列表 | 返回值 |
|---|---|---|
list.sort() | 原地修改 | 返回None |
sorted(list) | 不修改原列表 | 返回新的排序列表 |
内置排序是生产环境的首选。底层是 Timsort,针对现实中常见的“部分有序”数据做了优化,效率和稳定性都远超手写冒泡排序。那为什么还要学冒泡排序?因为面试和算法入门仍然需要它。更重要的是,理解冒泡排序能帮你理解“稳定排序”“原地排序”“时间复杂度”这些概念,这些概念在阅读 Python 官方文档和第三方库源码时经常出现。
6. 封装成通用排序函数
基础函数只能对数字列表升序排列,实际使用时还不够灵活。我们可以把它封装成支持降序、支持按 key 排序的通用函数。
6.1 支持升序和降序
增加一个reverse参数,默认False表示升序,传入True表示降序:
def bubble_sort(arr, reverse=False): n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): left = arr[j] right = arr[j + 1] need_swap = left > right if not reverse else left < right if need_swap: arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break return arr测试:
nums = [64, 34, 25, 12, 22, 11, 90] print(bubble_sort(nums[:], reverse=True)) # 预期输出:[90, 64, 34, 25, 22, 12, 11]6.2 支持按 key 字段排序
列表里的元素不一定是简单数字,也可能是字典、对象、元组。如果要让学生对象按成绩排序,可以传入key函数。实现时要注意:不能每比较一次就调用一次key,否则效率低且难以保持一致。正确做法是预先计算所有排序键,再根据键的对比结果交换原列表元素。
def bubble_sort_by(arr, key=None, reverse=False): key_func = (lambda x: x) if key is None else key n = len(arr) keys = [key_func(item) for item in arr] for i in range(n - 1): swapped = False for j in range(n - 1 - i): left_key = keys[j] right_key = keys[j + 1] need_swap = left_key > right_key if not reverse else left_key < right_key if need_swap: arr[j], arr[j + 1] = arr[j + 1], arr[j] keys[j], keys[j + 1] = keys[j + 1], keys[j] swapped = True if not swapped: break return arr测试代码:
students = [ {"name": "小明", "score": 88}, {"name": "小红", "score": 95}, {"name": "小刚", "score": 72}, ] bubble_sort_by(students, key=lambda s: s["score"]) for student in students: print(student)输出顺序会是小刚、小明、小红。这里用到了“预计算排序键”的做法,它能减少重复调用key带来的开销,也是内置排序中key参数的一种简化模拟。
6.3 批量排序多个列表
如果有一批列表需要统一处理,可以写一个for循环批量调用:
lists_to_sort = [ [3, 1, 2], [9, 5, 7], [4, 4, 2, 9], ] for index, data in enumerate(lists_to_sort, start=1): bubble_sort(data) print(f"列表{index}: {data}")输出如下:
列表1: [1, 2, 3] 列表2: [5, 7, 9] 列表3: [2, 4, 4, 9]如果你的批量任务很多,建议在每个列表排序前先复制,避免外部的原始数据被意外修改。批量处理时,可以继续沿用for循环,也可以使用列表推导式,但要注意列表推导式不适合有副作用的排序函数,还是显式循环更清晰。
7. 功能测试与效果验证
写完排序函数,下一步是验证它是否真的正确。冒泡排序的测试重点包括:空列表、单元素、重复元素、已升序列表、已降序列表、混合负数和浮点数,以及排序稳定性。
7.1 使用断言快速验证
在开发阶段,最直接的验证方式是使用assert断言:
from bubble_sort import bubble_sort def test_bubble_sort(): assert bubble_sort([64, 34, 25, 12, 22, 11, 90]) == [11, 12, 22, 25, 34, 64, 90] assert bubble_sort([]) == [] assert bubble_sort([42]) == [42] assert bubble_sort([5, 3, 8, 5, 2]) == [2, 3, 5, 5, 8] assert bubble_sort([5, 4, 3, 2, 1]) == [1, 2, 3, 4, 5] assert bubble_sort([-3, 1.5, 0, -2, 2]) == [-3, -2, 0, 1.5, 2] print("所有测试用例通过") if __name__ == "__main__": test_bubble_sort()这段代码直接输出了判断结果。所有断言没有报错,说明函数在基础场景下表现正确。
7.2 使用 unittest 做自动化验证
当你要长期维护这个函数时,建议把测试代码写成标准库unittest:
import unittest from bubble_sort import bubble_sort class TestBubbleSort(unittest.TestCase): def test_sorted_asc(self): nums = [64, 34, 25, 12, 22, 11, 90] bubble_sort(nums) self.assertEqual(nums, [11, 12, 22, 25, 34, 64, 90]) def test_empty_list(self): nums = [] bubble_sort(nums) self.assertEqual(nums, []) def test_single_element(self): nums = [1] bubble_sort(nums) self.assertEqual(nums, [1]) def test_duplicate_elements(self): nums = [5, 3, 8, 5, 2] bubble_sort(nums) self.assertEqual(nums, [2, 3, 5, 5, 8]) def test_reverse_sorted(self): nums = [5, 4, 3, 2, 1] bubble_sort(nums) self.assertEqual(nums, [1, 2, 3, 4, 5]) def test_mixed_numbers(self): nums = [1.5, -3, 0, 2, -2.5] bubble_sort(nums) self.assertEqual(nums, [-3, -2.5, 0, 1.5, 2]) if __name__ == "__main__": unittest.main()运行方式:
python -m unittest test_bubble_sort.py如果所有用例通过,你会看到类似下面的输出:
...... ---------------------------------------------------------------------- Ran 6 tests in 0.001s OK7.3 稳定性验证
冒泡排序是稳定排序,意味着相等的元素不会交换顺序。验证时可以在元组中加入原始序号:
items = [(3, "a"), (1, "b"), (3, "c"), (2, "d")] bubble_sort(items) print(items)输出中两个数字为 3 的元组,应该保持原来的先后顺序,即(3, "a")依然在(3, "c")前面。这就是冒泡排序稳定性的直观验证。
7.4 判断预期结果的标准
验证排序函数是否成功,可以看几个标准:
- 排序后列表长度不变。
- 对任意相邻位置
i,都有arr[i] <= arr[i + 1]。 - 排序后列表中的元素集合与原列表完全一致,不发生元素丢失。
- 如果原列表复制后做排序,原列表内容不应被修改。
写一个验证函数,可以代替一部分单元测试:
def is_sorted(arr): return all(arr[i] <= arr[i + 1] for i in range(len(arr) - 1))测试大量随机数据时,这个函数非常实用。它可以配合random.shuffle一遍遍验证算法正确性。
8. 性能观察与资源占用
冒泡排序最大的缺点是慢。下面具体分析它的时间消耗和资源占用方式。
8.1 时间复杂度与空间复杂度
对于一个长度为 n 的列表:
- 外层循环最多执行 n-1 次;
- 内层循环每一轮最多比较 n-1-i 次;
- 总比较次数大约是
(n-1) + (n-2) + ... + 1 = n(n-1)/2; - 因此时间复杂度是 O(n²)。
最好情况是列表已经完全有序,且使用优化版冒泡排序,此时第一轮只比较 n-1 次就结束,时间复杂度降低到 O(n)。空间复杂度则始终是 O(1),因为它只用了变量i、j、swapped等常数空间,且交换操作不需要额外数组。
8.2 使用 timeit 观察不同规模耗时
实际运行时间需要在本机测试,这里提供一个通用的测量脚本:
import random import timeit def bubble_sort(arr): n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break for size in [100, 1000, 5000]: data = list(range(size)) random.shuffle(data) elapsed = timeit.timeit(lambda: bubble_sort(data[:]), number=1) print(f"列表大小 {size}: {elapsed:.4f} 秒")随着size增大,运行时间会快速上升。在多数电脑上,一万个随机整数的排序也会明显卡顿。如果你使用基础版而不是优化版,即使输入是有序数据,它依然会执行大量无意义比较,这一点可以用timeit对比验证。
8.3 影响冒泡排序速度的关键因素
影响速度的因素主要有以下几类:
| 因素 | 影响 |
|---|---|
| 列表长度 n | 时间复杂度 O(n²),长度翻倍,耗时约翻 4 倍 |
| 数据是否基本有序 | 优化版可以提前退出,明显提速 |
| 元素比较开销 | 如果元素是复杂对象,比较成本更高 |
| Python 解释器本身 | 相比编译型语言,Python 手写循环天然较慢 |
key函数是否被重复调用 | 预计算 key 比每轮都调用 key 更快 |
如果遇到数据量很大却必须使用冒泡排序的场景,可以尝试减少比较范围、使用标志位提前退出、在外层循环开始时检查是否有序。但这些只是治标不治本,更稳妥的做法是改用内置排序。
9. 常见问题与排查方法
手写冒泡排序时,最容易遇到下面这些问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 排序后列表并没有变有序 | 内层循环范围用错,导致部分相邻元素没有比较 | 打印每一轮列表观察过程 | 内层使用range(n - 1 - i) |
索引越界IndexError | 内层循环写成range(n),最后访问arr[j + 1]越界 | 查看完整报错堆栈 | 保证j + 1 < n - i |
| 排序结果是降序 | 比较符号使用反了 | 检查if条件 | 升序使用arr[j] > arr[j + 1] |
| 列表没有被修改 | 函数内部重新给arr赋值或使用了切片副本 | 打印传入对象的id和排序后列表 | 使用arr[j]原地修改,不要执行arr = ... |
包含不同类型时报TypeError: '>' not supported | 列表元素类型不统一,无法比较 | 检查列表元素类型 | 先统一类型,或使用key指定比较字段 |
| 数字和字符串混排时排序随意报错 | Python 3 不允许隐式跨类型比较 | 打印元素类型 | 更换输入数据或自定义转换函数 |
函数返回None | 没有写return arr | 检查函数末尾 | 按要求返回原列表或新列表 |
| 基本有序时仍然很慢 | 没有使用提前终止优化 | 增加swapped标志 | 某一轮无交换就break |
| 排序结果不正确但代码不报错 | 外层循环次数错误,少了一轮 | 使用随机数据反复验证 | 外层range(n - 1),n 为元素个数 |
| 排序后原列表被修改,影响其他业务 | 函数原地排序,调用方可能不希望修改 | 确认调用场景 | 调用前使用data[:]传入副本 |
另一个常见误解是list.sort()返回None。很多初学者会写成:
nums = [5, 2, 3, 1] result = nums.sort() print(result) # 输出 None,而不是排序后的列表如果希望打印排序结果,要么打印原变量nums,要么使用sorted(nums)。这个坑与手写冒泡排序无关,但在 Python 排序相关代码中非常常见,建议留意。
10. 最佳实践与使用建议
10.1 学习阶段多写多验证
学习冒泡排序时,建议不要只看代码,而是动手写一个“打印每一轮排序过程”的调试版本,帮助自己建立直观认识:
def bubble_sort_with_trace(arr): n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True print(f"第 {i + 1} 轮: {arr}") if not swapped: break return arr bubble_sort_with_trace([5, 1, 4, 2, 8])输出如下:
第 1 轮: [1, 4, 2, 5, 8] 第 2 轮: [1, 2, 4, 5, 8] 第 3 轮: [1, 2, 4, 5, 8]这里第三轮没有发生交换,所以提前结束。看到这个过程后,你会更清楚为什么需要swapped标志。
10.2 生产环境优先使用内置排序
日常开发中,如果只是想对列表升序排列,请记住两条规则:
- 要修改原列表,用
list.sort(); - 要保留原列表,生成新列表,用
sorted()。
示例:
nums = [64, 34, 25, 12, 22, 11, 90] nums.sort() # nums 变成升序 sorted_nums = sorted(nums) # nums 不变,sorted_nums 是升序如果要对字典列表按某个字段升序,内置函数更简单:
students = sorted(students, key=lambda s: s["score"])10.3 将算法封装成可复用模块
如果确实要在项目中使用手写排序函数,建议把它放在独立模块中,例如sorting.py,并提供清晰的文档字符串:
def bubble_sort(arr, reverse=False): """对列表进行原地冒泡排序。 Args: arr: 可比较元素组成的列表。 reverse: 为 False 时升序,为 True 时降序。 Returns: 排序后的原列表。 """这样写既方便测试,也方便后续替换成更快的排序算法。单元测试尽量覆盖边界条件,不要只测一个正常用例。
10.4 批量任务要明确数据边界
如果你在写批量排序任务,建议先确定输入数据的规模和类型。如果每个列表都不大,循环调用bubble_sort是可行的;如果列表总量很大,推荐改为调用内置排序,并且使用多线程或多进程时要注意列表属于可变对象,避免并发修改同一份数据。排序前后的数据校验最好也要做,防止输入源本身包含脏数据。
11. 写在最后
冒泡排序的代码量不大,但它把“循环、比较、交换、边界处理、算法复杂度”这些概念都串在了一起。实现列表升序排列,本质上是让每两个相邻元素符合前一个 <= 后一个的关系。只要这个关系成立,排序就是正确的。
最容易踩的坑有三个:一是内层循环边界写错,导致索引越界或漏比较;二是忘掉函数会原地修改传入列表;三是把冒泡排序用于不该它处理的大数据场景。理解了这三点,冒泡排序基本就不会出大问题。
下一步你可以试着把代码改成降序,或者给列表里的字典按某个字段排序,再写一组单元测试验证正确性。练习完之后,再去对比一下 Python 内置的sorted()源码文档和 Timsort 的原理,你就能更清楚:为什么实际项目里首选内置排序,为什么算法基础仍然值得学。把这篇文章里的代码保存成自己的工具模块,面试前拿出来翻一翻,比临时背答案有用得多。