之前在给初学者讲 Python 列表操作时,几乎每次都会遇到同一个问题:给了一组杂乱的数据,怎么用代码把它按从大到小或从小到大排好?很多人第一反应是直接调用sorted()或list.sort(),这个答案没错,但如果你还没弄明白排序背后到底发生了什么,一旦面试官追问“不用内置函数怎么实现”,就会卡壳。冒泡排序作为最经典的入门排序算法,正好能帮你补齐这块短板。本文会从零开始拆解冒泡排序的实现思路,再用 Python 逐步写出对列表进行升序排列的完整代码,并针对常见报错、优化方案和工程习惯给出建议。内容适合刚学 Python 的初学者,也适合想复习算法基础、准备面试的开发同学。
1. 冒泡排序到底是什么
1.1 一个生活化的例子
想象一下,你面前有一排高低不同的人,老师要求按身高从矮到高站好。最笨但有效的办法是:从队伍最左边开始,依次比较相邻两个人的身高,如果左边的人比右边的人高,就让他们交换位置。这样走完一趟后,最高的人就像气泡一样“浮”到了队伍最右边。接着再从头开始,重复同样的过程,只不过这次不需要再管最右边那个已经排好的人。等到再也没有相邻位置需要交换时,整支队伍就排好了。
这个“相邻比较、不符合顺序就交换”的过程,就是冒泡排序的核心逻辑。因为大的元素会像水里的气泡一样逐步向上浮动,所以叫“冒泡排序”。
1.2 冒泡排序的专业定义
冒泡排序(Bubble Sort)是一种基于比较和交换的稳定排序算法。它重复地遍历待排序的列表,一次比较两个相邻元素,如果它们的顺序错误就交换过来。遍历列表的工作会重复多轮,直到没有任何一对相邻元素需要交换为止。
用更严谨的话描述:
- 输入:一个包含
n个元素的列表。 - 操作:进行多轮相邻元素比较,按升序要求将较大的元素向右移动。
- 输出:完成升序排列的新列表或原列表。
- 时间复杂度:最坏和平均情况为 O(n²),最好情况(列表已经有序)优化后可达到 O(n)。
- 空间复杂度:O(1),因为只需要一个临时变量用于交换,是原地排序算法。
1.3 为什么初学者要掌握冒泡排序
很多同学会觉得,明明有现成的sort()方法,为什么还要学冒泡排序?主要有三点原因:
- 理解排序的底层逻辑。直接调用 API 很容易,但排序算法的比较、交换、循环边界才是编程基本功。
- 培养循环思维。冒泡排序使用双层循环,内层循环负责一趟比较,外层循环控制趟数,非常适合锻炼对
for循环和while循环的理解。 - 面试和考试常考。无论是 Python 二级、数据结构课还是技术面试,手写冒泡排序都是高频考点。
另外,冒泡排序的实现代码短小,非常适合用来分析一个算法的执行过程。学会它之后,你再接触选择排序、插入排序、快速排序时,会有更清晰的方向感。
2. 环境准备与 Python 基础回顾
2.1 Python 环境说明
本文示例代码基于 Python 3,推荐使用 3.8 及以上版本。如果你还不知道如何安装 Python,可以在 Python 官网下载对应系统的安装包,安装时务必勾选“Add Python to PATH”。安装完成后,打开终端或命令行输入:
python --version如果能输出类似Python 3.10.11的版本信息,说明环境已经就绪。本文所有代码都使用 Python 自带的标准库和内置函数,不需要额外安装第三方包。
2.2 列表基础操作回顾
冒泡排序的操作对象是列表,所以先快速回顾几个列表的基础操作。
创建一个列表:
numbers = [5, 2, 9, 1, 7] print(numbers)访问和修改元素:
numbers[0] = 99 print(numbers[0])获取列表长度:
print(len(numbers))列表的切片操作在观察排序过程时会用到:
sub = numbers[1:3] # 获取索引1到索引2的元素,不包含索引3 print(sub)这些操作虽然简单,但都是后续代码的基础。如果你对列表切片和索引还不够熟悉,可以先用下面的小例子熟悉一下:
arr = [10, 20, 30, 40, 50] print(arr[0]) # 10 print(arr[-1]) # 50,负索引从末尾开始数 print(arr[1:4]) # [20, 30, 40]2.3 交换两个变量的值
冒泡排序中频繁用到“交换元素”。在 Python 里,交换两个变量可以写成:
a = 3 b = 5 a, b = b, a print(a, b) # 输出 5 3这种写法利用 Python 的元组解包特性,不需要中间变量。但在讲解算法原理时,为了更通用,有时会使用一个临时变量temp来完成交换:
a = 3 b = 5 temp = a a = b b = temp print(a, b) # 输出 5 3很多其他语言不支持直接交换,所以理解临时变量方式能帮你更好地阅读跨语言代码。在 Python 实战中,推荐直接使用a, b = b, a,代码更简洁。
2.4 如何观察排序过程
写算法时最容易出现“代码跑完发现结果不对,但不知道错在哪”。我的建议是:在关键位置加print(),把每一轮排序后的列表打印出来,用肉眼观察数据变化。后面章节会专门演示这种做法。
3. 冒泡排序核心原理拆解
3.1 基本思想
以升序排列为例,冒泡排序的基本思想是:
- 每一轮从左到右依次比较相邻的两个元素。
- 如果左侧元素大于右侧元素,就交换它们。
- 每一轮结束后,当前范围内最大的元素会移动到最右侧。
- 下一轮比较时,可以忽略已经排好的右侧区域,缩小比较范围。
重复上述过程,直到所有元素都处于正确位置。
3.2 一趟排序做了什么
假设有一个列表[5, 1, 4, 2, 8],我们只执行一趟完整的相邻比较,来看会发生什么。
初始状态:
[5, 1, 4, 2, 8]第一步:比较索引 0 和索引 1 的元素,即 5 和 1。因为 5 > 1,交换:
[1, 5, 4, 2, 8]第二步:比较索引 1 和索引 2 的元素,即 5 和 4。因为 5 > 4,交换:
[1, 4, 5, 2, 8]第三步:比较索引 2 和索引 3 的元素,即 5 和 2。因为 5 > 2,交换:
[1, 4, 2, 5, 8]第四步:比较索引 3 和索引 4 的元素,即 5 和 8。因为 5 < 8,不需要交换。
一轮结束后,列表变成了[1, 4, 2, 5, 8],最大值 8 已经移动到了最后。这一趟操作也验证了一个规律:每一趟结束,至少能让当前未排序区间的最大值归位。
3.3 多趟排序为什么能完成整体排序
既然一趟只能让一个最大值归位,那包含 n 个元素的列表最多需要 n-1 趟。因为当 n-1 个元素都排好时,剩下那个元素自然也在正确位置。
继续基于上面的列表,第二趟比较范围可以排除最后一个元素。过程中 5 会逐步浮动到倒数第二个位置;第三趟时 4 会归位;第四趟时 2 和 1 也完成排序。最终得到:
[1, 2, 4, 5, 8]这里容易有一个误区:是不是必须执行正好 n-1 趟?不一定。如果列表中某个元素已经有序,而且某一轮全程没有任何交换,说明列表已经有序,可以提前结束。优化时通常利用这一点。
3.4 关键代码模板
冒泡排序最经典的嵌套循环结构如下:
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]外层循环i控制第几趟,内层循环j控制这一趟比较哪些相邻位置。n - 1 - i是因为每一趟结束后,都会有一个元素在末尾固定下来,不需要再参与下一趟比较。
4. Python 实现列表升序排列的完整实战
这一节会从基础版逐步优化,写出完整、可直接运行的 Python 冒泡排序代码。
4.1 基础版:无优化冒泡排序
先实现一个最“老实”的版本:固定执行n - 1趟,每趟都遍历到未排序区间的末尾。
# bubble_sort_basic.py def bubble_sort_basic(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] if __name__ == "__main__": numbers = [64, 34, 25, 12, 22, 11, 90] bubble_sort_basic(numbers) print("排序结果:", numbers)运行输出:
排序结果: [11, 12, 22, 25, 34, 64, 90]这个版本是最容易理解的,但没有考虑“提前结束”的可能。如果列表已经有序,它仍然会傻乎乎地执行完所有循环。
4.2 优化一:减少不必要的比较轮数
细心观察会发现,列表长度是 n 时,最多只需要 n-1 趟。即使列表原本已经有序,基础版也会把 n-1 趟全部执行完。为了降低最好情况下的时间开销,可以增加一个标记变量swapped:
# bubble_sort_optimized.py 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这个优化的关键点在于:如果内层循环一轮遍历下来,没有任何相邻元素需要交换,那整个列表已经处于有序状态,不必再继续剩余循环。
例如列表[1, 2, 3, 4, 5],第一轮扫描时所有相邻元素都满足arr[j] <= arr[j+1],swapped保持为False,循环直接退出,时间复杂度退化为 O(n)。
4.3 优化二:记录最后一次交换位置
还有一个更细致的优化:每一轮内层循环的结束位置不一定要按n - 1 - i来定,可以记录最后一轮发生交换的位置,因为该位置之后的元素在上一轮已经排好序,下一轮无需再比较。
# bubble_sort_last_swap.py def bubble_sort_last_swap(arr): """记录最后一次交换位置的冒泡排序优化版""" n = len(arr) end = n - 1 while end > 0: last_swap = 0 for j in range(end): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] last_swap = j # 更新最后一次交换的位置 end = last_swap # 下次比较只需进行到这里这个优化思路理解起来稍难,但在面对大量数据时能减少不少无效比较。初学者可以先把前两种版本写熟练,再慢慢消化这一版。
4.4 封装成函数
实际项目中不建议把排序逻辑直接写在主流程里,更推荐封装成函数。函数除了接收待排列表,还可以增加一个参数控制升序还是降序:
def bubble_sort(arr, reverse=False): """ 冒泡排序实现 :param arr: 待排序列表 :param reverse: False 表示升序,True 表示降序 """ n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): # 升序时左边大于右边需要交换;降序时左边小于右边需要交换 if (not reverse and arr[j] > arr[j + 1]) or (reverse and arr[j] < arr[j + 1]): arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break调用方式:
my_list = [3, 1, 4, 1, 5, 9, 2, 6] bubble_sort(my_list) print(my_list) # [1, 1, 2, 3, 4, 5, 6, 9] my_list2 = [3, 1, 4, 1, 5, 9, 2, 6] bubble_sort(my_list2, reverse=True) print(my_list2) # [9, 6, 5, 4, 3, 2, 1, 1]这种封装方式对调用方更友好,后续修改算法细节时也不会影响外部调用逻辑。
4.5 使用示例与输出
下面给一个完整的演示脚本,包含随机数生成和排序结果打印:
# demo.py import random 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 if __name__ == "__main__": random.seed(42) data = [random.randint(1, 100) for _ in range(10)] print("原始列表:", data) bubble_sort(data) print("排序后为:", data)5. 运行、验证与代码可视化
5.1 在命令行运行脚本
把上面的demo.py保存到本地后,在终端执行:
python demo.py预期输出类似:
原始列表: [82, 15, 4, 95, 36, 32, 29, 18, 95, 14] 排序后为: [4, 14, 15, 18, 29, 32, 36, 82, 95, 95]这里的随机数序列是seed(42)固定的,所以每次输出相同,便于重复观察。
5.2 用 print 观察每一轮变化
为了看清楚冒泡排序的过程,可以改造函数,在每轮交换后或每轮结束后打印列表状态:
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 nums = [5, 1, 4, 2, 8] print("初始列表:", nums) bubble_sort_with_trace(nums)输出:
初始列表: [5, 1, 4, 2, 8] 第 1 轮后: [1, 4, 2, 5, 8] 第 2 轮后: [1, 2, 4, 5, 8] 第 3 轮后: [1, 2, 4, 5, 8]这里第 2 轮完成后列表已经有序,第 3 轮进入时发现swapped仍为False,于是直接退出,没有再执行第 4 轮。这个现象就是优化开关在起作用。
5.3 用断言验证排序结果
手写排序算法之后,建议使用assert自动验证结果是否真的正确,而不用肉眼一条条检查:
def test_bubble_sort(): test_cases = [ [], [1], [2, 1], [3, 3, 3], [5, 2, 9, 1, 5, 6], [10, 9, 8, 7, 6, 5, 4, 3, 2, 1], ] for case in test_cases: temp = case[:] # 复制一份,避免影响原列表 bubble_sort(temp) # 调用排序函数 assert temp == sorted(case) # 与内置排序结果对比 print("全部测试用例通过") test_bubble_sort()这种测试思路在写算法题时非常实用。利用 Python 内置sorted()作为参照,能帮你快速发现代码中的逻辑错误。
5.4 小扩展:降序排列
如果你已经掌握升序排列,降序排列只需把比较符号反转:
def bubble_sort_desc(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当然也可以复用 4.4 中的reverse参数,不必重复写函数。
6. 常见问题与排查思路
6.1 排序后原列表被修改了
问题现象:我调用排序函数后,原本的列表变了,但我不想让原列表被修改。
原因分析:冒泡排序是原地排序算法,函数内对列表元素的修改会直接作用到原对象上。这不是 bug,而是设计如此。
解决方案:如果你需要保留原始列表,在调用排序函数前用copy()或切片复制:
original = [3, 1, 2] new_list = original[:] # 复制一份 bubble_sort(new_list) # 对副本排序 print(original) # 原列表不受影响或者封装时不修改原列表,而是先创建一个新列表再排序:
def bubble_sort_immutable(arr): result = arr[:] # 复制 # 对 result 执行排序 return result6.2 为什么排序函数返回 None
问题现象:我执行result = bubble_sort(my_list),然后打印result发现是None。
原因分析:你写的函数内部没有return,函数默认返回None。原地排序修改的是传入的列表本身,所以不需要返回值。这是 Python 中常见的设计:像list.sort()就是原地排序并返回None;而sorted()则是返回新列表。
解决思路:如果你想通过返回值接收排序结果,要么在函数末尾return arr,要么改用排序新列表的实现。
6.3 索引越界
问题现象:代码报错IndexError: list index out of range。
原因分析:内层循环的边界范围计算错误。常见错误是写成:
for j in range(n - i): if arr[j] > arr[j + 1]: # j 最大为 n-1 时,arr[j+1] 越界解决方法:内层循环范围应该是range(n - 1 - i),确保j + 1最大为n - 1,不会越界。
6.4 相等元素位置变化了吗
问题现象:列表中有相同元素,排序后它们的相对顺序会变吗?
分析:冒泡排序只在>时交换,等于时不交换,所以相同元素的先后顺序不会改变。这种性质称为“稳定性”。例如[2a, 1, 2b],排序后2a仍然在2b前面,不会因为排序导致相同值的位置颠倒。
6.5 列表中有 None 或字符串会报错吗
问题现象:列表中混入了None或不同类型元素,排序时报错TypeError。
原因分析:冒泡排序依赖比较运算符>和<。Python 中不同的类型之间通常无法直接比较,比如整数和字符串:
5 > "3" # TypeError: '>' not supported between instances of 'int' and 'str'对于None,Python 3 中也不允许与数字直接比较。
解决建议:排序前保证列表元素类型一致。如果确实需要处理混合数据,可以自定义比较规则,但更推荐在数据清洗阶段就统一类型。
6.6 排查清单
遇到问题时,按下面顺序排查:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 结果不是升序 | 比较符号写反 | 检查>还是< |
| 列表有大数没排到末尾 | 内层循环边界少了-1 | 检查range(n - 1 - i) |
| 结果正确但运行慢 | 未做提前退出优化 | 增加swapped标记 |
| 函数返回 None | 没有return arr | 原地排序可忽略返回值 |
| 原始列表被改动 | 函数内部原地排序 | 调用前切片复制 |
| 类型错误 | 列表元素类型不一致 | 先统一类型 |
7. 最佳实践与工程建议
7.1 排序函数的封装与类型提示
日常工程中,如果只是排序,直接调用sorted()是最省事的。但为了学习或特定场景需要手写冒泡排序时,建议给函数加上类型注解和文档字符串,让代码更清晰:
from typing import List def bubble_sort(arr: List[int]) -> None: """ 使用冒泡排序对整数列表进行原地升序排序。 参数: 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: break7.2 复制列表避免修改原数据
无论使用哪种排序函数,只要你不希望函数影响原始数据,就应该显式地复制:
data = [4, 2, 9, 1] # 推荐使用切片复制 copied = data[:] # 或者使用 copy 方法 copied2 = data.copy() # 或者list工厂函数 copied3 = list(data)记住,直接赋值copied = data并没有创建新列表,两个变量指向的是同一个对象。
7.3 什么场景下不应该用冒泡排序
冒泡排序的时间复杂度是 O(n²),当列表长度很大(例如超过几千甚至上万)时,性能会明显下降。在生产环境中,处理真实业务数据时请直接使用 Python 内置的排序:
sorted(data):返回一个新的排好序的列表。data.sort():原地排序,更节省内存。- 自定义排序:
sorted(data, key=lambda x: x['age'])。
冒泡排序更适合教学、算法入门、小规模数据或对性能要求不高的场景。如果你在实际项目中为了提高排序性能手写冒泡排序,一定要慎重,内置排序 Timsort 的实现远比简单的冒泡排序高效。
7.4 结合 lambda 或 key 实现对象排序
虽然冒泡排序本身不提供key参数,但你可以先对元素应用某种规则生成中间列表,再排序。也可以改造冒泡排序,让它按照指定的函数返回值进行比较:
def bubble_sort_by_key(arr, key=lambda x: x): n = len(arr) for i in range(n - 1): swapped = False for j in range(n - 1 - i): if key(arr[j]) > key(arr[j + 1]): arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break students = [ {"name": "Alice", "score": 88}, {"name": "Bob", "score": 72}, {"name": "Cathy", "score": 95}, ] bubble_sort_by_key(students, key=lambda s: s["score"]) print(students)运行后 students 会按成绩score升序排列。这个示例展示了如何将冒泡排序思想灵活运用于字典等复杂对象。
7.5 测试与边界情况
写任何算法函数,都要考虑边界情况:
- 空列表:
[],排序后仍为空。 - 单元素列表:
[1],排序后不变。 - 全部相同元素:
[2, 2, 2],算法应稳定且快速结束。 - 倒序列表:
[5, 4, 3, 2, 1],这是冒泡排序最差的情况。 - 已升序列表:
[1, 2, 3, 4, 5],优化版可以在第一轮后提前退出。
把这些用例写进测试函数,能有效避免以后改动代码时引入隐藏 bug。
8. 总结与后续学习建议
到这里,冒泡排序的完整内容已经讲完了。你现在应该能理解冒泡排序的两层循环结构:外层控制轮数,内层控制相邻比较;也能写出带提前退出优化的 Python 函数;还知道了原地排序与返回新列表的区别,以及如何通过切片复制来保护原始数据。这个过程中接触到的比较、交换、索引边界和稳定性等概念,会继续出现在你后续学习的所有排序算法中。
下一步可以从两个方向继续深入:
- 再实现选择排序、插入排序,比较它们和冒泡排序的异同。
- 研究 Python 内置排序为什么快,学习 Timsort 的基本思想。
如果只是为了业务开发,请牢记一点:不需要重复造轮子,直接使用sorted()或list.sort()。但如果你想提升算法功底或应对面试,手写冒泡排序是一个很不错的起点。你可以把本文代码复制到本地,试着修改比较符号实现降序,试着加入print观察每轮变化,再用assert验证结果。多动手改代码,理解才会更深刻。