Python 新手入门,第一个排序算法怎么写
为什么从冒泡排序开始?
很多刚接触 Python 的朋友,一听到“算法”两个字就觉得头大,仿佛要面对什么高深的数学公式或者复杂的逻辑迷宫。其实,算法的本质就是解决问题的步骤。就像你整理书架上的书,总得有个顺序:是按作者名字排,还是按书名高低排?这个“整理的过程”就是算法。
在众多的排序算法中,冒泡排序(Bubble Sort)虽然在实际工程中效率不是最高的,但它绝对是新手入门的“最佳陪练”。它的逻辑非常直观,几乎不需要额外的数据结构知识,只需要理解最基础的循环和条件判断就能掌握。更重要的是,它能完美地展示计算机是如何通过“比较”和“交换”来让数据变得有序的。今天,我们就抛开那些枯燥的理论定义,直接动手用 Python 写出你的第一个排序算法。
算法核心逻辑:像气泡一样浮上来
想象一下,你有一杯刚倒好的汽水,里面有很多小气泡。这些气泡大小不一,代表了我们数组里乱序的数字。冒泡排序的名字由来,就是因为最大的那个数字(或者说最重的元素),会像水里的大气泡一样,经过不断的比较和交换,慢慢地“浮”到数列的最顶端(也就是数组的末尾)。
这个过程具体是怎么发生的呢?我们可以把它拆解成两个关键动作:
- 相邻比较:从头开始,拿着第一个数和第二个数比。如果前面的数比后面的大,那就把它们俩的位置互换;如果前面的已经比后面小了,那就保持不动。
- 重复推进:接着拿第二个数和第三个数比,重复上面的动作。一直比到这一轮的最后一个数。
当你把这一整套动作从头到尾做了一遍后,你会发现,整个数组里最大的那个数,一定已经被挪到了最后面。这就好比第一轮比赛结束,冠军已经诞生了,并且站到了领奖台的最右侧。
接下来,我们只需要对剩下的那些还没排好序的数,重复同样的过程。第二轮结束后,第二大的数就会停在倒数第二的位置。如此循环往复,直到所有的数都各就各位。
这里有一个非常关键的概念,也是新手最容易晕的地方:双重循环。
- 外层循环:控制我们要进行多少轮“冒泡”。如果有 N 个数,理论上我们需要 N-1 轮就能排完(因为最后一个数自然就位了)。
- 内层循环:负责在每一轮里,执行具体的“相邻比较”和“交换”动作。随着轮数增加,后面已经排好的数就不需要再参与了,所以内层循环的范围会逐渐缩小。
代码实战:逐行拆解与注释
光说不练假把式。下面这段代码就是完整的冒泡排序实现。我会像带着你写代码一样,把每一行的意图都讲清楚。你可以直接把这段代码复制到你的 Python 编辑器里运行。
def bubble_sort(arr): # 获取列表的长度,也就是我们要处理的元素个数 n = len(arr) # 外层循环:控制排序的轮数 # 范围是 0 到 n-1,意味着我们最多需要进行 n-1 轮比较 for i in range(n): # 标记变量:用于优化算法 # 假设这一轮开始时,列表已经是有序的了 swapped = False # 内层循环:负责每一轮的具体比较工作 # 注意这里的范围:n - 1 - i # 为什么要减 i?因为每一轮结束后,末尾的 i 个元素已经是最大的且有序的了,不需要再比较 for j in range(0, n - 1 - i): # 核心逻辑:比较相邻的两个元素 # 如果前一个元素 (arr[j]) 大于后一个元素 (arr[j+1]) if arr[j] > arr[j + 1]: # 交换位置! # Python 特有的语法糖,不需要临时变量就能直接交换 arr[j], arr[j + 1] = arr[j + 1], arr[j] # 既然发生了交换,说明列表之前并不是有序的 swapped = True # 优化检查:如果在这一轮完整的内层循环中,一次交换都没有发生 # 说明列表已经完全有序了,我们可以提前结束,不用再跑剩下的轮数 if not swapped: break # 测试数据:一个乱序的整数列表 numbers = [64, 34, 25, 12, 22, 11, 90] print("排序前的列表:", numbers) bubble_sort(numbers) print("排序后的列表:", numbers)重点细节解析
在这段代码中,有几个地方值得你停下来仔细琢磨:
首先是range(0, n - 1 - i)这个写法。很多初学者会直接写成range(n),这虽然也能跑通,但效率很低。试想一下,第一轮结束后,最大的数已经在最后了;第二轮再比较到最后一个是毫无意义的,因为它肯定比前面的大。减去i就是为了避免这种无效劳动,让内层循环的范围随着外层轮数的增加而动态缩小。
其次是swapped标志位的使用。这是一个很好的编程习惯。假如你传入的列表本来就是[1, 2, 3, 4, 5],如果没有这个优化,程序还是会傻傻地跑完所有外层循环,做大量无用的比较。有了它,程序在内层循环发现一次交换都没发生时,就会立刻break跳出,极大地提升了在“基本有序”情况下的性能。
最后是 Python 的交换语法a, b = b, a。在其他语言如 C 或 Java 中,你可能需要定义一个temp临时变量来暂存数据才能交换,但在 Python 里,这一行代码就优雅地解决了问题,既简洁又易读。
运行结果与可视化观察
当你运行上面的代码时,控制台会输出以下内容:
排序前的列表:[64, 34, 25, 12, 22, 11, 90] 排序后的列表:[11, 12, 22, 25, 34, 64, 90]为了让你更清楚地看到“冒泡”的过程,我们可以稍微修改一下代码,在每次交换时打印出当前的状态(实际开发中当然不会这么做,但这对于学习非常有用):
# 仅用于演示过程的简化版片段 arr = [5, 1, 4, 2, 8] n = len(arr) for i in range(n): for j in range(0, n - 1 - i): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] print(f"交换了 {arr[j+1]} 和 {arr[j]} -> 当前列表:{arr}")运行这段演示代码,你会看到数字8是如何一步步从中间位置,经过多次交换,最终“浮”到最右边的。紧接着是5,然后是4……这种动态的变化过程,能帮你建立起对算法执行流的直观感受。你会发现,越往后的轮次,需要移动的距离越短,因为后面的“大块头”都已经就位了。
新手常见错误排查
在自己动手尝试时,你可能会遇到一些典型的“坑”,提前了解这些能帮你节省不少调试时间。
1. 索引越界错误(IndexError)这是最常见的报错。通常发生在内层循环的范围设置上。如果你写成了range(n - i),那么当j取到最大值时,j + 1就会等于n,而列表的最大索引是n - 1。访问arr[n]自然会报错。
- 解决方法:务必记住内层循环的上限是
n - 1 - i,确保j + 1永远不会超出列表边界。
2. 缩进混乱导致逻辑失效Python 对缩进非常敏感。如果把if not swapped: break这句话缩进错了层级,比如缩进了内层循环里,那么只要有一次没交换,循环就会立即终止,导致排序只进行了一半。
- 解决方法:检查
break语句是否与内层的for循环对齐,它应该是在内层循环完全结束后才执行的判断。
3. 混淆了“原地排序”与“返回值”上面的bubble_sort函数没有return任何值,因为它直接修改了传入的列表(原地排序)。有些新手会在调用后写new_list = bubble_sort(numbers),然后打印new_list,结果发现是None。
- 解决方法:理解 Python 列表的可变性。直接打印原列表
numbers即可看到排序结果,或者在函数末尾显式加上return arr。
4. 比较方向搞反如果你想实现从大到小排序,却仍然使用if arr[j] > arr[j + 1],那结果依然是从小到大。
- 解决方法:想要降序排列,只需将大于号
>改为小于号<,逻辑就变成了“如果前面的比后面的小,就交换”,这样小的数就会慢慢浮上去,大的数沉下来。
动手练习建议
看懂了不代表学会了。建议你关掉代码,尝试自己在空白文件里重新写一遍冒泡排序。可以先从一个只有 5 个数字的列表开始,手动模拟一遍交换过程,然后再敲代码验证。
试着修改一下代码,让它支持用户输入任意一组数字进行排序;或者挑战一下,统计一下你的算法到底进行了多少次比较和交换。这些小小的变式练习,能帮你把“双重循环”的逻辑彻底刻在脑子里。当你能够不假思索地写出这个结构时,恭喜你,你已经正式迈过了算法入门的第一道门槛,接下来的归并排序、快速排序等更高级的算法,也不过是在此基础上的演变而已。
