ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

冒泡排序详解:Python 列表升序排列的实现与优化思路

冒泡排序详解:Python 列表升序排列的实现与优化思路 很多初学者在学 Python 时都会遇到一个绕不开的需求把列表里的数据按从小到大排序。比如统计成绩后要看分数排名、处理时间数据后要按日期先后展示、做数据分析前要对原始序列做清洗这些场景都离不开“排序”。虽然 Python 内置的sorted()和list.sort()已经非常强大但“如何手写一个排序算法”仍然是理解循环、边界条件、数据交换和算法复杂度的重要练习。本文就以 Python 列表的升序排列为目标完整拆解冒泡排序的原理、代码实现、优化思路和常见坑点。读完这篇文章你不光能写出一道“冒泡排序实现列表升序排列”的作业题还能在面试或笔试中把交换逻辑和优化点讲清楚。1. 冒泡排序是什么为什么值得学1.1 从名字理解冒泡排序冒泡排序的英文是 Bubble Sort是一种最基础的交换排序算法。它的核心思路可以概括为一句话重复地遍历待排序的列表依次比较相邻两个元素如果前一个元素比后一个元素大就把它们交换位置。每一轮遍历结束当前未排序部分中的最大值就像气泡一样慢慢“浮”到列表末尾。这里有一个容易混淆的点冒泡排序中的“浮起来”在不同的代码写法里方向可能不同。有的教程写成从第一个元素开始往后比较让大元素逐步向后移动有的教程写成从最后一个元素开始往前比较让小元素逐步向前移动。两种写法最终都能实现升序排列但它们观察过程的角度不一样。本文主要采用从头到尾遍历、把较大的元素向后交换的写法这也是最贴合“最大值浮到末尾”直觉的版本。冒泡排序是稳定排序吗是的。稳定指的是如果列表中存在两个相等的元素排序后它们的相对顺序不会改变。冒泡排序在实现时只对“前一个元素大于后一个元素”的情况进行交换相等的元素不会被交换位置所以相对顺序得以保留。这个特点在按多个条件排序时有实际意义比如先按总分排再按学号排稳定的算法可以避免无意义的乱序。1.2 冒泡排序解决什么问题冒泡排序解决的核心问题是把一个无序的列表整理成升序或降序列表。举例来说输入一个列表[6, 3, 8, 2]经过冒泡排序后输出应该是[2, 3, 6, 8]。在真实业务中手写冒泡排序并不是常规选择因为 Python 的内置排序算法 Timsort 在多数情况下更快。但冒泡排序依然值得掌握原因至少有三个它是理解算法入门的基础能帮助你掌握循环嵌套、边界控制、交换变量等基础语法。它是面试和笔试中的高频题目很多考核会要求不调用sort()手写排序过程。它的代码简单、过程直观适合用来观察排序算法的每一步变化为进一步学习插入排序、选择排序、快速排序打下基础。1.3 冒泡排序的适用边界冒泡排序的时间复杂度是 O(n²)这意味着当列表长度 n 变大时比较次数会迅速增长。比如 10 个元素的比较量大约在几十次1000 个元素则可能接近 50 万次比较。因此它更适合教学练习、数据量很小的排序场景或者对简单性要求高于性能要求的场合。如果数据量大到几千甚至上万条强烈建议改用内置的list.sort()或sorted()。内置排序采用更复杂的 Timsort 算法时间复杂度和稳定性都比手写冒泡排序优秀而且经过了大量测试不容易出错。后面的章节也会对比这两类方法方便你在实际项目中做取舍。2. 环境准备与运行方式2.1 需要的 Python 版本本文示例代码使用 Python 3 语法建议使用 Python 3.8 及以上版本。大多数代码只用了最基础的列表、循环和函数定义只要你的电脑装了 Python 3 就可以运行。老旧的 Python 2 环境不建议继续使用因为print语法、整数除法和类型处理方式都不同容易遇到不必要的兼容问题。如果你还没有安装 Python可以去 Python 官网下载对应操作系统的安装包。macOS 和 Linux 多数自带 Python 3Windows 安装时需要注意勾选“Add Python to PATH”。安装完成后打开终端或命令行窗口执行下面的命令检查版本python3 --version如果你使用的是 Windows部分环境里命令是python --version。执行后能看到类似Python 3.10.x、Python 3.12.x的输出版本信息即可。只要主版本是 3本文代码都可以正常运行。2.2 项目文件与运行命令这个示例不需要安装任何第三方库也不需要配置数据库和网络环境。建议新建一个单独的 Python 文件比如bubble_sort_demo.py然后把代码写入文件。这样比在交互式解释器里一行一行执行更容易调试也能看到完整的运行结果。在文件所在目录执行下面的命令python bubble_sort_demo.py如果你的系统把 Python 3 命令命名为python3就执行python3 bubble_sort_demo.py使用 VS Code、PyCharm 或 Jupyter Notebook 也完全可以。本文的代码以脚本文件为主如果你使用 Jupyter只需要把代码拆到不同的单元格里观察每一轮的结果会更直观。3. 用一个小例子拆解升序排列过程3.1 初始列表与第一轮排序假设初始列表是numbers [6, 3, 8, 2]目标是将它排列成[2, 3, 6, 8]。第一轮排序需要从头到尾依次比较相邻元素比较范围是整个列表。第一次比较6和3因为6 3所以交换列表变成[3, 6, 8, 2]。第二次比较当前第二项6和第三项8因为6 8所以不需要交换。第三次比较当前第三项8和第四项2因为8 2所以交换列表变成[3, 6, 2, 8]。第一轮结束后最大的数字8被移动到了列表末尾。这个结果验证了冒泡排序的第一条规律每经过一轮完整的遍历至少能保证当前参与排序范围内的最大值到达末尾位置。3.2 第二轮排序第二轮排序时最后一位8已经不该再参与移动因为它已经处于最终位置。这时只需要比较列表中前三个元素也就是[3, 6, 2]这部分。比较3和6不需要交换。比较6和2因为6 2所以交换列表变成[3, 2, 6, 8]。第二轮结束后第二个大值6被放到了倒数第二的位置。为了方便观察第二轮可以理解为“在剩余未排序区域内再把最大值浮到末尾”。由于前一轮已经把最大值送到了末端每轮要扫描的区间长度都会减少一个。3.3 第三轮排序与最终结果第三轮排序只需要处理前两个元素也就是[3, 2]。比较3和2因为3 2所以交换列表变成[2, 3, 6, 8]。此时列表已经有序。对于 4 个元素的情况外层最多需要执行 3 轮因为每轮确定一个最大值最后一个元素不需要再主动扫描。如果把整个交换过程打印出来可以看到每轮列表变化如下初始列表 [6, 3, 8, 2] 第 1 轮后 [3, 6, 2, 8] 第 2 轮后 [3, 2, 6, 8] 第 3 轮后 [2, 3, 6, 8]这个手动过程就是冒泡排序在内层循环中反复比较、交换的缩影。理解了这个过程再写代码就不会只停留在“背模板”的层面了。4. Python 实现冒泡排序的基础版本4.1 完整可运行的代码先来看一个最直接的基础版本。这个版本没有做过多的优化但逻辑清晰非常适合初学者对照上面拆解过程理解代码def bubble_sort(arr): n len(arr) for i in range(n - 1): for j in range(n - i - 1): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] return arr if __name__ __main__: numbers [6, 3, 8, 2] print(排序前, numbers) bubble_sort(numbers) print(排序后, numbers)运行这段代码会得到下面的输出排序前 [6, 3, 8, 2] 排序后 [2, 3, 6, 8]把这个基础版本保存为 Python 文件后可以直接复制运行。它不依赖任何第三方模块也不需要使用命令行参数是学习冒泡排序的清晰起点。4.2 代码逐段解释第一行def bubble_sort(arr):定义了一个函数接收参数arr这个参数是待排序的列表。n len(arr)先取列表长度后面的循环范围都需要它。之所以先算出来一方面是为了代码简洁另一方面是避免在循环内重复计算列表长度。外层循环for i in range(n - 1):控制需要进行多少轮排序。列表里有 n 个元素时最多需要 n - 1 轮因为每一轮至少会把一个元素放到正确位置而最后一个元素在剩余元素都排好之后自然就位于正确位置。如果写成range(n)虽然程序不会直接报错但会多做一轮无意义的扫描。内层循环for j in range(n - i - 1):控制当前轮要比较到哪一位。这里非常容易出错也是最容易出现IndexError的地方。由于比较时需要用j访问当前元素再用j 1访问后一个元素所以循环范围必须保证j 1不越界。外层执行第 i 轮时末尾已经有 i 个元素排好剩下的待排序区域长度为 n - i而区域中最后一个元素的下标是 n - i - 1因此内层j最大只能取到 n - i - 2。Python 的range(n - i - 1)恰好会生成从 0 到 n - i - 2 的整数序列。比较和交换是冒泡排序的核心动作。if arr[j] arr[j 1]:判断前一个元素是否大于后一个元素。如果是升序排列当前一个元素大于后一个元素时就应该交换如果调换比较符号排序结果就会变成降序。交换语句arr[j], arr[j 1] arr[j 1], arr[j]是 Python 中非常简洁的交换写法。它先计算右侧表达式得到两个值再同时赋值给左侧的两个位置。这种语法可以避免使用临时变量也使代码更贴近“交换两个元素”的语义。4.3 原地排序与返回列表上面这个函数会对传入的列表进行原地排序。所谓原地排序指的是直接修改了原始列表对象的内容而不是创建一个新列表。函数末尾的return arr其实只是把同一个列表对象返回出去方便调用方链式使用。即使不写return arr调用bubble_sort(numbers)之后外部变量numbers的内容也已经变化了。这种做法和 Python 内置的list.sort()类型一致但和sorted()不同。sorted()会生成一个新列表而原列表保持不变。理解这一点非常重要否则可能会写出“函数内部排序成功函数外部列表却没变”的代码。5. 冒泡排序的两种常见优化5.1 使用交换标记提前结束基础版本有一个缺点即使列表在某轮排序完成之前已经有序外层循环仍然会继续执行导致不必要的比较。比如一个几乎有序的列表[2, 1, 3, 4, 5]第一轮交换了2和1之后整个列表已经有序但基础版的第二轮和第三轮还会继续扫描。解决方案是增加一个布尔变量swapped每轮开始时把它设为False。内层循环只要发生交换就把它设为True。一轮结束后检查swapped如果仍然是False说明这一轮没有发生任何交换也就意味着列表已经有序可以直接退出循环。优化后的代码如下def bubble_sort_with_flag(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 if not swapped: break return arr这个版本的代码在最好情况下也就是列表本来已经有序时只需要扫描一轮就能判断出不需要后续排序。时间复杂度可以从 O(n²) 优化到 O(n)。这里的 O(n) 是指只进行一轮线性扫描比较 n - 1 次即可结束。对普通的随机无序列表来说交换标记虽然不能改变最坏情况下的 O(n²) 时间复杂度但可以减少一些不必要的无效扫描是一种低成本又有价值的改进。面试中如果能在写完基础版本后主动补充这个优化点通常会让评价明显更好。5.2 记录最后一次交换位置再进一步优化可以记录每一轮最后一次发生交换的位置。这个位置之后的元素通常已经在前面几轮中被放到了正确位置因此下一轮只需要扫描到该位置即可不需要再检查之后的部分。这种思路对“末尾已经是较大有序序列”的数据特别友好。def bubble_sort_last_swap(arr): last_swap_index len(arr) - 1 while last_swap_index 0: current_limit last_swap_index last_swap_index 0 for j in range(current_limit): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] last_swap_index j return arr这个版本稍微难理解一些。每轮开始时current_limit保存当前这一轮需要处理到的边界。内层循环中如果发生交换就把last_swap_index更新为 j表示这轮最后一次交换发生在下标 j 的位置。由于下标 j 之后的元素已经在前面的比较中没有交换说明它们已经达到有序状态所以下一轮的边界可以缩小到 j。从工程角度看这个版本更适合“大部分数字已经有序只有少数几个数字位置不对”的数据。不过它也更容易在循环边界上写错建议多拿几个不同数据测试后再使用。初学者可以先掌握带swapped标记的版本把记录最后一次交换位置作为进阶练习。5.3 排序过程中临时打印结果调试冒泡排序时最想在控制台里看到的是“每一轮结束后列表变成了什么样”。你可以在外层循环内部加入print快速定位是哪一轮比较逻辑出了问题。示例代码如下def bubble_sort_debug(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 print(f第 {i 1} 轮后{arr}) if not swapped: break运行下面的测试数据test_list [5, 1, 4, 2, 8] bubble_sort_debug(test_list)输出结果会是第 1 轮后[1, 4, 2, 5, 8] 第 2 轮后[1, 2, 4, 5, 8] 第 3 轮后[1, 2, 4, 5, 8]看到第 2 轮后列表已经有序但第 3 轮仍然进入循环扫描一次。这里的第 3 轮并不是“又排序了一次”而是因为第 2 轮发生过交换swapped仍是True所以程序需要再进行一轮确认。如果第 3 轮没有发生交换它就会在检查swapped后退出。这个现象也能帮助你理解“为什么冒泡排序有时会比理论最大轮数少一轮”的原因。6. 与内置排序方法的对比6.1list.sort()和sorted()的用法Python 中最推荐的升序排序方式并不是手写冒泡排序而是使用内置方法。list.sort()是列表对象的方法它会对原列表进行原地排序排序后原列表会改变。例如numbers [6, 3, 8, 2] numbers.sort() print(numbers) # [2, 3, 6, 8]numbers.sort()的返回值是None。不少初学者会把numbers numbers.sort()结果发现自己把列表变成了None这就是没有理解原地方法返回值造成的。sorted()是 Python 内置函数它可以接收一个可迭代对象比如列表、元组、字典的键然后返回一个新的列表。原列表不会被修改。写法如下numbers [6, 3, 8, 2] new_numbers sorted(numbers) print(numbers) # 原列表仍然是 [6, 3, 8, 2] print(new_numbers) # 排序后的新列表 [2, 3, 6, 8]6.2 常用参数reverse不论是list.sort()还是sorted()都支持reverseTrue参数用于从大到小排序。冒泡排序也可以通过把比较符号改成来实现降序但手写代码时多一个参数就显得不那么灵活。内置方法使用起来更简单scores [88, 95, 72, 100, 66] scores.sort(reverseTrue) print(scores) # [100, 95, 88, 72, 66]如果希望按字符串长度排序还可以使用key参数。sorted(words, keylen)表示按每个元素的长度排序。key参数是内置排序方法非常强大的特性手写冒泡排序要实现类似功能需要额外维护一个“比较依据”代码会复杂得多。6.3 什么时候使用内置方法什么时候手写实际项目中绝大多数排序需求都应该交给内置方法。内置方法由 C 语言实现经过了大量优化和测试不仅性能好也更可靠。但学习手写冒泡排序依然有意义因为它能让你更深刻地理解排序算法的复杂度来源、循环边界和数据交换方式也会在算法类笔试中派上用场。建议记住一个基本原则能确认使用场景、不追求复杂逻辑时优先用内置排序面试或课程设计要求手写排序时再实现冒泡排序。把这两个层次分开代码质量会更高。7. 常见问题与排查思路7.1 高频报错对照表不少初学者实现冒泡排序时遇到的问题其实集中在几个固定位置。下面这张表整理了最常见的现象、原因和排查方向问题现象常见原因解决思路IndexError: list index out of range内层循环范围设置过大内层写成range(n - i - 1)保证j 1不越界函数排序后外部列表没有变化函数内使用了arr 新列表而不是索引赋值检查是否用arr[j], arr[j1] ...原地交换相等元素顺序被打乱比较条件使用了升序排序使用让相等元素不交换运行后是降序排列比较方向反了升序用arr[j] arr[j 1]时交换对混合类型排序报TypeError列表里同时有字符串和数字排序前清洗数据保证元素类型一致list.sort()后变量变成None把原地排序的返回值赋给了原变量使用sorted(arr)或直接调用arr.sort()7.2 函数外列表没变为什么会这样这是一个很经典的 Python 列表传参问题。判断标准是函数内部是否修改了列表对象本身。如果写成arr sorted(arr)相当于把函数局部变量arr指向了一个新列表外部变量仍然指向原来的旧列表。正确的做法是直接修改原列表的元素位置示例def bubble_sort_wrong(arr): arr sorted(arr) # 这里只是局部变量重新指向新列表 return arr data [3, 1, 2] bubble_sort_wrong(data) print(data) # 仍然输出 [3, 1, 2]而正确的冒泡排序通过arr[j], arr[j 1] arr[j 1], arr[j]修改列表某个下标位置的内容并没有重新给arr赋值所以外部列表会被修改。要注意这两类写法的区别。7.3 保留原始列表的需求如果业务中需要同时保留原始列表和排序后的列表最好不要让排序函数原地修改原始数据。可以在调用前复制一份列表使用切片[:]或copy()方法source [6, 3, 8, 2] data_for_sort source[:] bubble_sort(data_for_sort) print(source) # 原始列表 [6, 3, 8, 2] print(data_for_sort) # 排序后列表 [2, 3, 6, 8]如果希望函数本身不修改传入列表而是在函数内部先复制再返回新列表也可以把arr[:]放在函数内部的最前面不过这样做之后函数的行为就和内置sorted()类似不再是“原地排序”了。代码设计前最好先明确是要原地排序还是要返回新列表不要让自己的函数语义模棱两可。7.4 用边界测试确认排序正确写完冒泡排序后建议至少用下面几类数据测试空列表[]只有一个元素的列表[7]已经升序排好的列表[1, 2, 3, 4]完全反序的列表[5, 4, 3, 2, 1]包含重复元素的列表[3, 1, 2, 3, 1]这些数据能快速暴露出边界条件错误、比较符号写反、重复元素处理不合适等问题。比如空列表和单元素列表如果代码没有使用长度变量正确控制循环很容易出现越界或返回异常结果。而反序列表则能验证最坏情况下排序是否完整重复元素则适合检查排序结果是否仍然正确。8. 最佳实践与工程建议8.1 把排序逻辑封装成函数不要在主流程中直接写三层循环除非你只是临时跑一次。把冒泡排序封装成独立函数可以让代码结构更清晰也方便单独测试和复用。函数命名建议使用bubble_sort参数名使用arr或items让读者一眼看出它处理的是一个序列。如果你希望函数不修改外部列表应在文档注释中明确写出来。如果写的是原地排序函数主流程中最好避免在排序后再次使用原列表中的相对顺序否则会造成逻辑混乱。保持“数据流向清晰”是编码规范中很重要的一环。8.2 关注时间复杂度与数据规模冒泡排序的时间复杂度是 O(n²)。在处理少量数据时这个复杂度影响不明显但当数据量达到几百上千时最高需要执行的比较次数会非常大。排序算法选择应结合数据规模来考虑不要因为学了冒泡排序就处处用它。如果明确要处理大量数据直接用内置的sorted()或list.sort()。如果只是演示算法细节可以把数据量控制在几十个元素以内这样输出过程也比较容易阅读。真正在生产环境里不建议手写冒泡排序来替代经过高度优化的内置排序。8.3 在函数中统一使用 Python 风格Python 风格的列表交换是a, b b, a不需要手动引入第三个临时变量。只要赋值语句左侧和右侧都是相同数量的表达式Python 会先计算右侧元组再按顺序赋值给左侧变量。这个小语法不仅能用在列表元素交换上也能用在两个普通变量的交换上。同时循环边界建议先判断清楚再写可以在函数开头写上注释比如“每轮结束后末尾 i 个元素已有序”。这种注释能帮助以后的维护者快速理解代码意图避免把外层循环范围单纯背成一个固定数字。代码不只要跑通还要让人容易读懂。8.4 给函数增加基本测试哪怕只是一个小算法也建议写一个简单的断言测试函数。断言不通过时程序会直接抛出AssertionError这比肉眼观察输出更可靠。下面是一个简单的测试写法def test_bubble_sort(): assert bubble_sort([]) [] assert bubble_sort([7]) [7] assert bubble_sort([3, 1, 2]) [1, 2, 3] assert bubble_sort([4, 4, 3, 3, 2]) [2, 3, 3, 4, 4] print(冒泡排序基础测试通过) if __name__ __main__: test_bubble_sort()上面测试中每次调用bubble_sort都会对传入的临时列表进行原地排序但由于传入的列表是字面量或临时创建的新列表所以不需要担心破坏外部数据。对于更复杂的项目可以改用unittest或pytest框架但这里用 Python 自带的assert已经足够验证排序正确性。8.5 善用调试输出如果某轮排序结果和预期不一致不要只看最终结果可以在内层循环里输出每次比较的两个值和交换后的列表。例如print(f比较 {arr[j]} 和 {arr[j 1]}, end ) if arr[j] arr[j 1]: print( 交换, end ) arr[j], arr[j 1] arr[j 1], arr[j] print(arr) else: print( 保持不变)这种方式能让你直接看到每一步的比较结果比单纯在纸上推演更容易找到代码中的逻辑错误。定位问题后记得删除调试输出避免污染正式结果。9. 进一步动手练习的方向代码已经能跑通并不意味着你已经完全掌握冒泡排序。建议你在本地新建一个 Python 文件写下基础版本和带swapped标记的优化版本再用不同的列表测试。你可以尝试把[6, 3, 8, 2, 7, 1, 9, 5]换成字符串列表比如[peach, apple, cherry, banana]观察冒泡排序能否按照字典顺序完成升序排列。接着可以尝试自己实现以下变体把列表改成降序排列只需要调整比较符号但要注意逻辑是否仍然正确。打印每一轮结束后的列表观察“每轮末尾元素确定”的过程。修改函数让它返回一个新列表而不是原地修改传入列表。使用random模块生成随机列表再与sorted()的结果做对比用断言确保结果一致。尝试把冒泡排序改成选择排序或插入排序比较三种算法在代码结构上的差异。当你看到自己写出的排序结果与sorted()完全一致时说明你已经掌握了列表排序中最基础的手写实现。下一步可以继续学习二分查找、快速排序、归并排序等更高效的算法这些都会用到今天反复练习的循环与交换思想。多说一句手写冒泡排序更适合入门和理解真正业务代码里遇到排序需求时请放心大胆使用 Python 内置的list.sort()或sorted()既省力又高效。
返回列表