Python 排序与顺序验证

2026-09-07 00:00    #Python   #算法竞赛  

排序不仅用于写正解,也常用于验证结论:尝试某种贪心顺序、构造规范化结果,或者把两个无序答案转成可比较的形式。Python 的关键不是记住排序算法,而是掌握 sortedlist.sortkey

sortedlist.sort

sorted 接受任意可迭代对象,返回一个新列表,不修改原数据:

1a = [5, 1, 4, 2]
2b = sorted(a)
3
4assert a == [5, 1, 4, 2]
5assert b == [1, 2, 4, 5]

list.sort 只用于列表,原地修改并返回 None

1a = [5, 1, 4, 2]
2result = a.sort()
3
4assert a == [1, 2, 4, 5]
5assert result is None
常见错误

不要写 a = a.sort()。执行后 a 会变成 None。需要新列表时用 sorted(a),允许修改原列表时单独调用 a.sort()

降序与翻转

1a = [5, 1, 4, 2]
2
3assert sorted(a, reverse=True) == [5, 4, 2, 1]
4assert list(reversed(a)) == [2, 4, 1, 5]

reverse=True 按排序规则降序排列;reversed(a) 只是把当前顺序反过来,两者含义不同。

使用 key 指定排序依据

key 接收一个函数。排序前,Python 对每个元素调用一次该函数,并按照返回值比较。

按绝对值排序:

1a = [-7, 2, -3, 5]
2
3assert sorted(a, key=abs) == [2, -3, 5, -7]

按第二个字段排序:

1items = [("apple", 3), ("banana", 1), ("pear", 2)]
2answer = sorted(items, key=lambda item: item[1])
3
4assert answer == [
5    ("banana", 1),
6    ("pear", 2),
7    ("apple", 3),
8]

lambda item: item[1] 是一个没有名字的短函数,相当于:

1def second(item):
2    return item[1]
3
4
5items = [("apple", 3), ("banana", 1), ("pear", 2)]
6assert sorted(items, key=second)[0] == ("banana", 1)

复杂逻辑应使用普通 def,不要为了少写一行把 lambda 变得难以阅读。

多关键字排序

Python 的元组按字典序比较:先比较第一项,相等时再比较第二项。

例如先按分数降序,再按名字升序:

 1students = [
 2    ("Bob", 90),
 3    ("Alice", 95),
 4    ("Carol", 90),
 5]
 6
 7answer = sorted(students, key=lambda item: (-item[1], item[0]))
 8
 9assert answer == [
10    ("Alice", 95),
11    ("Bob", 90),
12    ("Carol", 90),
13]

数字字段前加负号,可以让这一项按降序排列,同时保留其他字段的升序规则。

也可以直接利用元组默认顺序:

1pairs = [(2, 3), (1, 9), (2, 1), (1, 4)]
2
3assert sorted(pairs) == [(1, 4), (1, 9), (2, 1), (2, 3)]

itemgetter

operator.itemgetter 可以代替简单的下标 lambda

1from operator import itemgetter
2
3items = [("apple", 3), ("banana", 1), ("pear", 2)]
4
5assert sorted(items, key=itemgetter(1)) == [
6    ("banana", 1),
7    ("pear", 2),
8    ("apple", 3),
9]

它不是必须掌握的技巧。竞赛临时代码中,lambda item: item[1] 往往更容易立即看懂。

稳定排序

Python 排序是稳定的:如果两个元素的 key 相同,它们保持原来的相对顺序。

 1items = [
 2    ("first", 2),
 3    ("second", 1),
 4    ("third", 2),
 5]
 6
 7answer = sorted(items, key=lambda item: item[1])
 8
 9assert answer == [
10    ("second", 1),
11    ("first", 2),
12    ("third", 2),
13]

这允许我们分两次排序,但通常一次返回元组键更加直接。

用全排列验证排序贪心

nn 个任务,第 ii 个任务耗时为 tit_i。依次执行任务,每个任务的完成时间是它结束的时刻。目标是让所有完成时间之和最小。

贪心结论是按照耗时从小到大执行。小规模时可以枚举所有顺序验证:

 1from itertools import permutations
 2
 3
 4def completion_cost(order):
 5    elapsed = 0
 6    total = 0
 7
 8    for duration in order:
 9        elapsed += duration
10        total += elapsed
11
12    return total
13
14
15def brute_best(tasks):
16    return min(
17        (completion_cost(order), order)
18        for order in permutations(tasks)
19    )
20
21
22def greedy(tasks):
23    order = tuple(sorted(tasks))
24    return completion_cost(order), order
25
26
27tasks = [4, 1, 3]
28
29assert brute_best(tasks) == greedy(tasks) == (13, (1, 3, 4))

这里 min 比较 (代价, 顺序) 元组,先选择代价最小的方案;代价相同再按顺序打破平局。

自定义比较函数

Python 更推荐 key,因为每个元素的键只计算一次。确实只有“两元素比较规则”时,可以使用 cmp_to_key

 1from functools import cmp_to_key
 2
 3
 4def compare_length_then_text(a, b):
 5    if len(a) != len(b):
 6        return len(a) - len(b)
 7    return (a > b) - (a < b)
 8
 9
10words = ["bbb", "a", "cc", "aa"]
11answer = sorted(words, key=cmp_to_key(compare_length_then_text))
12
13assert answer == ["a", "aa", "cc", "bbb"]

这个例子用 key=lambda word: (len(word), word) 会更简单。只有比较规则难以转换成独立键时才考虑 cmp_to_key

规范化后比较答案

题目不要求输出顺序时,可以先排序再比较:

1expected = [(1, 3), (2, 4), (5, 8)]
2actual = [(5, 8), (1, 3), (2, 4)]
3
4assert sorted(actual) == sorted(expected)

如果元素允许重复并且只关心出现次数,Counter 通常比集合更合适。

参考资料