
这次我们来看一个 Python 初学者基本绕不开的排序算法冒泡排序。很多人在学习 Python 列表时只知道调用list.sort()或sorted()能直接完成排序却不清楚排序背后到底发生了什么。冒泡排序恰恰是把“列表遍历、双循环嵌套、元素交换、算法复杂度”这几个基础概念串起来的经典例子非常适合拿来理解 Python 列表操作和算法本质。这篇文章会把“用冒泡排序实现列表升序排列”这件事完整拆开从环境准备、基础代码、运行原理、函数封装、优化思路到测试验证、性能观察和常见报错排查全部讲一遍。如果你正准备 Python 入门、复习数据结构或者想搞懂为什么自己写的排序代码偶尔越界、偶尔结果不对可以直接收藏这篇文章。文中给出的代码都能在普通电脑上运行不依赖 GPU也不需要额外安装第三方库只要你本地有 Python 3 环境就够了。我们会先解释冒泡排序的核心特性再从简单实现开始逐步优化最后会补充一套排查清单方便你在出错时快速定位问题。1. 冒泡排序核心特性速览先给一个整体认知方便你快速判断这段内容是否适合当前需求。冒泡排序不是运行速度最快的排序算法但它是结构最简单的排序算法之一尤其适合学习阶段和小规模数据排序。特性项说明算法类型基于比较的稳定排序算法默认排序方向通过比较相邻元素每轮把当前未排序区间的最大值“冒泡”到末尾时间复杂度最坏和平均情况为 O(n²)最好情况已有序在优化后可达到 O(n)空间复杂度O(1)原地交换几乎不消耗额外内存稳定性稳定相等元素的相对顺序不会改变输入要求列表元素必须可比较通常是整数、浮点数、字符串等同类数据代码难度入门级适合练习 for 循环、while 循环、列表索引和变量交换典型应用学习算法、理解稳定排序、处理几十到几百个元素的小型列表从语言特性上看Python 中的列表非常适合实现冒泡排序因为列表本身是可变序列支持通过索引直接修改元素也支持用元组赋值一次性交换两个变量。理解这一点之后你会发现冒泡排序的核心代码比 C 或 Java 版本要简洁很多。2. 适用场景与使用边界冒泡排序适合哪类人我认为最主要的场景是 Python 初学者和准备算法面试的人。初学者可以通过冒泡排序理解循环嵌套外层循环控制轮数内层循环负责相邻比较面试复习则可以用它快速过一遍时间复杂度、稳定性和原地排序的概念。某些教学场景还会要求在不调用内置排序函数的前提下手写排序这时候冒泡排序就是一个门槛很低的答案。它不适合大数据量排序。假设列表长度是 10000冒泡排序最坏情况下需要执行约 5000 万次比较在纯 Python 环境下运行会明显变慢。如果列表长度继续增长到十万、百万级别继续用冒泡排序处理线上数据基本不现实。商业项目或数据分析中的常规排序任务应当优先使用 Python 内置的sort()或sorted()它们基于 Timsort 实现平均时间复杂度为 O(n log n)稳定性好性能远高于手写冒泡排序。冒泡排序还有一个容易被忽略的优点稳定。稳定排序的意思是如果列表中有两个值相等的元素排序之后它们的相对前后顺序不会改变。对只包含数字的列表来说这个特性无所谓但如果列表元素是对象并且你希望先按主关键字排序、再按次要关键字排序稳定的排序算法会有帮助。另外要特别注意使用边界手写的冒泡排序函数不应该被当成通用排序工具随意使用。在真实项目中优先相信语言内置排序在练习中则可以放心用随机列表验证自己的实现是否正确。理解这些边界才能知道自己写的代码什么时候能上什么时候必须换方案。3. Python 环境准备与前置条件开始写代码之前先确认电脑上的 Python 环境是正常的。大部分冒泡排序示例不需要额外第三方库因此只要 Python 3 能运行即可。打开终端或命令提示符输入下面两条命令检查版本python --version pip --version如果你在 Windows 上执行python提示“不是内部或外部命令”通常是因为安装 Python 时没有勾选“Add Python to PATH”。重新运行安装包在第一个安装界面勾选该选项再重启终端即可。macOS 和 Linux 下一般直接执行python3也能调用系统安装的 Python例如python3 --version如果你是用 Anaconda 或 Miniconda 管理 Python 环境可以先创建一个干净的虚拟环境避免不同项目的依赖互相影响conda create -n sort-demo python3.10 -y conda activate sort-demo如果使用系统自带的 Python 虚拟环境模块可以在项目目录里执行python -m venv venv source venv/bin/activate上面的命令适用于 macOS 和 Linux。Windows 下激活命令会改为venv\Scripts\activate环境准备好之后你还需要一个代码编辑工具。用 VSCode、PyCharm、IDLE 都可以。如果使用 VSCode建议先安装 Python 扩展再按Ctrl Shift P选择当前 Python 解释器这个步骤能避免运行代码时误用了别的环境。VSCode 中看不到函数列表或解释器不对时优先检查右下角的 Python 版本标识。进入正题后我们会把代码保存为.py文件比如bubble_sort.py然后在终端用命令运行python bubble_sort.py如果你想快速验证一两行代码也可以直接进入 Python 交互环境python进入交互环境后Python 的提示符会变成这时可以逐行输入代码测试适合查看列表当前内容或验证小函数。4. 用 Python 实现冒泡排序升序排列4.1 准备一个 Python 列表先来看列表的创建方式。列表是 Python 中最常用的序列类型用方括号表示元素之间用逗号分隔。下面是一个待排序的整数列表nums [64, 34, 25, 12, 22, 11, 90] print(nums)运行后会输出[64, 34, 25, 12, 22, 11, 90]列表支持索引访问比如nums[0]得到 64nums[-1]得到 90。冒泡排序要频繁访问相邻元素因此会大量用到nums[j]和nums[j 1]这样的写法。4.2 冒泡排序的升序实现第一版冒泡排序的基本思路是从列表第一个元素开始依次比较相邻两个元素如果前一个元素大于后一个元素就把它们交换这样一轮结束后最大的元素会被移动到列表末尾。然后忽略最后一个已经就位的元素继续对剩余元素重复同样的操作。直接看代码nums [64, 34, 25, 12, 22, 11, 90] n len(nums) for i in range(n - 1): for j in range(n - 1 - i): if nums[j] nums[j 1]: nums[j], nums[j 1] nums[j 1], nums[j] print(nums)这里有两个重点。第一外层循环for i in range(n - 1)表示最多需要 n - 1 轮排序因为每一轮至少能让一个元素到达最终位置所以 n 个元素最多需要 n - 1 轮。第二内层循环for j in range(n - 1 - i)表示每一轮只需要比较“还没有排序完成”的部分已经冒泡到末尾的元素不需要再参与比较。代码里的交换语句是 Python 的元组赋值特性nums[j], nums[j 1] nums[j 1], nums[j]它等价于其他语言里的三步交换写法但更简洁。如果你把这个写法展开用临时变量实现也是可以的temp nums[j] nums[j] nums[j 1] nums[j 1] temp第一版代码运行后nums会变成升序排列的[11, 12, 22, 25, 34, 64, 90]。为了让理解更直观我在下面列出第一轮完整交换过程比较次数比较元素是否交换列表变化第 1 次64 和 34交换[34, 64, 25, 12, 22, 11, 90]第 2 次64 和 25交换[34, 25, 64, 12, 22, 11, 90]第 3 次64 和 12交换[34, 25, 12, 64, 22, 11, 90]第 4 次64 和 22交换[34, 25, 12, 22, 64, 11, 90]第 5 次64 和 11交换[34, 25, 12, 22, 11, 64, 90]第 6 次64 和 90不交换[34, 25, 12, 22, 11, 64, 90]第一轮结束后最大的元素 90 已经位于列表末尾。第二轮会在前 6 个元素中继续比较将 64 移动到倒数第二个位置。每一轮“最大元素像气泡一样浮到末尾”这就是冒泡排序名字的由来。4.3 在交互环境中查看每一轮结果如果你希望看到每一轮排序后的列表可以用临时调试版代码在内外层循环之间加printnums [64, 34, 25, 12, 22, 11, 90] n len(nums) for i in range(n - 1): for j in range(n - 1 - i): if nums[j] nums[j 1]: nums[j], nums[j 1] nums[j 1], nums[j] print(f第 {i 1} 轮后: {nums})输出结果大致如下第 1 轮后: [34, 25, 12, 22, 11, 64, 90] 第 2 轮后: [25, 12, 22, 11, 34, 64, 90] 第 3 轮后: [12, 22, 11, 25, 34, 64, 90] 第 4 轮后: [12, 11, 22, 25, 34, 64, 90] 第 5 轮后: [11, 12, 22, 25, 34, 64, 90] 第 6 轮后: [11, 12, 22, 25, 34, 64, 90]注意看第 5 轮和第 6 轮的结果它们已经完全一样说明列表在第 5 轮就已经有序。如果数据本身基本有序我们仍然会多跑几轮空操作。这也是下一节要做优化的原因。5. 排序函数封装与优化5.1 把冒泡排序封装成函数在实际代码中我们不会把排序逻辑直接散写在脚本顶层而是封装成函数。封装之后可以复用便于测试和集成。下面这个函数版本会在内部复制一份列表排序后返回新列表不会修改调用方传入的原列表def bubble_sort(data): result data[:] n len(result) for i in range(n - 1): for j in range(n - 1 - i): if result[j] result[j 1]: result[j], result[j 1] result[j 1], result[j] return result函数第一行使用切片data[:]复制整个列表为什么不能直接写result data因为直接赋值只复制了引用result和data指向同一个列表对象。函数内部交换result元素时外部原列表也会被修改。切片复制之后原列表保持不动。测试这个函数nums [64, 34, 25, 12, 22, 11, 90] sorted_nums bubble_sort(nums) print(原列表:, nums) print(排序后:, sorted_nums)运行效果如下原列表: [64, 34, 25, 12, 22, 11, 90] 排序后: [11, 12, 22, 25, 34, 64, 90]这个行为与 Python 内置的sorted()一致返回新列表不改原列表。如果你希望像list.sort()那样原地修改就不要复制列表直接在传入的data上交换。5.2 优化一提前结束第一版代码对于已经有序的列表仍然会执行完整的双层循环操作。所谓优化核心思路是如果某一轮内层循环从头到尾都没有发生任何交换说明列表已经有序可以直接退出。实现方案是增加一个布尔标记swappeddef bubble_sort(data): result data[:] n len(result) for i in range(n - 1): swapped False for j in range(n - 1 - i): if result[j] result[j 1]: result[j], result[j 1] result[j 1], result[j] swapped True if not swapped: break return result当某一轮完全不需要交换时swapped保持为False循环提前退出。对于已经有序的列表冒泡排序的时间复杂度可以降到 O(n)。虽然这是理想情况但能在很大程度上避免无意义的遍历。5.3 优化二记录最后一次交换位置还有一种常用优化每一轮内层循环比较时最后发生交换的位置之后的元素已经有序下一轮不需要再比较到固定的n - 1 - i只需要比较到上一次最后一次交换的位置即可。def bubble_sort(data): result data[:] n len(result) while n 1: last_swap_index 0 for j in range(n - 1): if result[j] result[j 1]: result[j], result[j 1] result[j 1], result[j] last_swap_index j 1 n last_swap_index return resultlast_swap_index表示这一轮最后一次发生交换的位置的后一个索引。下一轮最多只需要比较到这个位置因为索引更靠后的部分已经不会再产生交换。这个版本在数据部分有序时能明显减少无效比较。5.4 支持降序和自定义比较方向有时候我们不只是需要升序排列还需要做一个降序版本。可以在函数里增加一个参数reverse升序时比较result[j] result[j 1]降序时比较result[j] result[j 1]def bubble_sort(data, reverseFalse): result data[:] n len(result) for i in range(n - 1): swapped False for j in range(n - 1 - i): if reverse: need_swap result[j] result[j 1] else: need_swap result[j] result[j 1] if need_swap: result[j], result[j 1] result[j 1], result[j] swapped True if not swapped: break return result调用方式nums [3, 1, 4, 1, 5, 9, 2, 6] print(bubble_sort(nums)) # 升序 print(bubble_sort(nums, reverseTrue)) # 降序对于纯数字列表升序就是从小到大排列降序就是从大到小排列。如果你以后碰到“把列表按其他规则排序”的需求比如按字符串长度、按对象属性排序也可以从这个函数中寻找思路核心是修改比较规则。6. 功能测试与效果验证写完排序函数之后不能只看一组数据就说实现正确。合理的做法是准备多组测试用例覆盖普通乱序、部分有序、完全有序、空列表、单元素列表等情况。下面是一个简单的验证脚本def bubble_sort(data, reverseFalse): result data[:] n len(result) for i in range(n - 1): swapped False for j in range(n - 1 - i): if reverse: need_swap result[j] result[j 1] else: need_swap result[j] result[j 1] if need_swap: result[j], result[j 1] result[j 1], result[j] swapped True if not swapped: break return result test_cases [ [64, 34, 25, 12, 22, 11, 90], [5, 2, 9, 1, 5, 6], [1, 2, 3, 4, 5], [], [42], ] for case in test_cases: result bubble_sort(case) print(f{case} - {result})预期输出分别是升序排列后的列表其中空列表排序后还是空列表单元素列表排序后还是单元素。除了肉眼判断也可以用断言直接测试assert bubble_sort([3, 1, 2]) [1, 2, 3] assert bubble_sort([5, 2, 9, 1, 5, 6]) [1, 2, 5, 5, 6, 9] assert bubble_sort([]) [] assert bubble_sort([42]) [42] assert bubble_sort([3, 1, 2], reverseTrue) [3, 2, 1] print(所有测试通过)如果脚本没有任何输出报错说明基础逻辑通过了测试。建议你以后写排序相关函数时都保留这样一套最小测试用例它能快速暴露边界问题。还可以用 Python 内置排序做交叉验证。拿随机生成的列表分别交给手写冒泡排序和内置sorted()比较结果是否一致import random for _ in range(10): data [random.randint(0, 1000) for _ in range(200)] result1 bubble_sort(data) result2 sorted(data) if result1 ! result2: print(结果不一致:, data) break else: print(随机数据验证通过)这种验证方式很实用。手写算法的结果如果和内置排序一致大概率说明你的基本逻辑没有错误如果不一样则要重点检查比较符号、循环边界和索引是否正确。测试完数字列表可以顺带试一下字符串列表。只要所有字符串类型一致Python 会按字典序比较代码不需要改动words [banana, apple, cherry, date] print(bubble_sort(words))运行后得到[apple, banana, cherry, date]这也说明冒泡排序本身不关心元素具体是谁只关心“两个相邻元素是否满足顺序要求”。7. 性能观察与内置排序对比7.1 时间复杂度和空间复杂度从复杂度角度分析基础版冒泡排序在最坏情况下需要执行约 n²/2 次比较因此时间复杂度是 O(n²)平均情况同样接近 O(n²)。优化版在已经有序的列表上可以达到 O(n)因为第一轮就会因为没有交换而提前退出。空间方面冒泡排序属于原地排序只使用常数级额外空间空间复杂度是 O(1)。Python 内置的sorted()和list.sort()使用 Timsort 算法平均时间复杂度为 O(n log n)并且会在内部处理已经有序的片段。所以从工程角度考虑内置排序性能远好于手写冒泡排序。冒泡排序的价值更多是教学与算法思维训练。7.2 用 timeit 观察运行时间如果你想直观看到性能差异可以使用timeit模块。用它测试同一个随机列表的冒泡排序耗时和内置排序耗时import random import timeit def bubble_sort(data): result data[:] n len(result) for i in range(n - 1): swapped False for j in range(n - 1 - i): if result[j] result[j 1]: result[j], result[j 1] result[j 1], result[j] swapped True if not swapped: break return result data [random.randint(0, 10000) for _ in range(2000)] time_bubble timeit.timeit(lambda: bubble_sort(data), number1) time_builtin timeit.timeit(lambda: sorted(data), number1) print(冒泡排序耗时:, time_bubble) print(内置排序耗时:, time_builtin)运行时间会受到电脑性能、Python 版本和随机数据的影响具体数值不用太在意。关键是观察趋势随着data长度从 1000 增加到 5000、10000冒泡排序耗时增长非常明显而内置排序耗时增长相对平缓。如果你的代码运行速度慢到无法接受优先确认是不是数据量太大或者自己是不是还在用没有优化的基础版排序处理数千条记录。7.3 什么情况下应该观察资源占用纯 Python 排序不需要额外关注显存它是 CPU 计算任务内存占用也不高。真正需要观察的是排序耗时和 CPU 使用率。即使是一个 1 万元素的列表手写冒泡排序也可能让你明显感到卡顿如果列表长度达到 10 万基本不建议再用冒泡排序。遇到这种情况直接换sorted()或list.sort()才是正确选择。从工程角度看判断一个函数是否可用不能只看它能输出正确结果还要看它在真实数据规模下能不能按时完成。建议你在自己的排序函数里保留输出开关或日志方便调试时观察每轮状态但正式使用时不要频繁打印中间结果否则 I/O 反而会成为性能瓶颈。8. 常见问题与排查方法初学者在尝试“冒泡排序实现列表升序排列”时报错类型往往集中在缩进、索引越界、类型错误和结果方向不对这几类。下面是一张实用的排查表问题现象可能原因排查方式解决方案运行脚本后没有任何输出脚本中只定义了函数没有调用检查脚本末尾有没有调用函数和 print在末尾调用bubble_sort(nums)并打印结果IndentationError: expected an indented blockif 或 for 下面的代码没有正确缩进查看报错行号Python 用缩进表示代码块统一使用 4 个空格IndexError: list index out of range内层循环范围写成range(len(data))检查 j 的最大取值使用range(n - 1 - i)保证访问result[j 1]不越界TypeError: not supported between instances of int and str列表中同时包含整数和字符串无法直接比较打印列表元素类型先清洗数据或把元素统一转成同类型排序结果是降序比较符号方向写反打印某一轮前后状态确认升序用降序用返回值是 None函数内部直接交换原列表且没有 return或 return 写错位置打印函数返回值检查明确设计返回新列表就 return result原地修改则不要依赖返回值原列表被修改没有用切片复制列表在函数入口打印id(data)和id(result)用result data[:]创建副本列表已经有序但程序仍很慢没有使用提前退出优化用一个有序长度为 10000 的列表测试增加swapped标记无交换时直接 break命令行运行提示找不到命令Python 未安装或未加入环境变量输入python --version重新安装 Python 并勾选 Add Python to PATH除了表格里的问题还有几个经验性建议第一个建议是善用print调试。遇到排序结果不对时不要盯着代码猜直接在每一轮循环结束后打印当前列表。比如下面的日志版函数能帮你看到排序过程def bubble_sort_with_log(data): result data[:] n len(result) for i in range(n - 1): swapped False for j in range(n - 1 - i): if result[j] result[j 1]: result[j], result[j 1] result[j 1], result[j] swapped True print(f第 {i 1} 轮后: {result}) if not swapped: break return result第二个建议是警惕“等于号比较”导致死循环。冒泡排序只在出现逆序时交换如果写成虽然对于纯数字的升序排列结果看起来仍然正确但会破坏稳定性并且可能增加不必要的交换次数。如果面试题考察稳定排序这一点会成为扣分项。第三个建议是不要在函数内部直接修改传入列表除非你有意实现“原地排序”。外部代码调用排序函数时通常不希望原来的数据被悄悄覆盖。如果你需要原地效果用data.sort()更合适如果需要新列表则使用sorted(data)。手写冒泡排序时也要参考这两类 API 的语义避免和调用方产生误解。9. 列表相关小技巧与最佳实践冒泡排序的代码写多了之后你会发现它离不开列表的各种基础操作。这里单独整理几个与列表排序密切相关的技巧方便你后续处理更复杂的数据。第一个技巧是复制列表。前面已经提到data[:]是 Python 中常用的浅拷贝方式。如果你写b a两个变量指向同一个列表修改任意一个都会影响另一个。需要保留原列表时务必使用切片或list(data)创建副本。第二个技巧是切片应用。假设你只想查看排序后最小的 3 个数可以先排序再切片nums [5, 9, 2, 7, 1, 8, 3] sorted_nums sorted(nums) top3 sorted_nums[:3] print(top3)列表中[:3]表示取前 3 个元素。列表切片和排序配合使用非常适合处理排行榜、Top N 查询等需求。如果你的数据量不大这样写非常直观。第三个技巧是列表推导式。测试冒泡排序时经常需要生成随机列表用列表推导式最方便import random data [random.randint(0, 1000) for _ in range(100)] print(data[:10])上面代码生成了 100 个 0 到 1000 之间的随机整数。把列表推导式和函数调用结合起来可以快速构造特殊测试数据例如nearly_sorted [i for i in range(1, 101)] nearly_sorted[50], nearly_sorted[51] nearly_sorted[51], nearly_sorted[50]这样可以得到一个只有少量乱序的列表用来测试冒泡排序提前退出优化是否生效。第四个技巧是使用enumerate和zip方便同时遍历。虽然冒泡排序本身不需要这两个函数但当你需要对多个列表做关联处理时它们很常见。比如“两个列表转字典”在 Python 中可以这样写keys [a, b, c] values [1, 2, 3] result dict(zip(keys, values)) print(result)这样得到{a: 1, b: 2, c: 3}。如果你在练习排序时遇到这类需求建议先掌握基本的列表遍历、切片和推导式再扩展到字典和集合。第五个技巧是保持代码风格一致。手写排序函数时建议给函数名加上明确的语义比如bubble_sort不要用list作为变量名否则会覆盖 Python 内置类型导致后续类型转换或排序相关代码出现奇怪问题。也要避免使用sorted作为函数名因为它会覆盖内置函数。养成良好的命名习惯能省下不少排错时间。10. 总结与下一步回到文章标题Python 小技巧用冒泡排序实现列表升序排列。表面上这是一个排序问题实际上它考察的是列表索引、双循环嵌套、元素交换和边界条件控制。最值得你亲自跑通的是基础版代码第一版跑通之后再逐步加入函数封装、提前退出优化和降序支持。最容易踩的坑有三个内层循环边界越界、比较符号写反、函数修改了原列表。建议先把自己写的冒泡排序和sorted()放到同一组随机数据上做交叉验证确认结果一致再继续优化性能。如果只是练习算法数据量控制在几百到几千即可如果要在真实项目里处理大数据直接用内置排序是更稳的选择。在此之后你可以继续学习选择排序、插入排序、归并排序和快速排序等理解了不同算法的时间复杂度和稳定性再回来对比冒泡排序会有更完整的认识。