排序算法比较与应用
困难0排序算法比较与应用:帮你选择最合适的排序方法
什么是排序?为什么要学它?
排序就是把一堆乱糟糟的数字(或数据),按照从小到大(或从大到小)的顺序重新排列。就像你每天早上整理书包——把所有书本按大小码好,这样上课时就能快速找到。在编程中,排序是最基础的操作之一,无论是处理成绩表、排队顺序,还是查找某个数据,排序常常是第一步。
今天我们要比较三种简单又常用的排序算法:冒泡排序、选择排序和插入排序。学完它们,你不仅能写出排序程序,还能根据情况选择最快的方法。
三种排序算法,从生活例子讲起
1. 冒泡排序 —— 像小鱼吐泡泡
生活例子:想象班里的同学排成一排,老师要求按身高从矮到高重新站。你从左到右依次比较相邻两个人,如果前面的人比后面高,就让他们交换位置。这样,最高的同学就会像泡泡一样,慢慢地“浮”到队伍的右边。重复这个操作,直到所有人都排好。
工作原理:
- 从左到右遍历列表,依次比较相邻两个元素。
- 如果前一个 > 后一个,就交换它们。
- 每遍历一次,最大的数就“冒泡”到末尾。
- 重复 n-1 次(n 为元素个数),列表就排好序。
代码与注释:
def bubble_sort(arr):
n = len(arr) # 列表长度
for i in range(n): # 外层循环:需要遍历 n 次
for j in range(0, n-i-1): # 内层循环:每次比较的范围逐渐缩小
if arr[j] > arr[j+1]: # 如果前一个比后一个大
arr[j], arr[j+1] = arr[j+1], arr[j] # 交换位置
新手容易犯的错误:
- 内层循环忘记写
n-i-1,写成n-1,导致每次都比较所有位置,浪费时间。 - 忘记交换变量,写成
arr[j] = arr[j+1]导致数据丢失。
2. 选择排序 —— 像选美比赛
生活例子:老师让同学们按身高从矮到高排好队。第一次,老师从所有人中选出最矮的同学,让他站到第一位。然后从剩下的同学中再选出最矮的,让他站到第二位……这样依次选出,直到所有人都站好位置。
工作原理:
- 在未排序部分中找到最小的元素。
- 把它放到已排序部分的末尾(即与当前位置交换)。
- 重复 n-1 次,完成排序。
代码与注释:
def selection_sort(arr):
n = len(arr) # 列表长度
for i in range(n): # 外层循环:每次确定一个位置
min_idx = i # 假设当前 i 位置的值最小
for j in range(i+1, n): # 在 i 后面找更小的
if arr[j] < arr[min_idx]:
min_idx = j # 更新最小值的索引
arr[i], arr[min_idx] = arr[min_idx], arr[i] # 将最小值放到 i 位置
新手容易犯的错误:
- 忘记更新
min_idx,导致交换时还是初始的 i 位置,排序错误。 - 把交换写在了内层循环里,反复交换,变成冒泡了。
关于稳定性:选择排序是 不稳定 的。例如 [5a, 5b, 3](a和b是相同数字,但来自不同位置),第一次找到最小值3,与第一个5a交换,变成 [3, 5b, 5a],两个5的顺序颠倒了。如果你的程序要求相同值的顺序不能变(比如按成绩排序,同分按学号),选择排序就可能会出问题。
3. 插入排序 —— 像玩扑克牌时理牌
生活例子:你打扑克摸牌时,每摸到一张新牌,就把它插入到手牌中已经排好顺序的牌里。如果新牌比某张牌小,就插到它前面,否则往后放。这样手里的牌始终是有序的。
工作原理:
- 把列表分为“已排序部分”(左边)和“未排序部分”(右边)。
- 每次从未排序部分取第一个元素,从右向左扫描已排序部分,找到合适位置插入。
- 重复直到未排序部分为空。
代码与注释:
def insertion_sort(arr):
for i in range(1, len(arr)): # 从第二个元素开始,因为第一个默认已排好
key = arr[i] # 当前要插入的元素(新牌)
j = i - 1 # 从已排序部分的最后一个开始往前比较
while j >= 0 and arr[j] > key: # 如果前面的数比 key 大
arr[j+1] = arr[j] # 就把前面的数往后挪一位
j -= 1
arr[j+1] = key # 把 key 插入到正确位置
新手容易犯的错误:
- 忘记写
j >= 0条件,导致索引变成负数,程序崩溃。 - 把
arr[j+1] = arr[j]写成arr[j] = arr[j+1],顺序全乱。 - 认为插入排序只能从小到大,其实只要修改比较条件,也能从大到小。
特殊优点:当数据 基本有序 时,插入排序非常快,几乎只需要扫描一遍,时间复杂度接近 O(n)。
怎么比较这三种排序?
| 算法 | 最快情况时间复杂度 | 最慢情况时间复杂度 | 平均情况时间复杂度 | 是否稳定 | 数据交换次数 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) (已有序) | O(n²) | O(n²) | 稳定 | 很多 |
| 选择排序 | O(n²) | O(n²) | O(n²) | 不稳定 | 较少 (n-1次) |
| 插入排序 | O(n) (已有序) | O(n²) (逆序) | O(n²) | 稳定 | 少 (主要移动) |
简单理解:
- 冒泡:每次都必须交换好多对,像气泡慢慢上浮,最慢,但容易写。
- 选择:每次只交换一次,但无论如何都要从头到尾扫描所有剩余元素,所以也是 O(n²),但实际运行比冒泡快一点(因为交换次数少)。
- 插入:如果数据已经差不多排好,它几乎不用怎么移动,非常快;如果数据完全逆序,它和最慢的冒泡一样慢。
适用场景(用你的零花钱来举例):
- 数据很少(比如不到 10 个数):三种差别不大,就像买 3 根笔,用哪只手拿都行。
- 数据部分有序(比如考试排名只差几分乱了顺序):插入排序是王者,像整理一个基本整齐的书架。
- 数据很多(比如上千个):最好别用这三种,改用更快的算法(如快速排序、归并排序)。Python 自带的
list.sort()就是用的 Timsort(一种混合排序)。
Python 完整测试与运行结果
下面一个程序会生成两种列表:
- 随机列表:包含 1000 个随机数。
- 部分有序列表:前 90% 已经是排序的,后 10% 是乱序的(模拟考试成绩单基本排好但几个调皮鬼插队)。
你会看到插入排序在部分有序时快得惊人。
import random
import time
def bubble_sort(arr):
"""冒泡排序"""
n = len(arr) # 列表长度
for i in range(n):
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
def selection_sort(arr):
"""选择排序"""
n = len(arr) # 列表长度
for i in range(n):
min_idx = i # 假设当前 i 位置最小
for j in range(i+1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
def insertion_sort(arr):
"""插入排序"""
for i in range(1, len(arr)):
key = arr[i] # 当前要插入的元素
j = i - 1
while j >= 0 and arr[j] > key:
arr[j+1] = arr[j] # 向后移动
j -= 1
arr[j+1] = key # 插入
# ---------- 测试1: 随机列表 ----------
print("===== 测试1: 1000个随机数 =====")
data = [random.randint(0, 10000) for _ in range(1000)]
data1 = data.copy()
data2 = data.copy()
data3 = data.copy()
start = time.time()
bubble_sort(data1)
t_bubble = time.time() - start
start = time.time()
selection_sort(data2)
t_selection = time.time() - start
start = time.time()
insertion_sort(data3)
t_insertion = time.time() - start
print(f"冒泡排序用时:{t_bubble:.4f}秒")
print(f"选择排序用时:{t_selection:.4f}秒")
print(f"插入排序用时:{t_insertion:.4f}秒")
# ---------- 测试2: 部分有序的列表 ----------
print("\n===== 测试2: 1000个部分有序的数(前900个已排序) =====")
data_part = list(range(1, 901)) + [random.randint(1, 1000) for _ in range(100)]
data1 = data_part.copy()
data2 = data_part.copy()
data3 = data_part.copy()
start = time.time()
bubble_sort(data1)
t_bubble = time.time() - start
start = time.time()
selection_sort(data2)
t_selection = time.time() - start
start = time.time()
insertion_sort(data3)
t_insertion = time.time() - start
print(f"冒泡排序用时:{t_bubble:.4f}秒")
print(f"选择排序用时:{t_selection:.4f}秒")
print(f"插入排序用时:{t_insertion:.4f}秒")
可能的运行结果(每次不同,但规律相似):
===== 测试1: 1000个随机数 =====
冒泡排序用时:0.0890秒
选择排序用时:0.0450秒
插入排序用时:0.0460秒
===== 测试2: 1000个部分有序的数(前900个已排序) =====
冒泡排序用时:0.0280秒
选择排序用时:0.0440秒
插入排序用时:0.0030秒
你能看到:
- 在随机数时,插入排序和选择排序接近,冒泡最慢。
- 在部分有序时,插入排序快了一个数量级(0.003秒 vs 0.028秒),因为它只需要处理后面乱序的那一点点。
常见错误总结(新手一定注意!)
-
冒泡排序:内层循环的终点写错了,导致越界或效率低。
- ❌
for j in range(0, n-1):会多比较已经排好的部分。 - ✅
for j in range(0, n-i-1):每次减少比较次数。
- ❌
-
选择排序:忘记更新最小值索引,或者交换时用错了变量。
- ❌
if arr[j] < arr[min_idx]:后面没有更新min_idx。 - ✅ 必须写
min_idx = j。
- ❌
-
插入排序:while 循环条件忘记
j >= 0,导致arr[-1]访问(Python 会取最后一个元素,逻辑错误)。- ❌
while arr[j] > key:当 j 为 -1 时,arr[-1]是列表最后一个数,条件可能意外成立。 - ✅
while j >= 0 and arr[j] > key:
- ❌
-
混淆排序方向:如果想从大到小排,只需要把
>改成<,但注意三个函数的修改要统一。 -
直接修改原数据:有时候你不想改变原列表,记得用
.copy()复制一份再排序。
小结与进阶指引
- 数据少于 100:随便选一个,冒泡最简单,插入最实用。
- 数据接近有序:插入排序是王者,速度快,写法也简单。
- 数据很多(成千上万):别用这三种,改用 快速排序、归并排序 或直接调用 Python 内置的
list.sort()和sorted()。内置排序是用 C 语言实现的 Timsort,又快又稳定。
想继续学习?
- 快速排序(Quick Sort)—— 分治思想,适合大部分情况。
- 归并排序(Merge Sort)—— 稳定,适合链表或大数据量。
- 桶排序、计数排序—— 针对特定范围的数据,可以快到 O(n)。
- 在竞赛中,如果数据规模在 10^3 以内,手写插入排序有时比内置排序还快(因为常数小);超过 10^4 就放心用内置排序吧。
最后一句:排序算法不仅是考试重点,更是你编程思维的训练——学会从“如何做”到“怎么做更好”的思考。动手写写代码,跑跑测试,很快就能掌握它们。
例题精讲
在冒泡排序、选择排序和插入排序中,以下关于时间复杂度的描述正确的是?
假设要对列表 [5, 3, 8, 6, 2] 进行升序排序,如果使用冒泡排序并加入优化(当某一轮没有发生交换时提前结束),那么在第几轮排序后可以提前结束?
插入排序在数组基本有序的情况下,其时间复杂度可以接近O(n)。
选择排序是一种稳定的排序算法,因为它在交换元素时不会改变相等元素的相对顺序。
请完成下面的插入排序函数,将列表 arr 按升序排序。
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
___ # 将arr[j]后移一位
j -= 1
arr[j + 1] = key
请填写正确的代码。