拓冰建站拓冰建站
首页 / 资讯中心 / 正文

冒泡排序双层循环边界详解:从过程追踪到Python与C语言实现

冒泡排序大概是很多学生遇到的第一道“看着会一写就废”的算法题。在选修一《数据与数据结构》5.3 节里第1课时通常会把冒泡排序的思想讲清楚重复比较相邻元素、逆序就交换、每一轮把最大值或者最小值“冒”到数组末尾。课堂上用几张卡片一摆学生很快就说“懂了”。但真到了第2课时让他们在电脑上写出代码不少人却会卡住而且卡的点几乎一模一样外层循环该循环几次内层循环的上界为什么不是 n为什么我写的代码运行结果越界了这一节的真正难点不在“冒泡”的想法而在“把想法翻译成双层循环结构”。本文想围绕 5.3 冒泡排序2给出一个完整可用的教学与自学思路先从一轮排序的追踪过程出发建立轮次与下标的对应关系再推导出双重循环边界然后给出 Python 和 C 语言的完整实现、三种常用优化方法、课堂练习设计以及高频报错的排查思路。无论你是正在备课的信息技术教师还是准备学考、刚接触数据结构的学生都可以直接对照使用。1. 这篇文章真正要解决的问题先说结论第2课时最重要的教学目标是让学生完成一次思维转换——从“用手比划排序过程”转换到“用循环变量表达轮次和下标的规律”。很多学生能口头描述冒泡排序“每一次把大的往后放下一轮就不用管最后那个了。”这句话听着简单但转化为代码时他需要回答三个问题一共需要几轮比较每一轮内部需要比较多少次当前比较的两个数下标分别是 j 和 j1还是 i 和 j这三个问题只要有任何一个答错程序要么越界要么排序结果不对。第1课时偏重“看懂过程”第2课时偏重“写出代码并验证”中间的桥梁就是双层循环。所以本文要解决的是三个层面的问题理解层面为什么冒泡排序需要两层循环外层和内层各自管什么实现层面如何从排序过程追踪表推导出range(n-1-i)这样的循环边界应用层面如何写出正确代码、如何优化提前结束、如何排查常见错误文章读者包括两类一是信息技术教师可以直接把这里的追踪表、代码和练习迁移到课堂二是正在学习《数据与数据结构》选修课、准备算法与程序设计相关内容的学生可以按文章顺序逐步上机验证。2. 冒泡排序基本思想回顾与衔接在进入第2课时之前需要先回顾第1课时已经建立的认知基础。冒泡排序的基本思想可以概括为对一个包含 n 个元素的序列从第一个相邻元素对开始依次比较每一对相邻元素如果它们的顺序不符合要求比如从小到大排序时前面的数大于后面的数就交换它们的位置。第一轮结束后序列中值最大的元素会被交换到最后一位就像气泡浮到水面一样。然后开始第二轮这时最后一位已经“就位”不需要再参与比较。这里有几个关键认知学生在第1课时容易形成但未必能表达清楚每一轮只能确定一个元素的最终位置所以 n 个元素最多需要 n-1 轮。第 1 轮比较 n-1 次第 2 轮比较 n-2 次第 i 轮比较 n-i 次。如果某一轮没有发生任何交换说明序列已经有序可以提前结束。为了衔接我们先看一个经典教学数据[64, 25, 12, 22, 11]。这个数据故意设计成完全逆序方便观察每一轮交换的过程。第一轮的全部比较过程如下表所示比较次数比较的元素对是否交换交换后数组第1次64 和 25交换[25, 64, 12, 22, 11]第2次64 和 12交换[25, 12, 64, 22, 11]第3次64 和 22交换[25, 12, 22, 64, 11]第4次64 和 11交换[25, 12, 22, 11, 64]第一轮结束64 被“冒”到了最后一位。观察这张表可以发现一个规律每一轮中参与比较的两个元素它们的下标始终是连续的而且比较次数依次递减。这个规律正是后面构造循环边界的关键。第1课时如果能把这张表讲透第2课时学生理解代码就会顺畅得多。3. 从过程演示到代码实现双重循环的建立从第1课时的“过程演示”跨到第2课时的“代码实现”最关键的一步是建立双层循环模型。为什么必须用两层循环因为冒泡排序的执行过程天然嵌套外层描述“一共进行多少轮”内层描述“每一轮内部进行多少次相邻比较”。如果不用嵌套结构也可以把所有比较顺序平铺但那样代码无法通用数组长度一变就失效了。先看外层循环。假设数组长度为 n需要排序成从小到大。每一轮结束后最大的元素会落到数组的最后一个未就位位置。就位一个下一轮就可以少比较一个。由于最后一个元素不需要再比较所以 n 个元素最多只需要 n-1 轮。再看内层循环。内层循环要做的是“从数组开头开始两两比较相邻元素”。如果当前是第 i 轮那么这一轮结束后数组末尾已经有 i 个元素就位这里的 i 从 0 开始计数第 0 轮结束后有 1 个元素就位。所以内层循环只需要比较数组下标 0 到 n-2-i 的元素对也就是访问下标 j 和 j1其中 j 从 0 到 n-2-i。写成 Python 的循环范围就是for i in range(n - 1): # 外层控制轮数共 n-1 轮 for j in range(n - 1 - i): # 内层控制比较次数第 i 轮比较 n-1-i 次 # 比较 arr[j] 和 arr[j1]这段伪代码是第2课时教学的核心轴。课堂上有必要让学生自己推导n-1-i的来源而不是直接抄代码。推导方法很简单假设 i0第一轮需要比较 n-1 次对应range(n-1)假设 i1第二轮最后一个元素已就位只需要比较 n-2 次对应range(n-2)。由此归纳出第 i 轮对应range(n-1-i)。4. Python 基础版冒泡排序完整实现在理解了双层循环边界之后就可以进入代码实现环节。这里用 Python 写一个最基础、没有优化的冒泡排序并且刻意添加了每一轮结束后的输出语句方便课堂上观察过程。# 文件bubble_sort_basic.py def bubble_sort(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] print(f第{i 1}轮排序后{arr}) data [64, 25, 12, 22, 11] print(原始数据, data) bubble_sort(data) print(排序结果, data)这段代码有几个值得在课堂上强调的点。第一Python 的arr[j], arr[j 1] arr[j 1], arr[j]是一次赋值交换两个变量的值不需要临时变量。这是 Python 语言的特性但正因为它太简洁学生反而可能不理解“交换”本身需要覆盖值。如果学生从 C 语言入门这一点尤其容易出错后面第 6 节会给出 C 语言对照。第二内层循环range(n - 1 - i)是本节最容易写错的地方。如果写成range(n - 1)程序不会报错因为循环次数变多了但会出现重复比较如果写成range(n)当 j n-1 时会访问arr[n]直接触发 IndexError。第三if arr[j] arr[j 1]中的是升序排序。如果改成程序就会变成降序排序。这是一个非常好的课堂提问素材。运行上面代码会得到如下输出原始数据 [64, 25, 12, 22, 11] 第1轮排序后[25, 12, 22, 11, 64] 第2轮排序后[12, 22, 11, 25, 64] 第3轮排序后[12, 11, 22, 25, 64] 第4轮排序后[11, 12, 22, 25, 64] 排序结果 [11, 12, 22, 25, 64]比较输出结果和第 2 节的追踪表可以发现每一轮的结果完全一致。这一步是为了让学生确信我们推导出的循环边界和手工模拟的过程是严格对应的。5. 三种优化方法与效率分析基础版冒泡排序可以正确完成排序但存在明显的浪费。课堂上讲完基础版之后再讲优化正契合“从正确到高效”的算法学习路径。5.1 优化一通过交换标志提前结束第一个优化思路来自一个事实如果某一轮比较结束后整个数组已经有序那么后面的轮次就完全没有必要再执行了。例如数组[1, 2, 4, 3, 5]第一轮把 4 和 3 交换后变成[1, 2, 3, 4, 5]第二轮再也找不到需要交换的元素此时可以立即停止。实现方式是在每轮开始时设置一个标志位默认认为这一轮没有交换一旦发生交换就修改标志位。本轮结束后检查标志位如果仍为 False说明数组已经有序直接结束外层循环。# 文件bubble_sort_optimized.py 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 data [1, 2, 4, 3, 5] bubble_sort(data) print(data)注意swapped False的位置它必须在每一轮内部循环开始之前被重置。如果把这一行放到外层循环外面那么第一轮之后swapped一直为 True优化就失效了这是一个很经典的逻辑错误。5.2 优化二记录最后一次交换位置第二个优化思路更精细。观察每一轮交换过程可以发现最后一次发生交换的位置之后的所有元素实际上都已经有序了。下一轮内层循环没有必要再比较到n - 1 - i只需要比较到上一次记录的最后交换位置即可。# 文件bubble_sort_best.py def bubble_sort(arr): n len(arr) boundary n - 1 while boundary 0: last_swap 0 for j in range(boundary): if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] last_swap j boundary last_swap data [64, 25, 12, 22, 11] bubble_sort(data) print(data)这种写法把外层for换成了while用boundary表示当前这一轮内层循环需要比较到的最大下标。每轮结束后把boundary更新为last_swap因为最后一次交换位置之后的元素已经有序不需要再参与比较。5.3 三种版本的效率对比从时间复杂度看三种版本的最坏情况都是 O(n²)这是冒泡排序的固有代价。但常数因子和实际比较次数差异很明显版本最坏情况比较次数最好情况比较次数额外空间适用场景基础版n(n-1)/2n(n-1)/2O(1)教学演示代码最简单标志优化版n(n-1)/2n-1O(1)日常小规模数据排序边界优化版n(n-1)/2n-1O(1)需要减少比较次数的场合这里最值得让学生理解的是“最好情况”从 O(n²) 变成了 O(n)对于一个已经有序的数组优化版只需要 n-1 次比较就能判断出“不需要排序”而基础版仍然要走完所有轮次。这正是算法优化的直观意义。6. C 语言版本对照很多学生在搜索“数据结构冒泡排序”时看到的经典教材例子是用 C 语言写的。在课堂里也经常有学生问为什么 C 语言里面交换两个数要用三行而 Python 一行就完成了这里给出一个 C 语言版本的冒泡排序便于对照。// 文件bubble_sort.c #include stdio.h void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped 1; } } if (!swapped) { break; } } } int main() { int data[] {64, 25, 12, 22, 11}; int n sizeof(data) / sizeof(data[0]); bubble_sort(data, n); for (int i 0; i n; i) { printf(%d , data[i]); } return 0; }C 语言版本和 Python 版本在结构上完全一致主要区别有三点。第一C 语言中数组作为函数参数传递时不会自动携带长度信息所以必须额外传一个n。Python 通过len(arr)直接获取长度这降低了初学者的实现难度但也让部分学生缺乏“数组边界”意识。第二C 语言的交换必须借助临时变量temp。完整的交换是三行代码先把arr[j]保存到temp再把arr[j 1]赋值给arr[j]最后把temp赋值给arr[j 1]。任何一行顺序颠倒了排序结果都会出错。第三C 语言不会检查数组下标越界。如果内层循环写成j n - 1在 Python 里会直接报错在 C 语言里可能不会报错但会访问到数组之外的内存产生不可预测的结果。这一点值得在课堂上特别强调越是看起来“没报错”的程序越要检查边界条件。编译运行这段 C 代码输出结果为11 12 22 25 647. 课堂练习与常见错误排查7.1 课堂练习设计第2课时的最好结尾不是“大家自己再看看”而是给出一组有层次的上机练习。这里推荐四层递进练习每一层都对应一个教学目标。练习一为追踪题给定数组[45, 12, 8, 33, 19]要求学生手动写出每一轮冒泡排序后数组的状态并与程序输出进行比对。这个练习主要考察对“轮次”和“比较范围”的理解。练习二为补全题删去代码中的内层循环边界条件让学生根据数组长度推导range(n - 1 - i)。这个练习直接对应本节的核心难点。练习三为统计题在基础版代码中加入两个计数器分别统计比较次数和交换次数输出到屏幕。这个练习可以帮助学生直观感受冒泡排序的效率瓶颈。def bubble_sort_with_count(arr): n len(arr) compare_count 0 swap_count 0 for i in range(n - 1): for j in range(n - 1 - i): compare_count 1 if arr[j] arr[j 1]: arr[j], arr[j 1] arr[j 1], arr[j] swap_count 1 print(比较次数, compare_count) print(交换次数, swap_count) data [64, 25, 12, 22, 11] bubble_sort_with_count(data)练习四为改错题故意写出一个内层循环上界错误的版本让学生先阅读、再运行、最后修改并解释错误原因。这是培养“读代码”能力的好方法也是算法题常见的考察方式。7.2 高频错误与排查思路根据课堂反馈学生在写冒泡排序时最容易出现的问题是边界和交换逻辑。整理成下表方便上机时快速对照问题现象可能原因排查方式解决方案IndexError: list index out of range内层循环上界越界检查range参数将内层改为range(n - 1 - i)排序结果不对部分元素乱序比较的是非相邻元素打印每一对比较的下标统一使用arr[j]和arr[j 1]优化版无法提前结束swapped标志没有在每轮重置检查swapped False的位置把它移到外层循环内部开头数列完全逆序时排序极慢没有做任何优化观察比较次数统计在循环中加入提前结束判断C 程序输出异常但不报错数组下标越界访问用调试器或 printf 检查下标缩小内层循环上界这里最值得展开说明的是第一个错误。初学者经常把外层循环写在for i in range(n)内层写在for j in range(n)。表面上看循环次数多了程序也能跑出看似排序成功的结果但偶尔会碰到数组越界。排查方法很简单在 if 判断之前加一行print(j, j1)马上就能看出最后一次比较的下标是否合法。8. 教学与自学建议关于冒泡排序2这一节无论用于备课还是自学有几点建议可以直接采用。第一先建立过程追踪表再写代码。多数学生写不出代码不是不会写 Python而是脑中缺少一张清晰的“轮次-比较下标”映射表。建议在写代码前让学生先手写完成[64, 25, 12, 22, 11]的完整排序过程再让他们从表中归纳出两层循环的边界。第二每一轮输出一次数组状态。课堂演示代码一定要保留每轮结束后的输出不要一开始就追求“最简代码”。看到输出结果与自己手写过程一致学生的信心会明显提升如果不一致输出也直接暴露了逻辑错误。第三重视“交换”这个基础操作。在教学过程中最好先让学生用三个赋值语句实现交换再展示 Python 的一行交换写法。不然学生虽然通过了 Python 上机换到 C 语言或者用笔写手写代码时交换逻辑很容易写错。第四把复杂度分析放到优化之后讲。直接讲 O(n²) 公式学生往往只记住结论。更好的顺序是先通过统计题计算出不同输入规模下的比较次数再引出时间复杂度概念最后归纳出最好情况、平均情况、最坏情况的区别。第五关于学承与学考备考。不同地区对“算法与程序设计”的考查深度有所不同但冒泡排序的追踪、补全代码、改错通常是高频考查方向。建议以教材和当地考试大纲为准同时至少做到能手动追踪一轮过程、能写出内层循环边界、能在代码中识别两个典型错误。9. 总结与后续学习方向冒泡排序2这一节实际上完成了三个充满教学价值的跨越从“一轮过程表”跨越到“n-1 轮完整过程表”从“手写模拟过程”跨越到“双重循环代码”从“正确的冒泡排序”跨越到“能提前结束的优化版本”。这三个跨越真正对应的能力是将算法思想转化为循环结构并准确定义每一个边界条件。在学习方向上冒泡排序只是一个开始。接下来值得继续研究的内容包括插入排序、选择排序与冒泡排序的过程对比因为它们解决的是同一个问题但比较顺序、交换次数和稳定性各不相同“排序稳定性”这个概念也非常适合用冒泡排序作为第一个例子来理解再进一步可以学习时间复杂度从 O(n²) 降到 O(n log n) 的归并排序和快速排序体会“递归分治”为什么能带来质的提升。如果这篇文章对你有帮助建议收藏备用。你可以直接把追踪表、Python 代码和 C 语言代码用于自己的课堂或复习也可以在此基础上继续实现更复杂的排序算法。动手写一遍比看十遍的效果都好。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门