Python冒泡排序详解:从零实现列表升序排列
2026/9/4 17:48:33 网站建设 项目流程

很多人在学习 Python 时都会遇到一个困惑:明明一个sort()方法就能解决的问题,为什么还要去学冒泡排序?如果只看表面,确实容易产生这样的误解。但冒泡排序在计算机基础教育中的地位,绝不是因为它能“更高效地排序”,而是因为它提供了一种最直观的方式,让你理解算法设计中最核心的思维:循环嵌套、相邻比较、逐步交换。

如果你正准备面试初级开发岗位,或者在准备计算机基础考试,又或者你正处在“能写代码但不懂原理”的瓶颈期,这篇文章会用最清晰的拆解方式,带你从头实现一个用冒泡排序对 Python 列表进行升序排列的完整案例。更重要的是,我会在代码之外,讲清楚它为什么是学习算法的“第一课”,以及它适合什么场景、不适合什么场景。

1. 在写排序代码之前,先理解你到底在解决什么问题

排序是计算机程序设计中最基础、最普遍的问题之一。无论你是在做数据采集、Web 后端开发,还是数据分析脚本,几乎都会碰到需要对一组数据重新排列的需求。

在 Python 中,如果你只是想要一个结果,直接写sorted(my_list)或者my_list.sort()就可以得到升序结果。既然如此,为什么我们还要专门研究冒泡排序的实现?

这里需要澄清一个关键认知:调用内置排序函数,和掌握排序算法的思维,是两件不同的事情。

内置的sorted()函数固然高效,它在底层用到了 Timsort 算法,综合时间复杂度和空间占用都比冒泡排序优秀太多。但它对你来说是一个“黑盒”。当你将来需要面对性能调优,或者需要在某些受限环境下自己实现排序逻辑,或者是在面试中被要求手写基础算法时,你是否还能做到心中有数?

冒泡排序的价值正好体现在这里:它的算法过程极为直观,逻辑链条非常短。如果你能把这个经典算法吃透,后续再看快速排序、归并排序、堆排序等进阶排序,都会更容易建立起自己的知识坐标系。

回到标题的场景。我们的任务是“将一个 Python 列表中的数字元素按从小到大,也就是升序排列”。举例来说,列表[64, 34, 25, 12, 22, 11, 90]经过排序后应该变成[11, 12, 22, 25, 34, 64, 90]

接下来我会从列表的基本概念出发,逐步把冒泡排序的完整实现过程逐一拆解。

2. 冒泡排序的核心概念与算法原理

2.1 什么是列表

Python 中的列表(list)是一种可变的、有序的数据集合。所谓“有序”,指的是每个元素都有一个确定的位置索引;所谓“可变”,指的是我们可以直接修改列表中的元素,包括增删改查。

numbers = [64, 34, 25, 12, 22, 11, 90]

我们可以通过索引访问元素,比如numbers[0]得到64numbers[3]得到12。也可以直接给某个位置赋值,比如numbers[0] = 100,这样列表就会变成[100, 34, 25, ...]

列表这种“可以直接通过索引修改元素”的特性,是冒泡排序能够实现的基础条件之一。

2.2 冒泡排序的基本思想

冒泡排序的英文名是 Bubble Sort,它的核心思想非常贴近它的名字:每一轮比较都像气泡上浮一样,把当前未排序区域中的最大元素“浮”到这一轮区域的末尾。

具体过程描述如下:

  1. 从列表的第一个元素开始,依次比较相邻的两个元素。
  2. 如果前一个元素大于后一个元素,就交换它们的位置。
  3. 对每一对相邻元素做同样的操作。经过第一轮全部比较后,列表中最大的元素就会像气泡一样被移动到最后一个位置。
  4. 继续对前面剩下的 N-1 个元素重复上述比较操作。
  5. 每一轮都能确定一个元素最终位置。总共需要执行 N-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,5 大于 2,交换位置,列表变为[1, 4, 2, 5, 8]
  • 比较 5 和 8,5 小于 8,不交换,列表保持不变

第一轮结束,最大的元素 8 已经到了最后位置。

有读者可能会问:“每一轮比较的次数为什么是递减的?”原因是每一轮都确定了当前区域的最大值,这个值已经位于正确位置,下一轮排序就不需要再与它比较了。这个“轮次逐渐缩小比较范围”的细节,恰恰是新手写冒泡排序最容易犯错的点。很多人写出的代码前几轮没问题,后面就会因为多比较了已经排好序的元素而出错,或者是索引越界。

2.3 冒泡排序与其他排序算法的直观对比

排序算法平均时间复杂度额外空间排序稳定性是否适合大列表
冒泡排序O(n²)O(1)稳定不适合
选择排序O(n²)O(1)不稳定不适合
插入排序O(n²)O(1)稳定小规模时可用
快速排序O(n log n)O(log n)不稳定适合
归并排序O(n log n)O(n)稳定适合
Python 内置 sortO(n log n)O(n)稳定生产中首选

从表中可以看到,冒泡排序的最大优势其实不是效率,而是易于理解和实现。它适合教学、适合作为算法入门,但在生产环境的大规模数据排序场景中,并不推荐直接使用。

3. 环境准备:开始编写 Python 冒泡排序代码

很多人提到 Python 开发,总觉得要先把环境配置得很复杂才能开始。但下面这份是学习冒泡排序之类的算法时最合适的配置:电脑上装一个 Python 解释器,外加一个能写代码的编辑器。

如果你还没有安装 Python,可以去 Python 官方网站下载对应你操作系统的安装包。安装步骤相对直观,但需要注意的是:在 Windows 安装过程中,有一个“Add Python to PATH”的复选框,务必勾选上,否则后续在命令行运行 Python 命令时会找不到解释器。安装完成后,可以在终端里运行下面这段命令验证:

python --version

或者在某些 Linux 系统中可能需要用 python3 命令:

python3 --version

输出结果类似Python 3.10.xPython 3.11.x之类的版本号,就说明环境已经可用。本文代码适配 Python 3.6 以上所有版本,写法不依赖任何高级语法特性,所以无论你电脑装的是 Python 3.8 还是 Python 3.12,都能正常运行。

编辑器方面没有强制要求。对于刚接触算法的朋友,推荐在这三个阶段逐步改善:

  1. 起步阶段:使用简单的文本编辑器和命令行。
  2. 熟悉阶段:使用 VS Code 这类轻量级编辑器。
  3. 项目阶段:使用 PyCharm 或 VS Code 配合专业插件。

本文所有代码都不复杂,完全可以使用基础环境来运行和验证。最简单的执行方式,是将代码写在一个.py文件中,然后在文件所在目录执行:

python bubble_sort.py

也可以直接进入 Python 交互式环境,一行一行地试验细节。

4. 冒泡排序实现流程拆解

在正式开始写代码前,先把这个算法的实现过程从“描述”变成“步骤”。这能帮助我们避免写代码时一头扎进循环细节,最后却忘了整体结构。

4.1 第一步:明确外层循环的作用

外层循环控制的是“排序轮数”。对于一个长度为 n 的列表,最多只需要进行 n-1 轮比较。

为什么是 n-1?因为每进行一轮,都会有一个元素被放到正确位置。比如 n 个元素,前面 n-1 个元素都归位了,剩下的最后一个元素自然也在正确位置。

for i in range(len(numbers) - 1): # 每一轮内部比较

变量 i 这里可以理解为“已经完成排序的元素个数”。i 越大,说明末尾已经有 i 个元素不需要再动。

4.2 第二步:明确内层循环的作用

内层循环负责“相邻比较”。在每一轮排序中,需要从列表开头开始,一直比较到“未排序区域的倒数第二个位置”。

以内层第 i 轮为例,未排序区域是从索引 0 到len(numbers) - 1 - i。需要比较的相邻元素对中,最后一个比较对应该包含索引len(numbers) - 1 - i - 1len(numbers) - 1 - i

所以内层循环的写法是:

for j in range(0, len(numbers) - 1 - i): # 比较 numbers[j] 和 numbers[j+1]

这是整个实现里最需要理解透彻的一行。索引范围写错,程序要么越界报错,要么多比较了已经确定位置的元素,导致排序结果不正确。

4.3 第三步:确立比较和交换逻辑

有了索引和范围,接下来就是算法的心脏部分:

if numbers[j] > numbers[j + 1]: numbers[j], numbers[j + 1] = numbers[j + 1], numbers[j]

这里的numbers[j], numbers[j + 1] = numbers[j + 1], numbers[j]是 Python 特有的多重赋值语法。计算机底层执行这个操作时,会先把右侧的两个值计算出来,然后用一个临时变量完成交换。它本质上等价于其他语言中常见的写法:

temp = numbers[j] numbers[j] = numbers[j + 1] numbers[j + 1] = temp

Python 的多重赋值写法更简洁,但对于刚接触编程的读者来说,理解成底层的三步交换逻辑会更有帮助。

这里真正容易踩坑的地方是:如果你用多重赋值时把等号左右两边写反,比如写成numbers[j + 1], numbers[j] = numbers[j], numbers[j + 1],其实结果是一样的。真正要注意的是不要写成numbers[j] = numbers[j + 1]这种单行赋值,这样会造成元素覆盖丢失。

4.4 第四步:确认排序方向

题目要求升序排列,所以我们使用“如果前一个元素大于后一个元素,就交换”。如果要求降序,只需要把判断条件中的>改成<即可。

5. 完整示例代码实现

代码的组织方式会影响你的可读性和复用性。下面给出一个基础版、一个装饰优化版和一个双向冒泡版,每个版本都附有运行方式说明。

5.1 冒泡排序基础版

这是最标准的实现,也是面试和考试中最常出现的形态。

创建文件bubble_sort.py

def bubble_sort(arr): """ 使用冒泡排序对列表进行升序排列(原地排序) 参数: arr: 包含可比较元素的列表 返回: 无,直接修改原列表 """ n = len(arr) # 外层循环控制轮数,共需要 n-1 轮 for i in range(n - 1): # 内层循环负责比较相邻元素 # 每轮需要比较的元素数量逐渐减少 for j in range(0, 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] print("排序前:", numbers) bubble_sort(numbers) print("排序后:", numbers) assert numbers == [11, 12, 22, 25, 34, 64, 90], "排序结果错误"

代码里最关键的是range(0, n - 1 - i)这个范围控制。第一轮 i=0 时,内层比较到 n-1,覆盖到最后一个元素;第二轮 i=1 时,已经排好的最后一个元素不再参与比较,内层只需要比较 n-2 对;以此类推。

运行方式:

python bubble_sort.py

预期输出:

排序前: [64, 34, 25, 12, 22, 11, 90] 排序后: [11, 12, 22, 25, 34, 64, 90]

5.2 提前终止优化版

基础版中存在一个效率问题:如果一个列表在比较两三轮后已经有序,剩余轮次完全不会发生交换,但仍然会把循环执行完。优化思路是引入一个标志位swapped,如果某一轮没有发生任何交换,说明列表已经有序,可以直接终止循环。

创建文件bubble_sort_optimized.py

def bubble_sort_optimized(arr): """ 冒泡排序的优化版本: 引入 swapped 标志位,如果某一轮没有发生交换, 说明列表已经有序,可以提前退出。 """ n = len(arr) for i in range(n - 1): swapped = False # 内层循环仍然控制相邻比较 for j in range(0, 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__": test_list = [1, 3, 5, 7, 9] print("排序前:", test_list) bubble_sort_optimized(test_list) print("排序后:", test_list)

这个版本特别适合基本有序的数据。

第二个变量问题在于,假如列表刚开始就是有序列表,第一轮扫描一遍后swapped仍然是 False,程序直接退出。因此最佳情况的时间复杂度从 O(n²) 降低到了 O(n)。

5.3 双向冒泡排序版本

双向冒泡排序,也被称为鸡尾酒排序。它的思路是在每一轮遍历中,不仅把最大值“沉”到右侧,也同时把最小值“浮”到左侧。这样左右两侧都能逐步形成有序区间,效率上会比单方向冒泡稍好一些。

创建文件bubble_sort_cocktail.py

def cocktail_sort(arr): """ 双向冒泡排序(鸡尾酒排序): 先从左往右冒泡确定一个最大值放在右侧, 再从右往左冒泡确定一个最小值放在左侧。 交替进行,直到全部有序。 """ n = len(arr) left = 0 right = n - 1 while left < right: # 从左到右的冒泡:最大值移动到右侧 for i in range(left, right): if arr[i] > arr[i + 1]: arr[i], arr[i + 1] = arr[i + 1], arr[i] right -= 1 # 从右到左的冒泡:最小值移动到左侧 for i in range(right, left, -1): if arr[i - 1] > arr[i]: arr[i - 1], arr[i] = arr[i], arr[i - 1] left += 1 if __name__ == "__main__": data = [24, 3, 56, 1, 78, 45, 12] print("排序前:", data) cocktail_sort(data) print("排序后:", data)

这段代码展示了冒泡排序的一个变种,帮助打开思路。不过在日常场景中,最常用的仍然是基础版和带标志位的优化版,双向冒泡通常只作为理解扩展。

6. 运行结果与效果验证

编写代码只是第一步,更关键是学会如何验证你写的排序算法是正确的。

6.1 使用断言验证

在上面的代码中,已经用assert numbers == [11, 12, 22, 25, 34, 64, 90]进行了自动校验。断言是一种非常轻量的验证方式,如果排序结果不符合预期,程序会抛出AssertionError异常,这样能在开发阶段就发现问题。

6.2 使用随机数据验证

固定数据只能验证特定的情况。你还可以用 Python 内置的random模块生成随机列表,来测试算法的通用性。

import random def bubble_sort(arr): n = len(arr) for i in range(n - 1): for j in range(0, n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] # 生成 100 组随机列表进行测试 for _ in range(100): random_list = [random.randint(0, 1000) for _ in range(50)] expected = sorted(random_list) bubble_sort(random_list) assert random_list == expected print("全部测试通过")

这种“使用内置排序函数作为基准答案”的验证方法,在平时练习算法时非常高效。它可以帮你快速确认你的算法逻辑和标准结果是否一致。

6.3 运行失败时的第一排查方向

如果排序结果不正确,优先检查这几个地方:

  1. 内层循环范围:检查for j in range(0, n - 1 - i)中的n - 1 - i是否写成了n - i。如果范围过大会导致索引越界,过小则会导致尾部元素没参加比较。
  2. 交换方向:确认arr[j] > arr[j + 1]是升序条件。如果这里写反了,排序方向也会相反。
  3. 原列表污染:如果你在测试时候把原列表和期望列表做了别名引用,修改一个会同时影响另一个。注意在 Python 中expected = random_list不是复制,而是新引用。

7. 冒泡排序在真实场景中的表现与性能边界

在文章前半部分,我们一直在强调冒泡排序适合教学。但作为技术作者,还需要给读者一个更客观的使用建议:在真实项目中,冒泡排序适合什么场景,不适合什么场景。

7.1 冒泡排序适合哪些场景

首先,数据规模很小的时候,比如列表长度在几十以内,冒泡排序的性能劣势并不明显。此时最大的瓶颈往往不是排序本身的耗时,而是代码的可读性和开发效率。如果你的目标是给代码阅读者展示一个清晰的排序逻辑,冒泡排序是完全可以接受的。

其次,在实际项目中遇到“基本有序”的数据时,可以选用带有swapped标志位的优化版冒泡排序。最佳情况下它的时间复杂度可以降低到 O(n)。例如,你要对外部传入的数据做异常值重排,已知大部分数据本来就有序,那么这种优化版本会表现得不错。

另外,在嵌入式系统或其他内存极受限的环境中,很多高级排序算法因为需要额外的空间开销而不可用。冒泡排序的额外空间复杂度是 O(1),也就是只需要一个临时变量。对于这类场景,实现简单、空间需求低这一点就显得比较珍贵。

7.2 冒泡排序不适合哪些场景

当列表长度达到几千或几万以上时,冒泡排序的 O(n²) 时间复杂度会造成明显的性能瓶颈。以 10000 个元素为例,在最坏情况下内层循环大约需要执行 5000 万次比较,在普通计算机上会产生肉眼可感受到的延迟。

生产环境推荐使用 Python 内置的list.sort()方法,它是由 C 语言实现的 Timsort 算法。这种算法在真实数据上的表现显著优于冒泡排序。

7.3 三种排序的实测路径

如果你对排序性能有疑问,可以自己写一段测试代码来看不同算法的时间差异:

import random import time def bubble_sort(arr): n = len(arr) for i in range(n - 1): for j in range(0, n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] data = [random.randint(0, 10000) for _ in range(3000)] start = time.time() bubble_sort(data) end = time.time() print(f"冒泡排序耗时: {end - start:.4f} 秒")

如果你有兴趣,可以用同样的方式测试sorted(data)的耗时,差异会非常明显。不过这不代表冒泡排序“没有用”,而是在提醒我们,选对算法和场景同样重要。

8. 冒泡排序中的常见问题与排查方法

在带初学者写这个算法时,有几个问题频繁出现。整理成表格,方便你对照排查。

问题现象可能原因排查方式解决方案
程序报 IndexError: index out of range内层循环范围使用了range(n)查看循环范围内最后一次索引取值将内层范围改为range(n - 1 - i)
排序后最大的元素在最后一位,但前面的元素仍然乱序外层循环轮数不足range(n)改成range(n-1)或反过来外层应为range(n - 1)
输出结果是降序而不是升序比较符号方向写反检查 if 条件,打印每次交换前的两个数if arr[j] < arr[j+1]改为if arr[j] > arr[j+1]
原列表被修改,调用方不想要这个副作用排序函数是原地修改确认传入参数和返回值引用关系排序前使用arr.copy()创建副本
字符串、数字混排报错Python 不同数据类型无法比较大小检查列表中元素类型是否一致保持列表元素类型统一
使用return sorted(arr)得到新列表,原列表没变需要区分内置方法和自定义函数行为检查代码是否误用了内置语句如需修改原列表需要显式赋值

8.1 关于函数是原地排序还是返回新列表

这个话题在很多 Python 技术讨论中被反复提起。内置方法中,list.sort()是原地修改列表并返回Nonesorted(list)是返回一个新列表,原列表不变。自己实现冒泡排序时也需要明确设计意图。

上面的示例中,bubble_sort()是原地修改列表。如果希望保持原列表不变,返回一个排序后的新列表,可以做一层复制:

def bubble_sort_safe(arr): # 创建列表副本,避免影响原始数据 new_arr = arr.copy() n = len(new_arr) for i in range(n - 1): for j in range(0, n - 1 - i): if new_arr[j] > new_arr[j + 1]: new_arr[j], new_arr[j + 1] = new_arr[j + 1], new_arr[j] return new_arr

使用arr.copy()是很必要的细节,不然只是new_arr = arr的话,两个变量指向同一个底层对象,任何修改都会同步到原列表上。这个知识点也是 Python 学习中的高频混淆点,建议专门做一次实验来加深印象。

8.2 列表中出现混合类型元素怎么办

初学者往往会尝试给同一个列表放入多种类型,比如[3, "hello", 2.5]。当冒泡排序执行到比较3 > "hello"这一步时,Python 会抛出TypeError。核心原因是数字和字符串之间没有统一的比较规则。这一层限制不是冒泡排序独有的,Python 内置排序也一样不支持混排。解决起来也很简单:在构造数据时校验类型,或在排序前对其他类型做过滤和映射。

9. 冒泡排序的工程应用与最佳实践

虽然生产级别的大数排序一般不建议使用冒泡排序,但了解它的工程实践边界,对于培养算法思维非常有帮助。

9.1 利用“提前终止”处理近乎有序的数据

系统日志中,时间戳记录通常是基本有序的,只有少量乱序数据可能因为网络延迟等原因插入到错误位置。此时你用优化版冒泡排序有可能运行得非常快,因为第一轮或前几轮就完成了排序,整个算法只会扫描很少次数就退出。

不过要强调的是,这种场景并不是只能用冒泡排序解决。Timsort 这类算法同样对局部有序数据优化得很好。真正值得借鉴的是这样一种判断能力:发现输入数据具有某些特征时,选择与之匹配的算法。

9.2 算法复杂度分析可以解释为什么实测结果差很远

冒泡排序的时间复杂度:

  • 平均时间复杂度:O(n²)
  • 最坏时间复杂度:O(n²),发生在列表完全逆序时
  • 最优时间复杂度:O(n),发生在列表本身就有序且使用优化版时
  • 空间复杂度:O(1),只需要一个临时变量用于交换
  • 稳定性:稳定。在冒泡排序中,如果两个元素相等,它们的位置不会被交换,这保持了它们在原列表中的相对顺序。

在 Python 中,稳定性这个概念一般不会成为痛点,但在数据库中它会体现为“当排序依据的 key 相同时,是否维持原记录的先后顺序”。如果你正在理解数据库 ORDER BY 的排序逻辑,这个知识点会比价有帮助。

9.3 通过冒泡排序建立“测试驱动”的自信

很多人学算法,看完代码认为自己懂了,但一运行就报错。冒泡排序是一个非常适合进行“测试驱动”练习的启动对象。

基本思路是写一个可以处理任何随机列表的测试函数,只有测试通过才算真正掌握,而不是“看着代码觉得应该没问题”。这个习惯会影响你的整个编程生涯。

import random # 你可以把任意版本的冒泡排序函数传进来进行测试 def verify_sort_function(sort_func): for _ in range(200): arr = [random.randint(-100, 100) for _ in range(random.randint(1, 30))] expected = sorted(arr) sort_func(arr) if arr != expected: print("错误案例:", arr) print("期望结果:", expected) return False return True

这个验证模式放在日常工作中同样适用。无论你写一个算法还是写一个业务模块,先想清楚如何验证正确性,再开始写实现,是更高效率的工作方式。

9.4 从冒泡排序延伸到其他内容

如果你想持续深入学习 Python 和算法,以下几个方向会非常自然:

  • 用冒泡排序理解时间复杂度和空间复杂度的概念
  • 用交换两个元素的技巧理解 Python 变量引用
  • 用浅拷贝和深拷贝理解为什么arr = old_arr不能创建独立副本
  • 用列表推导式生成测试数据,对比分析不同排序算法的性能

在 Python 中,列表还有其他丰富的应用场景和操作,比如列表切片、列表与元组和集合的相互转换、两个列表合并成字典等。这些主题和冒泡排序完全不冲突,它们都为同一个目标服务:让列表操作更加高效、准确、优雅。

10. 总结与下一步建议

冒泡排序实现列表升序排列,这个需求看起来简单,但沉淀下来的算法思想非常值得反复咀嚼:通过相邻元素的比较与交换,把局部无序逐步转化为全量有序;通过外层循环控制区间边界,通过内层循环执行具体交换;通过标志位的引入,让算法具备对有序输入的敏感度。

通过这些细节你会发现,算法并不神秘,它本质上是在回答一个问题:如何用最少的操作,完成一个明确的目标。

如果你刚入门 Python,建议你按这个路径练习:

第一步,把文中的基础版代码手动在本地跑通,试着把列表换成你身边真实存在的数据,比如成绩列表、价格列表。

第二步,修改比较符号,将升序改成降序,再运行一次,体会符号变化带来的行为差异。

第三步,增加一个提前终止标志位,手动设计一个基本有序的列表,观察程序执行了几轮退出。

第四步,尝试将你的冒泡排序函数封装成一个工具模块,供其他脚本导入使用。

这样一轮练习下来,你掌握的就不只是冒泡排序这个知识点,而是一套处理列表排序问题的底层分析能力。以后无论是学习更高效的排序算法,还是应对面试中反复出现的算法题,你都会有一个扎实的起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询