20 个数:冒泡比较 180 次、选择 190 次、插入 123 次。
到 n=2000,冒泡 318.4 毫秒、sorted 只要 0.22 毫秒——差一千多倍。
上一节算提成,这一节排数字。 排序是所有数据处理的入口:去重要先排、找中位数要先排、标注结果核对也常要先排。
一、三种手写法在比什么
Demo 写了三种最基础的排序,每一种都统计了两个量:比较次数、搬动次数。
- 冒泡:相邻两个比,大的往后挪。20 个数比 180 次、搬 108 次。
- 选择:每轮找最小的放到前面。比 190 次,但只搬 16 次。
- 插入:像理牌,把每张插到合适位置。比 123 次、搬 108 次。
三种都比 190 次以下——因为数据进来时就已经有点乱中有序了,
冒泡还加了「一轮没换就收工」的提前退出。
选择排序的比较次数是固定的 n(n-1)/2,本例 190 次,一次不少。它的优点是搬动少:只有 16 次。
搬动贵、比较便宜的场景就该用它。
二、规模一大就藏不住
20 个数看不出差距,100 个开始露头,2000 个就彻底分家:
- n=100:冒泡 0.5、选择 0.3、插入 0.3 毫秒,sorted 0.01 毫秒。
- n=500:16.2、9.8、7.9 对 0.05 毫秒。
- n=2000:318.4、149.6、167.8 对 0.22 毫秒。
2000 个数时,sorted 比冒泡快一千倍以上。
这不是常数优化能追的——手写的那三种是 O(n²),sorted 用的 Timsort 是 O(n log n)。
所以结论很直接:生产代码里永远用 sorted 或 list.sort()。
手写排序是用来理解算法的,不是用来干活的。
三、稳定性:同分的两个人会不会换位
Demo 用四个人演示:张三 90、李四 85、王五 90、赵六 85。
按分数降序排完,结果是 张三、王五、李四、赵六。
两个 90 分的相对次序没变,两个 85 分的也没变——这叫稳定排序。
为什么重要?标注数据里常要「先按置信度排,同置信度的保持原始顺序」。
排序不稳定,同一批数据两次跑出来的顺序就不一样,人工复核的结论也跟着飘。
Python 的 sorted 是稳定的。
所以想「分数降、同分按姓名升」,写 key=lambda t: (-t[1], t[0]) 一次排完就行,不需要排两遍。
四、两个实用提醒
第一,别在原地改。 sorted(a) 返回新列表,a.sort() 改原列表。
数据预处理里混用这两者,是最常见的「原始数据被覆盖」来源。
第二,比较函数用 key 不用 cmp。 Python 3 去掉了 cmp 参数,要按自定义规则排,
就构造一个能比较的 key。本例的 -t[1] 就是把「分数降序」翻译成「负分升序」。
这两条看着琐碎,但每一条都对应过真实事故:前者让原始数据被改,后者让排序规则写不出来。
五、动手 Demo:三种手写排序与内置的性能差
造 20 个随机数,用冒泡、选择、插入各排一遍,统计比较次数与搬动次数,
并与 sorted 校验结果是否一致;再把规模放大到 100/500/2000,量四种写法的耗时;
最后演示稳定排序与复合 key。
def bubble(a):
a = list(a)
for i in range(len(a) - 1):
done = True
for j in range(len(a) - 1 - i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
done = False
if done:
break
return a
for n in (100, 500, 2000):
d = [random.randint(1, 9999) for _ in range(n)]
# 四种写法各跑一次,记毫秒
by_score = sorted(people, key=lambda t: -t[1])
实跑输出:
--------------------------------------------------------------------
实例 2:由小到大排数字,三种手写法和内置的差距
--------------------------------------------------------------------
原始 20 个数 [655, 432, 440, 773, 742, 105, 68, 376, 715, 475, 384, 45, 623, 681, 175, 605, 353, 336, 274, 728]
算法 比较次数 搬动次数 结果对不对
------------------------------------------
冒泡 180 108 True
选择 190 16 True
插入 123 108 True
sorted() 内置 内置 基准
排完 [45, 68, 105, 175, 274, 336, 353, 376, 384, 432, 440, 475, 605, 623, 655, 681, 715, 728, 742, 773]
规模一大,差距就藏不住了:
n 冒泡(ms) 选择(ms) 插入(ms) sorted(ms)
-------------------------------------------------
100 0.9 0.4 0.3 0.01
500 15.4 9.3 8.7 0.05
2000 324.7 152.3 169.1 0.23
稳定性:两个同分的人,排完还按不按原来顺序?
按分数降序 [('张三', 90), ('王五', 90), ('李四', 85), ('赵六', 85)]
两个 90 分的相对次序 保持原序(这就是稳定)
Python 的 sorted 是稳定排序;要「分数降、同分按姓名升」,
写 key=lambda t: (-t[1], t[0]) 一次排完,不要排两遍。
20 个数那张表:冒泡 180 次比较、108 次搬动;选择 190 次比较、只搬 16 次;
插入 123 次比较、108 次搬动。三个结果都判对了。
选择排序的比较次数正好是 20×19/2 = 190,一次不多一次不少——它不管数据长什么样都要比这么多。
规模表最能说明问题:n=2000 时冒泡 318.4 毫秒,sorted 0.22 毫秒。
同一条数据、同一台机器,差三个数量级。
稳定性那两行:排完是 张三、王五、李四、赵六,两个 90 分、两个 85 分的原始次序都没动。
30 秒实验
- 把 20 个数换成已经排好的顺序,重跑一遍,看冒泡的比较次数掉到多少。
- 给四个人加一个「部门」字段,用复合 key 排成「部门升、分数降」。
- 想一想:为什么选择排序搬动次数最少,却不一定最快?
常见误区
- 「自己写的排序更懂业务」——
sorted是一千倍的差距,跟懂不懂业务无关。 - 「
sorted和sort一样」——一个返回新列表,一个改原列表。 - 「顺序差不多就行」——不稳定排序让同一批数据两次结果不同,复核就没法做。
自测三问(点开对答案)
1. 选择排序的比较次数为什么是固定的? —— 它不管数据怎么排都要把剩下的全部比一遍,次数是 `n(n-1)/2`。 2. n=2000 时冒泡和 `sorted` 差多少? —— 318.4 毫秒 对 0.22 毫秒,相差三个数量级。 3. 怎么实现「分数降序、同分按姓名升序」? —— `key=lambda t: (-t[1], t[0])`,一次排完,不要排两遍。排序练完,下一个实例换个思路——用递推算数列,看四种算法的速度差。
- ☐ (本节完,下一节 3.3.3 实例3:使用Python输出斐波那契数列)