Python冒泡排序:手把手实现列表升序排列

发布时间:2026/9/4 22:18:32
Python冒泡排序:手把手实现列表升序排列 Python 冒泡排序是很多人第一次接触排序算法时会遇到的主题它解决的是把一个 Python 列表按从小到大排列的需求也就是列表升序排列输入一个乱序列表输出一份有序列表。如果你刚学 Python想弄懂两层循环、相邻比较、元素交换这几件事这篇文章可以从最小例子带你走一遍如果你已经会调用内置排序只是想把手写冒泡的过程、边界和坑整理清楚也可以直接看后面几节。1. 先搞清楚“列表升序排列”要处理的是什么1.1 先把输入和输出定义清楚排序看起来简单但写代码之前最好先明确一件事升序排列到底要得到什么。比如说有一个列表nums [5, 2, 9, 1]升序排列之后目标结果是[1, 2, 5, 9]这组数本身没有重复值比较好理解。如果列表里出现了重复值比如nums [4, 1, 3, 1]排完之后应该是[1, 1, 3, 4]并不是严格递增而是“后面的元素不小于前一个元素”。平时说的升序在很多语言和场景里其实都是这个含义属于非降序。冒泡排序里相邻比较时相等元素不交换所以结果也是稳定的。输入方面这个排序函数接收一个列表列表里的元素必须能互相比较。数字可以字符串可以但如果把数字和字符串混在一个列表里排序就会遇到麻烦。这个后面会专门说。1.2 为什么这个算法叫“冒泡”冒泡排序的核心操作只有一个从左到右比较相邻的两个元素。如果前一个元素比后一个元素大就交换它们的位置。比较完一对再继续比较下一对。一轮扫完之后当前范围内最大的那个数就会被一步一步挪到最右边。这个过程很像气泡从水底慢慢往上浮所以叫冒泡排序。拿一个小的例子看[3, 1, 2]第一轮比较3 和 1 比较3 比 1 大交换变成[1, 3, 2]3 和 2 比较3 比 2 大交换变成[1, 2, 3]这时候最大值 3 已经到末尾了。第二轮只需要看剩下两个元素也就是[1, 2]1 和 2 不需要交换排序结束。有些教材会把这个过程描述成多轮冒泡每一轮都会有一个当前最大值冒到最右边。理解这一点之后再去看两层循环会更容易。1.3 冒泡排序和“找最大值”是同一件事每一轮比较的本质就是在没排好的那一段里找最大值然后把它放到这一段的最右边。第一轮负责整个列表找出最大值放到最后一位。第二轮负责前 n-1 个元素找出其中的最大值放到倒数第二位。这样逐轮缩小范围直到整个列表有序。这个思路是理解代码的关键。外层循环控制“还剩多少元素需要处理”内层循环才真正做相邻比较。2. 在 Python 里写出第一版冒泡排序2.1 环境准备不要卡在安装这一步写 Python 排序代码不需要很复杂的环境装好 Python 解释器再有一个能写代码的编辑器就够了。Windows、macOS、Linux 都可以。去 Python 官网下载安装包的时候Windows 用户记得勾选 Add Python to PATH这样命令行里直接用python命令才不会被系统拒绝。装好之后打开终端验证python --version能输出版本号说明基础环境没问题。编辑器方面刚开始可以用 IDLE也可以用 VS Code。如果用的是 VS Code需要先安装 Python 插件并确认右下角或命令面板里选择的解释器是刚安装好的那个 Python。这一步经常被忽略很多人代码写好了但运行不了原因不是代码错了而是解释器没选对。只要代码能在本地跑起来后面所有测试都很方便。2.2 把“相邻交换”翻译成两层循环第一版先写最基础的版本。功能很简单直接修改传入的列表把它变成有序的函数不返回值。def bubble_sort_asc(numbers): n len(numbers) for i in range(n - 1): for j in range(n - 1 - i): if numbers[j] numbers[j 1]: numbers[j], numbers[j 1] numbers[j 1], numbers[j]这是最典型的冒泡写法。外层循环的i表示已经处理了多少个“最大值”。比如i 0时还没有元素被固定到末尾这一轮要让整个列表的最大值到最后一个位置。i 1时最后一个位置已经确定就不需要再碰它。所以内层循环写成了range(n - 1 - i)。这个范围的意思是当前还需要参与比较的元素越来越少比较的次数也跟着减少。内层循环里j和j 1分别代表相邻位置。如果前一个比后一个大就用 Python 的多重赋值把它们交换numbers[j], numbers[j 1] numbers[j 1], numbers[j]这个写法比很多语言用临时变量方便很多。Java 或 C 写习惯了可能第一反应是temp numbers[j] numbers[j] numbers[j 1] numbers[j 1] temp在 Python 里不需要这么麻烦一行就能完成交换。2.3 运行之后怎么判断结果对不对写完之后用一个简单的列表试一下nums [5, 2, 9, 1] bubble_sort_asc(nums) print(nums)输出[1, 2, 9, 5]不对那就错了。正确输出应该是[1, 2, 5, 9]如果你看到[1, 2, 9, 5]就要检查内层循环范围是不是没减掉已经处理好的末尾元素。没有减的话虽然大的数也会向右移动但最后一轮比较次数多了一次逻辑已经不正确。如果你想在调试时看得更清楚可以在内层循环结束后加一行打印观察每一轮的效果for i in range(n - 1): for j in range(n - 1 - i): if numbers[j] numbers[j 1]: numbers[j], numbers[j 1] numbers[j 1], numbers[j] print(numbers)这个过程能直观看到每一轮谁被“冒”到了末尾也更容易排查是不是比较方向写反了。3. 原地修改和返回新列表两条路都要会写3.1 列表是可变对象函数里改列表会影响外部上面的第一版写法会直接修改传入的列表。也就是说nums [5, 2, 9, 1] bubble_sort_asc(nums) print(nums)执行完函数后原来的nums本身就变成了[1, 2, 5, 9]。这是因为 Python 里列表是可变对象函数接收的是列表的引用。函数内部对元素做修改外部看到的同一个列表也会变化。很多人第一次写到这里会有点蒙为什么函数没有return外部列表还是变了其实这不是 bug而是“原地修改”的特征。换一个角度想如果我想保留原始列表同时又想得到排序后的新列表直接用这个函数就会有问题。因为调用完之后原列表已经被改掉了。3.2 用列表切片复制一份再排序如果希望排序后原列表保持不变最常见的做法是复制一份再做操作。复制列表最简单的办法是使用切片arr numbers[:]numbers[:]会生成一个包含所有元素的新列表和原来的列表内容一样但是两个独立对象。对arr做修改不会影响numbers。那么函数可以改成这样def bubble_sort_asc(numbers): arr numbers[:] 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这个版本的函数不再直接修改传入列表而是返回一个排好序的新列表。使用方式也变了nums [5, 2, 9, 1] sorted_nums bubble_sort_asc(nums) print(sorted_nums) # [1, 2, 5, 9] print(nums) # [5, 2, 9, 1]列表切片在这里起到了复制作用。以后学列表切片时arr[:]是一个很常用的例子。3.3 两种写法什么时候选哪种如果只是学习、验证算法过程怎么写都行。但如果做一个小工具或者封装函数给别人用我更推荐“传入列表、返回新列表”的方式。因为调用方往往不希望自己的原始数据被悄悄改掉。比如你有一段数据是从文件读出来的后面还要用原始顺序做其他统计排序前就必须保留一份副本。相应地Python 内置排序也有两种形式sorted(data)返回新列表原列表不变。data.sort()就地排序原列表改变。手写冒泡时你也面临同样的选择。写函数前先想清楚是要原地排序还是要返回新列表。这两种需求会导致代码差别很大。如果函数内部复制了一份但是最后忘了写return arr调用方拿到的就是None。这是一个很常见的坑。4. 加一个“提前结束”标记小数据集立刻减少空转4.1 已经有序时基础版仍然在空转前面写的第一版有个明显问题不管列表是否已经有序外层循环都会固定跑n - 1轮。内层循环也会每轮都跑。比如输入是[1, 2, 3, 4, 5]这个列表本来就已经有序。但基础版仍然需要执行多轮循环每一轮都会把所有没处理完的元素重新比较一次。虽然比较过程中一次交换都不会发生但 CPU 的时间已经被消耗掉了。在小数据量上这点浪费无所谓。但如果你用一万个已经排好序的数字测试仍然会感受到明显的空转。优化思路很简单如果某一轮从头到尾没有任何一次交换说明当前序列已经全局有序了后面几轮不可能再产生新的有序变化可以直接退出。4.2 用一个布尔标记做优化在每轮循环开始前先定义一个swapped变量记录这一轮是否发生了交换。def bubble_sort_asc(numbers): arr numbers[:] 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注意swapped False要放在外层循环内部每一轮开始前都重置一遍。如果这一轮内层循环发生了交换说明还没有完全排好需要继续下一轮。如果整轮下来swapped仍然是False就说明比较过程中没有出现“左边比右边大”的情况后面的顺序已经正确用break跳出外层循环。4.3 加上之后性能有什么区别直观判断标准是看“有序输入”的处理速度。对于一个已经有序的列表第一版会跑完所有外层轮次比较次数接近n * (n - 1) / 2。优化后的版本第一轮比较n - 1次发现没有任何交换立即退出。所以最理想情况下优化后的冒泡排序时间复杂度可以到 O(n)也就是只扫一遍就结束。如果输入是完全逆序的比如[5, 4, 3, 2, 1]那么每轮都会发生交换优化标记并不会减少最坏情况下的比较次数整体仍然是 O(n²)。我一般建议初学者这样处理第一遍先写没有优化的版本因为逻辑直白容易理解。等跑通了再在这个基础上加swapped标记对比一下两次实现。不要把优化放在最开始否则你会分不清是排序逻辑没懂还是标记位置写错了。5. 想不到的边界条件空列表、单元素、重复值和混合类型5.1 边界用例其实并不难很多排序代码在常规数据上没问题一到边界就翻车。冒泡排序算比较温和的空列表和单元素列表都不会报错因为外层循环一开始就满足不了函数会直接返回。下面这张表可以作为固定测试用例输入期望输出说明[][]空列表不需要排序[7][7]单个元素天然有序[3, 3, 3][3, 3, 3]重复元素不交换[3, 1, 2, 2][1, 2, 2, 3]有重复值要保持稳定[-2, 10, 0][-2, 0, 10]负数也可以正常比较[1, 2.5, 0][0, 1, 2.5]整数和浮点数可以混合排序[banana, apple, Cherry][Cherry, apple, banana]字符串按字典序排序字符串排序时要注意Python 区分大小写Cherry会排在apple前面因为大写字母的编码比小写字母靠前。这一点和很多业务场景里的“忽略大小写排序”不一样不要直接认为 Python 的字符串排序会自动忽略大小写。5.2 输入不是列表怎么办如果函数参数是元组或者某个可迭代对象怎么办比如data (5, 2, 3)元组本身不可修改不能用原地排序。但如果你希望函数支持元组输入并返回一个排好序的列表可以在函数开头加一行arr list(numbers)list()会把元组、range、列表等可迭代对象转成新的列表。原先传入的列表也会被复制不会影响原来对象。不过有一个点要提醒如果传入的是字符串list(cba)会转成字符列表[c, b, a]排序后得到[a, b, c]。这到底是不是你想要的取决于业务需求。写第一版的时候不用把所有类型都兼容。先把“输入是列表元素可比较”这个假设说清楚。等代码稳定后再去扩展输入类型兼容。5.3 元素类型混在一起会报 TypeError如果一个列表里既有数字又有字符串[1, a, 2]排序时 Python 会尝试比较1 a最后抛出TypeError: not supported between instances of int and str这个报错不是代码写错了而是输入数据本身不支持比较。数字和字符串之间没有统一的“谁大谁小”规则。遇到这种错误时不要急着改排序算法先检查列表里的元素类型。可以用一个简单办法快速判断print(set(type(x) for x in data))如果输出结果里有两种或以上类型说明问题出在数据本身。稳妥的做法是在输入阶段就把类型统一或者在排序前校验。6. 用断言和随机数据验证排序结果6.1 固定用例不要只看打印写完排序函数最容易犯的错误是肉眼判断输出。数据少还好数据一多就会看漏。更好的办法是写断言。Python 的assert可以帮你自动判断条件是否成立def test_bubble_sort_asc(): assert bubble_sort_asc([]) [] assert bubble_sort_asc([7]) [7] assert bubble_sort_asc([5, 2, 9, 1]) [1, 2, 5, 9] assert bubble_sort_asc([4, 1, 3, 1]) [1, 1, 3, 4] assert bubble_sort_asc([3, 3, 3]) [3, 3, 3] assert bubble_sort_asc([banana, apple]) [apple, banana] print(基本用例全部通过)如果函数行为符合预期脚本会正常结束并打印内容。如果某个用例没过assert会直接抛出异常中断程序。这样比盯着一堆输出判断快很多。写assert的时候要注意如果排序函数返回的是None而你把返回值拿去和列表比较断言会一直失败。这也是很多新手写测试时发现“怎么都对不上”的原因。6.2 随机数据循环测试很管用固定用例只能覆盖你已经想到的情况。想要更放心可以生成随机数据然后把结果和 Python 内置sorted()比较。import random def test_random(): for _ in range(1000): raw [random.randint(-100, 100) for _ in range(random.randint(0, 30))] result bubble_sort_asc(raw) assert result sorted(raw), (raw, result) print(随机数据测试通过)这组代码循环 1000 次每次生成长度随机、元素随机的列表。如果手写冒泡排序结果和内置排序结果不一致程序会在出问题的那一次停下来并把原始数据和结果打印出来方便排查。随机测试覆盖的数据形态比较丰富能发现很多固定用例发现不了的问题。如果你用的是“返回新列表”版本还可以顺便验证原始列表有没有被修改before raw[:] result bubble_sort_asc(raw) assert raw before这样可以同时检查两件事排序结果是否正确原列表是否保留。6.3 只判断“相邻是否升序”也可以如果不想依赖内置sorted()也可以只检查结果本身的顺序def is_sorted_asc(arr): return all(arr[i] arr[i 1] for i in range(len(arr) - 1))这个函数专门检查相邻元素是否满足升序关系。但有一点要注意只检查“升序”还不够因为结果如果少了一个元素、多了一个重复元素也可能满足相邻升序。所以最严谨的验证包含两层含义结果是有序的。结果是原来那一组元素数量不变组成不变。用sorted(raw)作为对照正好同时满足这两个条件。7. 写出结果之后再回头排掉这几个常见坑7.1 列表没变或者输出成了 None常见情况有三种函数内部复制了列表但没有return调用方只看到None。调用方把返回值赋给变量但函数根本没有返回值。函数内部用arr numbers[:]排了序却没返回外部拿不到结果。排查时先看函数签名和最后一行。如果你的版本是“返回新列表”最后一行必须是return arr如果你的版本是“原地修改”调用时不要写result bubble_sort_asc(nums)因为原地修改版本没有返回值result会是None。正确的做法是bubble_sort_asc(nums) print(nums)先确认自己到底要哪一种再去看代码逻辑。7.2 出现 IndexError大概率是内层循环范围错了如果报错信息类似IndexError: list index out of range先检查这一行numbers[j 1]如果内层循环写了range(n)那么当j走到n - 1时j 1就变成n已经超过列表结尾。正确范围是range(n - 1 - i)也就是不断缩小比较范围。这样每次访问的都是当前范围内相邻的两个元素不会越界。7.3 结果是降序或者一部分位置不对如果你得到的输出是从大到小说明比较方向写反了。升序排列的核心规则是前一个元素大于后一个元素时交换也就是把大的向右移动。前一个元素小于后一个元素时不交换。所以代码里的判断应该是if numbers[j] numbers[j 1]:如果写成了那就是发现前一个更小才交换大的会不断向左移动最后变成降序。7.4 别把内置排序和手写排序搞混Python 里常用的排序方式有两个sorted(data) # 返回新列表 data.sort() # 原地排序返回 None加上手写的bubble_sort_asc一共可能会在代码里出现三种排序写法。关键区别在于写法原列表是否改变返回值适合场景sorted(data)不变新列表需要保留原列表时data.sort()改变None原列表本身可以修改时手写原地冒泡改变None学习算法过程手写返回新列表不变新列表学习并需要保留原始数据写测试时如果混用了这些方式很容易出现“我明明排了序怎么还是乱的”的情况。比如result data.sort()这时候result是None因为sort()是原地修改。而result sorted(data)result才是排好序的新列表。理解了这个差别很多排序相关问题都能解决。7.5 排序太慢先看数据规模冒泡排序的时间复杂度是 O(n²)数据量一大就会非常慢。如果你测试 Python 列表排序数据量从一万变到十万内置排序可能只需要零点几秒手写冒泡可能要跑到无法接受。这不是 Python 的问题也不是代码没写对而是冒泡排序本身不适合处理大规模数据。第一版不加优化的时候1 万个整数已经够让人感觉到明显耗时加上提前结束标记后随机数据仍是 O(n²) 级别优化幅度有限。所以实际项目中需要排序首选内置sorted()或list.sort()。手写冒泡更多是学习价值用来理解比较、交换、循环和算法复杂度。写到这里我想留一个建议不要急着把冒泡排序扩展成很复杂的版本也不要觉得学会了冒泡就能解决所有排序问题。我更推荐先把最简单的版本跑通把原地排序和返回新列表的区别搞清楚再补上边界测试最后理解为什么需要提前结束标记。走过这一圈之后再看选择排序、插入排序以及更快的排序算法你会顺很多。排序这个主题里最值钱的不是背下来代码而是能说清楚每一步在做什么。