
题目考点
这道题主要考这几个知识点:
- 字符串遍历
- 字典统计频次
- 排序规则设计
- 按要求输出结果
这题的核心其实就一句话:
先统计每个字符出现了几次,再按照“次数降序、字母升序”排序,最后取前三个。
所以它不是一道“难在语法”的题,而是一道很典型的“先把题意翻译成步骤”的题。
审题
输入是什么
输入只有一行,是一个字符串 s,全部由小写字母组成。
例如:
aabbbccde
输出是什么
输出出现次数最多的 3 个字符,每个字符和它的出现次数占一行。
例如:
b 3
a 2
c 2
题目要求我们特别注意什么
这里有两个排序规则:
第一层规则:按出现次数从大到小排。
第二层规则:如果出现次数一样,按字母表顺序排。
这句话非常关键。
比如:
a出现 2 次c也出现 2 次
那么 a 要排在 c 前面,因为字母顺序里 a < c。
边界条件
题目已经保证:
- 字符串长度大于 3
- 至少有 3 种不同字符
所以我们不用担心“没有 3 个字符可选”的问题。
思路提示
先不要急着写代码,先想这题要分几步。
第一步:统计每个字符出现次数
看到“哪个字符出现最多”这种题,第一反应就应该是:
要做频次统计。
也就是把字符串中的每个字符数一遍。
比如:
aabbbccde
统计后应该得到:
a: 2
b: 3
c: 2
d: 1
e: 1
第二步:把统计结果排好序
题目不是随便输出,而是有顺序要求。
所以统计完之后,还要排序。
排序规则要写成:
- 先按次数降序
- 如果次数一样,再按字符升序
第三步:取前三个输出
排完序后,前 3 个就是答案。
完整设计思路
这题可以拆成 3 个明确步骤。
第 1 步:用字典记录每个字符出现次数
我们准备一个空字典 count_dict。
然后遍历字符串中的每个字符:
- 如果字符已经在字典里,就次数加 1
- 如果不在字典里,就先记为 1
这样遍历完以后,字典里就保存了所有字符的出现次数。
例如:
{'a': 2, 'b': 3, 'c': 2, 'd': 1, 'e': 1}
第 2 步:把字典变成可以排序的形式
字典本身不好直接按我们这个复杂规则输出,所以通常会把它变成:
[('a', 2), ('b', 3), ('c', 2), ('d', 1), ('e', 1)]
也就是“字符 + 次数”的列表。
然后对这个列表排序。
第 3 步:设计排序规则
这题最关键的地方就在这里。
我们想要的是:
- 次数大的在前面,所以次数要“降序”
- 字母小的在前面,所以字符要“升序”
Python 里常写成:
sorted_items = sorted(items, key=lambda x: (-x[1], x[0]))
这里的意思是:
x[1]是次数-x[1]表示让次数大的排前面x[0]是字符,默认升序排列
也就是说,它会先比较 -次数,如果一样,再比较字符。
这正好就是题目要求。
代码实现
下面先给你一个适合初学者理解的基础写法。
s = input().strip()
count_dict = {}
# 统计每个字符出现次数
for ch in s:
if ch in count_dict:
count_dict[ch] += 1
else:
count_dict[ch] = 1
# 转成列表,方便排序
items = list(count_dict.items())
# 按“次数降序,字符升序”排序
items.sort(key=lambda x: (-x[1], x[0]))
# 输出前三个
for i in range(3):
print(items[i][0], items[i][1])
运行演示
我们用样例来手动走一遍。
输入:
aabbbccde
第一步:统计次数
遍历字符串:
- 读到
a,记成a:1 - 再读到
a,变成a:2 - 读到
b,记成b:1 - 再读到
b,变成b:2 - 再读到
b,变成b:3 - 读到
c,记成c:1 - 再读到
c,变成c:2 - 读到
d,记成d:1 - 读到
e,记成e:1
最后字典变成:
{'a': 2, 'b': 3, 'c': 2, 'd': 1, 'e': 1}
第二步:转成列表
[('a', 2), ('b', 3), ('c', 2), ('d', 1), ('e', 1)]
第三步:排序
按规则:
b出现 3 次,最多,排第一a和c都出现 2 次,但a字母更小,所以a在前c排第三d和e都是 1 次,但已经不在前三里了
排序后:
[('b', 3), ('a', 2), ('c', 2), ('d', 1), ('e', 1)]
第四步:输出前三个
b 3
a 2
c 2
这题最核心的思考方法
以后你再遇到这种题,可以直接这样想:
第一问:题目是不是在问“谁出现得最多”
如果是,那大概率要做频次统计。
常见关键词有:
- most common
- frequency
- count
- 重复次数
- 出现最多
只要看到这些词,就要想到:
先统计。
第二问:统计完后是不是还要求顺序
如果题目说了:
- 按大小排序
- 相同的时候再按别的规则排
那你就要想到:
统计之后还要排序。
第三问:最后是不是只取前几个
如果题目说 top 3、前 5、最多的 1 个,那就说明:
排完序后只取前几项。
方法总结
这道题的标准套路可以总结成一句模板:
遍历统计 -> 按规则排序 -> 取前几个输出
以后遇到同类题,你就按这个顺序拆。
可以把这题记成下面这个做题模板:
- 输入一个字符串
- 用字典统计每个字符次数
- 把字典转成
(字符, 次数)的列表 - 用排序规则处理
- 输出前 3 个
补充说明:为什么这里用字典最合适
因为我们要做的是:
“一个字符” 对应 “它出现的次数”
这正是字典最擅长的事:
键 -> 值
字符 -> 次数
比如:
{'a': 2, 'b': 3, 'c': 2}
如果不用字典,你就得反复数很多次,会更麻烦。
所以看到“统计每个元素出现次数”,优先想到字典。
练习
练习题
输入一个小写字符串,输出其中出现次数最多的 2 个字符。
要求:
- 先按出现次数降序
- 如果次数相同,按字母升序
- 每个结果单独占一行输出
例如输入:
banana
你可以先自己试着想。
提示
先思考这三步:
- 怎么统计每个字符出现次数
- 怎么把统计结果排序
- 怎么只输出前 2 个
为什么要单独讲这句代码
这句代码确实是这道题里最关键的一句:
items.sort(key=lambda x: (-x[1], x[0]))
很多初学者看到它会觉得“每个符号都认识,但放在一起就看不懂”。这很正常,因为它其实把三个知识点压在了一行里:
sort()是排序key=是告诉 Python “按什么规则排”lambda x: (-x[1], x[0])是临时写出来的排序规则
所以这节我们不急着记这句,而是把它一层一层拆开。
先看 items 到底是什么
在这道题里,前面我们通常会先统计字符出现次数,例如:
count_dict = {'a': 2, 'b': 3, 'c': 2, 'd': 1, 'e': 1}
然后写:
items = list(count_dict.items())
这时 items 变成:
[('a', 2), ('b', 3), ('c', 2), ('d', 1), ('e', 1)]
也就是说,items 是一个列表,里面每个元素都是一个二元组:
(字符, 次数)
例如:
('a', 2)表示字符a出现 2 次('b', 3)表示字符b出现 3 次
这个前提一定要先看清楚,因为后面的 x[0]、x[1] 都是基于这个结构来的。
items.sort() 是什么
先只看最外层:
items.sort()
它的意思是:
把列表 items 原地排序。
“原地排序”就是直接改这个列表本身,不会新建一个新列表。
例如:
nums = [3, 1, 2]
nums.sort()
print(nums)
结果是:
[1, 2, 3]
原来的 nums 自己变了。
所以这道题里的:
items.sort(...)
就是在说:
把 items 这个列表按某种规则重新排列。
key= 到底是什么意思
这一部分是最容易卡住的地方。
items.sort(key=...)
这里的 key 可以理解成:
“排序时,你拿每个元素的什么东西来比较?”
比如有这样一个列表:
words = ['apple', 'kiwi', 'banana']
如果直接排序:
words.sort()
会按字母顺序排。
但如果你想按“单词长度”排,就可以写:
words.sort(key=len)
因为 key=len 的意思是:
- 对
'apple',拿len('apple') = 5 - 对
'kiwi',拿len('kiwi') = 4 - 对
'banana',拿len('banana') = 6
也就是实际比较的是:
5, 4, 6
所以最后顺序就变成:
['kiwi', 'apple', 'banana']
所以,key 不是“直接排序的结果”,而是:
先从每个元素身上提取一个“比较依据”,再按这个依据排序。
lambda x: 是什么
现在看这一段:
lambda x: (-x[1], x[0])
你可以先把它理解成:
写了一个很短的小函数。
它等价于这样一个普通函数:
def rule(x):
return (-x[1], x[0])
所以:
items.sort(key=lambda x: (-x[1], x[0]))
其实就等价于:
def rule(x):
return (-x[1], x[0])
items.sort(key=rule)
只是 lambda 写法更短,所以常用于这种“临时只用一次的小规则”。
这里的 x 到底是谁
因为 items 是这样的列表:
[('a', 2), ('b', 3), ('c', 2), ('d', 1), ('e', 1)]
所以排序时,Python 会把列表里的每一个元素,依次拿出来交给 lambda x: ...。
也就是说,这里的 x 有可能是:
('a', 2)
也有可能是:
('b', 3)
也可能是:
('c', 2)
所以这里:
x[0]就是元组里的第 0 个元素,也就是字符x[1]就是元组里的第 1 个元素,也就是次数
比如当:
x = ('b', 3)
那么:
x[0] == 'b'
x[1] == 3
(-x[1], x[0]) 到底在返回什么
这是整句最核心的地方。
它返回的是一个二元组:
(-次数, 字符)
比如:
当 x = ('a', 2)
(-x[1], x[0]) = (-2, 'a')
当 x = ('b', 3)
(-x[1], x[0]) = (-3, 'b')
当 x = ('c', 2)
(-x[1], x[0]) = (-2, 'c')
所以排序时,Python 实际比较的不是原来的:
('a', 2), ('b', 3), ('c', 2)
而是比较它们对应的“排序钥匙”:
(-2, 'a'), (-3, 'b'), (-2, 'c')
为什么要写 -x[1]
因为默认排序是升序。
升序的意思是:小的在前,大的在后。
但是题目要求次数降序,也就是大的在前,小的在后。
怎么办?
一个很常见的技巧就是:把数字取负号。
例如本来次数是:
3, 2, 2, 1, 1
取负后变成:
-3, -2, -2, -1, -1
升序排列这些负数时:
-3 < -2 < -1
就会得到对应原数的:
3 > 2 > 1
也就是说:
用负号,把“想要的降序”变成了“默认的升序”。
这就是为什么写 -x[1]。
为什么后面又要写 x[0]
题目要求是:
- 先按次数降序
- 如果次数相同,再按字母升序
那前面的 -x[1] 只解决了第一层:次数降序。
如果两个字符次数一样怎么办?
这时就要看第二层,所以加上:
x[0]
也就是字符本身。
字符默认比较就是按字母顺序升序排:
'a' < 'b' < 'c'
所以:
(-x[1], x[0])
刚好表示:
- 第一关键字:次数降序
- 第二关键字:字符升序
Python 为什么能按元组排序
这是这句代码能成立的根本原因之一。
Python 比较两个元组时,是从前往后逐个比较的。
例如比较:
(-3, 'b') 和 (-2, 'a')
先比第一个位置:
-3 < -2
第一个位置已经分出大小了,所以不用看第二个位置了。
于是 (-3, 'b') 会排在前面。
再看:
(-2, 'a') 和 (-2, 'c')
先比第一个位置:
-2 == -2
第一个位置一样,那就继续比第二个位置:
'a' < 'c'
所以 (-2, 'a') 排在前面。
这就完全符合我们的需求:
- 次数不同,先看次数
- 次数相同,再看字母
用样例手动模拟一遍
假设:
items = [('a', 2), ('b', 3), ('c', 2), ('d', 1), ('e', 1)]
执行:
items.sort(key=lambda x: (-x[1], x[0]))
第一步:每个元素先算出自己的 key
('a', 2)->(-2, 'a')('b', 3)->(-3, 'b')('c', 2)->(-2, 'c')('d', 1)->(-1, 'd')('e', 1)->(-1, 'e')
第二步:按 key 排序
也就是排:
(-3, 'b')
(-2, 'a')
(-2, 'c')
(-1, 'd')
(-1, 'e')
第三步:得到原元素的新顺序
于是原列表变成:
[('b', 3), ('a', 2), ('c', 2), ('d', 1), ('e', 1)]
这就是最终结果。
为什么不能简单写成 reverse=True
很多初学者会想:
“既然次数要从大到小,那我直接反过来排不就行了?”
比如写:
items.sort(key=lambda x: (x[1], x[0]), reverse=True)
这样通常会出问题。
因为 reverse=True 会把整个比较结果一起反过来。
这意味着不仅次数会变成降序,连字符也会变成降序。
举个例子:
('a', 2)
('c', 2)
本来题目要求次数相同时按字母升序,所以应该:
a 在前,c 在后
但如果你整体 reverse=True,就可能变成:
c 在前,a 在后
这就不符合题意了。
所以这题不能偷懒写成“整体反转”,而要精确地写成:
(-x[1], x[0])
也就是:
- 次数手动做降序
- 字符保留升序
这是更准确的做法。
再把整句翻译成人话
现在你可以把这句代码完整翻译成一句中文:
items.sort(key=lambda x: (-x[1], x[0]))
意思就是:
把 items 里的每个元素 x 都转换成 (-出现次数, 字符) 这个比较依据,然后按这个依据升序排序。
因为:
-出现次数会让原来的大次数排前面字符会让相同次数时按字母顺序排
所以最终效果就是:
按次数降序,次数相同按字母升序。
你以后遇到类似写法,应该怎么拆
以后看到这种代码,不要整句硬背,而是按这个顺序拆:
第 1 步:先看列表里的每个元素长什么样
比如这题里每个元素是:
(字符, 次数)
第 2 步:再看 x[0]、x[1] 分别代表谁
这里就是:
x[0]是字符x[1]是次数
第 3 步:最后看 key 返回了什么结构
这里返回的是:
(-次数, 字符)
那就说明排序规则是:
- 先按次数降序
- 再按字符升序
你只要按这三步拆,很多 sort(key=lambda ...) 都能看懂。
本节小结
这句代码最本质的含义,不是“某个神秘语法”,而是:
人为构造一个排序依据。
items.sort(key=lambda x: (-x[1], x[0]))
里面每一部分都在服务同一件事:
items.sort():给列表排序key=:指定排序规则lambda x::临时写一个小函数x[1]:取次数-x[1]:让次数实现降序效果x[0]:取字符(-x[1], x[0]):先比次数,再比字母
所以这句代码不是“背下来”,而是应该读成:
按照“次数从大到小、字母从小到大”的规则排序。
练习
你可以先自己思考下面这几个 key 分别表示什么:
练习 1
items.sort(key=lambda x: x[1])
提示:这里只看次数,而且是默认升序。
练习 2
items.sort(key=lambda x: -x[1])
提示:这里只看次数,而且是降序,不管字符顺序。
练习 3
items.sort(key=lambda x: (x[0], -x[1]))
提示:这次比较顺序和原题不一样了。先想一想,它会先按什么排,再按什么排。
为什么很多人会卡在 lambda
你现在卡住的,其实不是排序本身,而是这个感觉:
“为什么这里不用先写一个 def 函数,再把函数名传进去?为什么一个 lambda 表达式,居然可以直接塞进 sort(key=...) 里面?”
这个问题问得非常好。因为一旦这里想通,后面你看很多 Python 代码都会顺很多。
这节我们就不急着回到原题,先把 lambda 本身讲透。
先说结论:lambda 本质上就是函数
最核心的一句话是:
lambda 不是特殊的数据,不是魔法语法,它本质上就是一个函数对象。
也就是说,Python 看到:
lambda x: x + 1
会把它当成一个“函数”。
它和下面这个普通函数,在作用上很像:
def add_one(x):
return x + 1
只是写法更短。
所以你之所以能把 lambda 塞进 sort(key=...),本质原因就是:
key= 需要的本来就是一个函数,而 lambda 恰好也会产生一个函数。
先别看 sort,先看函数到底能不能“当东西传来传去”
这是理解 lambda 的前提。
在 Python 里,函数不只是“能执行的一段代码”,函数本身也可以当成一个值。
你可以把它理解成:
- 数字可以赋值给变量
- 字符串可以赋值给变量
- 列表可以赋值给变量
- 函数也可以赋值给变量
例如:
def add_one(x):
return x + 1
f = add_one
print(f(5))
输出:
6
这里要注意:
f = add_one
不是在调用函数,而是在说:
把函数 add_one 本身交给变量 f。
所以后面:
f(5)
就相当于:
add_one(5)
这说明一件很重要的事:
函数在 Python 里是“第一类对象”。
也就是说,它可以像普通值一样:
- 赋值给变量
- 作为参数传给别的函数
- 作为返回值返回出去
而 sort(key=...) 正是利用了第 2 点:把一个函数传给另一个函数。
先理解:sort(key=...) 到底想要什么
比如:
words = ['apple', 'kiwi', 'banana']
words.sort(key=len)
这里的 key=len,不是在让 len 立刻执行一遍所有内容。
而是在告诉 sort:
“等会儿你自己去排序的时候,每拿到一个元素,就调用一下 len,看看这个元素的长度,把长度作为比较依据。”
也就是说,sort 想要的是:
一个“拿到元素后,能返回比较依据”的函数。
这个函数可以是:
- 内置函数
len - 你自己用
def写的函数 - 也可以是
lambda
所以 key= 后面并不要求“必须写函数名”,它要求的是:
给我一个函数就行。
为什么 def 能传进去
先看普通写法:
items = [('a', 2), ('b', 3), ('c', 2)]
def rule(x):
return (-x[1], x[0])
items.sort(key=rule)
这里的 rule 是一个函数。sort 在排序时,会反复做类似这样的事:
- 对
('a', 2)调用rule(('a', 2)) - 对
('b', 3)调用rule(('b', 3)) - 对
('c', 2)调用rule(('c', 2))
得到:
(-2, 'a')(-3, 'b')(-2, 'c')
然后按这些结果去排序。
所以:
items.sort(key=rule)
意思就是:
把函数 rule 交给 sort,让它在内部使用。
lambda 为什么也能传进去
因为 lambda 写出来的结果,本身也是函数。
例如:
lambda x: (-x[1], x[0])
你可以把它理解成:
“现场直接创建了一个函数,这个函数接收参数 x,返回 (-x[1], x[0])。”
它几乎等价于:
def rule(x):
return (-x[1], x[0])
所以:
items.sort(key=lambda x: (-x[1], x[0]))
本质上就是:
“我懒得先单独写一个 def rule(x): ...,我就在这里临时造一个函数,直接传给 sort。”
所以不是 lambda 被“塞进去了”,而是:
lambda 创建出的那个函数对象,被传给了 sort。
你可以把它拆成两步看
很多人之所以觉得神秘,是因为它一行写完了。
其实你可以把它拆开:
写法 1:先定义函数,再传进去
def rule(x):
return (-x[1], x[0])
items.sort(key=rule)
写法 2:先用 lambda 造函数,再赋值给变量
rule = lambda x: (-x[1], x[0])
items.sort(key=rule)
写法 3:直接现场创建并传进去
items.sort(key=lambda x: (-x[1], x[0]))
这三种写法,从本质上说,做的是同一件事。
区别只是:
- 第一种最完整,最适合初学理解
- 第二种说明
lambda真的可以先存起来 - 第三种最简洁,适合“这个函数只用一次”的场景
专门看一下:lambda 到底“返回”了什么
很多人误以为:
key=lambda x: (-x[1], x[0])
是在把“计算结果”传进去。
其实不是。
这里传进去的不是某个结果,而是一个函数。
你可以做个概念上的类比:
f = lambda x: x + 1
这不是在算 x + 1,因为此时连 x 是谁都还不知道。
这句话的意思是:
创建一个函数,这个函数以后接收一个参数 x,并返回 x + 1。
所以同样地:
lambda x: (-x[1], x[0])
也不是立刻去计算某个具体元组。
它是在定义一种规则:
“以后如果有人给我一个 x,我就返回 (-x[1], x[0])。”
而 sort 就是那个“以后会给你 x 的人”。
sort 和 lambda 是怎么配合的
现在把两者接起来看。
假设:
items = [('a', 2), ('b', 3), ('c', 2)]
然后你写:
items.sort(key=lambda x: (-x[1], x[0]))
可以把它想象成下面这种过程:
第一步:你给 sort 一个函数
这个函数的规则是:
x -> (-x[1], x[0])
第二步:sort 自己遍历列表元素
它内部会把每个元素拿出来,交给这个函数:
x = ('a', 2),得到(-2, 'a')x = ('b', 3),得到(-3, 'b')x = ('c', 2),得到(-2, 'c')
第三步:sort 按这些结果排序
于是原列表变成:
[('b', 3), ('a', 2), ('c', 2)]
所以真正的分工是:
lambda负责“提供规则”sort负责“调用规则并排序”
为什么说它像“临时函数”
因为它通常没有名字,而且只用一次。
比如:
items.sort(key=lambda x: (-x[1], x[0]))
这时候这个函数只是为了这一次排序服务。
排序结束后,你也不打算以后再单独调用它,比如不会写:
这个函数下次我还要用
所以这里最自然的写法就是:
现场定义,现场使用。
这就是“临时函数”的感觉。
而如果某个函数会多次用到,或者逻辑比较复杂,就更适合用 def 单独写出来。
lambda 和 def 的真正关系
很多初学者会以为:
def是定义函数lambda是另外一种奇怪东西
其实更准确的理解是:
def和lambda都能产生函数- 只是使用场景不同
def 更适合:
- 逻辑比较长
- 需要多行代码
- 需要写注释
- 以后还要重复使用
- 想给函数一个正式名字
例如:
def is_even(x):
return x % 2 == 0
lambda 更适合:
- 逻辑很短
- 只是一句简单表达式
- 只用一次
- 写在参数位置更方便
例如:
nums.sort(key=lambda x: x % 10)
所以你可以把它们理解成:
def:正式定义函数lambda:快速写一个一次性小函数
为什么 lambda 只能写一条“表达式”
这也是你以后会注意到的一点。
lambda 后面不能写很多行复杂逻辑,比如不能这样:
lambda x:
y = x + 1
return y
这是不行的。
lambda 只能写成这种风格:
lambda 参数: 表达式
例如:
lambda x: x + 1
lambda x: x[1]
lambda x: (-x[1], x[0])
也就是说,lambda 更像是:
快速写一个“输入 -> 输出”的小规则。
这就特别适合 sort(key=...) 这种场景,因为 key 本来就只需要一个“从元素提取比较依据”的简单函数。
一个非常关键的区分:传函数 和 调函数
这是理解 lambda 和 key 的核心。
传函数
items.sort(key=rule)
这里是把函数 rule 传进去。
调函数
rule(x)
这里才是调用函数。
同样地:
传 lambda 产生的函数
items.sort(key=lambda x: (-x[1], x[0]))
这里传进去的是一个函数对象。
而不是你自己在外面调用它。
真正调用它的是 sort 内部。
这点一定要分清:
你不是在外面先算完,再把结果交给 sort。你是在把“怎么算”的规则交给 sort。
用一个生活类比理解
你可以把 sort 想成一个“整理员”。
它会问你:
“这些东西我该按什么标准整理?”
你回答它的方式,不是直接给最终答案,而是给它一个规则。
比如:
- 按长度排
- 按最后一个字母排
- 按次数从大到小、字母从小到大排
这个“规则”就是 key 函数。
而 lambda 只是你把规则写出来的一种简洁方式。
所以:
items.sort(key=lambda x: (-x[1], x[0]))
就像是在对整理员说:
“每个元素你都把它看成 (-次数, 字符),再去排序。”
再看几个最小例子,帮助你彻底建立感觉
例 1:按数字本身排序
nums = [30, 12, 25, 41]
nums.sort(key=lambda x: x)
print(nums)
这里 lambda x: x 的意思是:
“每个元素就按它自己来比较。”
其实和默认排序差不多。
例 2:按个位数排序
nums = [30, 12, 25, 41]
nums.sort(key=lambda x: x % 10)
print(nums)
这里不是按数字大小排,而是按个位数排:
- 30 -> 0
- 12 -> 2
- 25 -> 5
- 41 -> 1
排序依据变成:
0, 2, 5, 1
于是结果会按个位数顺序重排。
这里更能看出:key 函数不是“改数据”,而是“告诉 sort 用什么角度看数据”。
例 3:按字符串长度排序
words = ['banana', 'kiwi', 'apple']
words.sort(key=lambda x: len(x))
print(words)
这里 lambda x: len(x) 的意思是:
“不要按字母顺序看这些单词,而要按长度看。”
所以:
'banana'-> 6'kiwi'-> 4'apple'-> 5
于是结果按长度排列。
回到原题那句代码,你现在应该怎么读
items.sort(key=lambda x: (-x[1], x[0]))
现在你可以按下面这个顺序去理解:
第 1 层:sort 需要一个函数
它要靠这个函数决定怎么排。
第 2 层:lambda x: ... 现场造了一个函数
这个函数接收列表里的一个元素 x。
第 3 层:这个函数返回排序依据
返回的是:
(-x[1], x[0])
也就是:
- 次数取负,达到降序效果
- 字符原样,达到升序效果
第 4 层:sort 自己反复调用这个函数
对每个元素都算出 key,再完成排序。
所以这整句并不是魔法,而只是:
把一个一次性的小函数,直接作为参数传给了 sort。
本节小结
你最该记住的不是语法形式,而是这个思想:
在 Python 里,函数本身也可以被当作“值”传来传去。
因此:
sort(key=...)需要一个函数def能定义函数lambda也能定义函数- 所以
lambda可以直接塞进sort(key=...)
把这件事压缩成一句话就是:
lambda 之所以能直接写进 sort(key=...),是因为它本来就在“现场创建一个函数”,而 key 参数要的恰好就是函数。
练习
你可以先自己判断下面三句分别在做什么,不急着写答案。
练习 1
nums.sort(key=lambda x: -x)
提示:想一想,这里的 lambda 返回的是什么,它会让数字按什么顺序排。
练习 2
words.sort(key=lambda x: x[-1])
提示:这里是按单词的哪一部分排序?
练习 3
把下面这句:
items.sort(key=lambda x: (-x[1], x[0]))
改写成“先用 def 定义函数,再传给 sort”的形式。
提示:这道练习非常重要,因为一旦你能在 lambda 和 def 之间来回改写,说明你真的理解了。
为什么 key=lambda x: … 里写的是 x,但我们从来没有手动给它传参数,它到底是谁传进来的?
为什么这又是一个关键问题
你现在问的这个问题,已经非常接近本质了:
items.sort(key=lambda x: (-x[1], x[0]))
这里明明写了一个参数 x,但我们自己从头到尾都没有手动写过:
lambda(某个东西)
那这个 x 到底是谁给的?
答案是:
不是你传的,是 sort() 在内部调用这个函数时传进去的。
也就是说,x 的值来自 sort 排序过程中,依次拿出来的列表元素。
这节我们就把这件事彻底讲透。
先给一句最直接的人话版
当你写:
items.sort(key=lambda x: (-x[1], x[0]))
你其实是在对 Python 说:
“我给你一个规则函数。你在排序 items 的时候,每拿到一个元素,就把这个元素传给这个函数。函数参数名我这里写成 x。”
所以:
x不是你手动传的x是sort()内部帮你传的x每次都会变成列表中的一个元素
先回忆一下:函数参数不一定非要你亲手传
很多初学者脑子里默认有一种印象:
“函数参数,一定要我自己在括号里写出来。”
比如:
def add_one(n):
return n + 1
print(add_one(5))
这里确实是你手动传了 5 给 n。
所以你会形成一个印象:
n的值是我写进去的
这个印象没错,但它只是“最常见情况”,不是唯一情况。
因为还有另一种情况:
你把函数交给别的函数,那个别的函数会替你调用它。
这时,参数就不是你亲手传,而是“外层函数”帮你传。
先看一个最简单的类比
我们先自己写一个函数,模拟这种“函数里面再调用函数”的感觉。
def use_function(f):
print(f(10))
这里 use_function 接收一个参数 f,而且它假设 f 是一个函数。
然后它内部做了这件事:
f(10)
也就是说,是 use_function 在调用 f,并且给它传了参数 10。
现在我们这样用:
use_function(lambda x: x + 1)
你会发现这里我们没有手动写:
(lambda x: x + 1)(10)
但程序依然能运行。
为什么?
因为调用过程实际上是这样的:
第一步
你把这个函数交给 use_function:
lambda x: x + 1
第二步
use_function 内部自己调用它:
f(10)
第三步
于是这个 10 就变成了 lambda 里的 x
也就是:
x = 10
所以结果是:
11
这个例子非常重要,因为它和 sort(key=...) 的关系几乎一模一样。
sort(key=...) 本质上也在做同样的事
当你写:
items.sort(key=lambda x: (-x[1], x[0]))
这里并不是你在自己调用这个 lambda。
而是你把这个 lambda 函数交给了 sort。
然后 sort 在内部会做类似这样的事情:
key(某个列表元素)
也就是说,真正调用这个函数的是 sort。
所以 x 的来源就是:
sort 每次拿出来处理的那个元素。
回到原题,x 到底可能是谁
假设:
items = [('a', 2), ('b', 3), ('c', 2)]
那么排序时,sort 会依次处理这些元素。
于是某一次调用时,可能相当于做了:
(lambda x: (-x[1], x[0]))(('a', 2))
这时:
x = ('a', 2)
再下一次,可能相当于:
(lambda x: (-x[1], x[0]))(('b', 3))
这时:
x = ('b', 3)
再下一次:
(lambda x: (-x[1], x[0]))(('c', 2))
这时:
x = ('c', 2)
所以这里的 x 并不是一个固定值,而是:
排序过程中,当前正在被处理的那个元素。
为什么我们看不到这个“传参动作”
因为这个动作发生在 sort() 的内部。
这就像你用洗衣机洗衣服:
- 你只负责把衣服放进去、按模式
- 至于洗衣机内部什么时候进水、什么时候旋转、什么时候甩干,你看不到
同样的道理:
- 你只负责把规则函数交给
sort - 至于
sort内部什么时候调用这个函数、调用多少次、按什么顺序调用,这些都由sort自己完成
所以你会感觉:
“我没传啊,怎么 x 就有值了?”
其实不是没传,而是:
不是你直接传的,是 sort 替你传的。
把这句代码拆成“隐藏版”来看
你看到的是:
items.sort(key=lambda x: (-x[1], x[0]))
但你可以把它脑补成:
第一步:先把函数交给 sort
key_function = lambda x: (-x[1], x[0])
items.sort(key=key_function)
第二步:sort 内部开始工作
它会做类似这样的事:
key_function(('a', 2))
key_function(('b', 3))
key_function(('c', 2))
于是就分别得到:
(-2, 'a')
(-3, 'b')
(-2, 'c')
第三步:按这些结果排序
最后得到正确顺序。
这样一展开,你就会看得很清楚:
x 之所以有值,是因为 sort 在内部调用 key_function 时,把列表元素传进去了。
这和 for x in items 其实很像
你可以把它和循环对比着理解。
比如:
for x in items:
print(x)
这里你也没有手动写:
x = ('a', 2)
x = ('b', 3)
x = ('c', 2)
但是循环执行时,x 会依次变成每个元素。
为什么?
因为 for 这套语法机制会自动从 items 里一个一个取元素,赋给 x。
同样地,在:
key=lambda x: ...
这里你也没有手动给 x 赋值。
但是 sort 会自动把每个元素传给这个函数,于是 x 就依次代表每个元素。
所以这两种感觉很像:
循环里
for x in items:
是“循环机制”自动给 x 赋值。
排序里
key=lambda x: ...
是“sort 机制”自动把元素传给 x。
参数名为什么偏偏写 x
其实不一定非得写 x。
你写成别的名字也行,比如:
items.sort(key=lambda item: (-item[1], item[0]))
或者:
items.sort(key=lambda pair: (-pair[1], pair[0]))
这些都可以。
因为参数名只是一个“占位名字”,用来表示:
“将来别人调用这个函数时,传进来的那个东西,我在函数内部怎么称呼它。”
所以:
- 写
x可以 - 写
item可以 - 写
pair也可以
之所以很多示例爱写 x,只是因为短、方便。
但从学习角度来说,初学者其实写成 item 往往更清楚:
items.sort(key=lambda item: (-item[1], item[0]))
这样你更容易看懂:
“哦,item 就是列表里的一个元素。”
再看一个你自己能完全控制的例子
为了把“谁在传参数”这件事彻底看清,我们自己写一个简化版。
def my_sort_like(data, key_func):
for item in data:
print('当前元素:', item)
print('key结果:', key_func(item))
然后这样调用:
items = [('a', 2), ('b', 3), ('c', 2)]
my_sort_like(items, lambda x: (-x[1], x[0]))
执行过程可以理解成:
第一次循环
item = ('a', 2)
key_func(item)
这时相当于:
(lambda x: (-x[1], x[0]))(('a', 2))
所以 x = ('a', 2)
第二次循环
item = ('b', 3)
key_func(item)
所以 x = ('b', 3)
第三次循环
item = ('c', 2)
key_func(item)
所以 x = ('c', 2)
你看,只要自己模拟一次,就很清楚了:
参数是谁传的,取决于是谁在调用这个函数。
在这个例子里,是 my_sort_like 传的。
在真正的排序里,是 sort 传的。
一个非常重要的原则
以后只要你看到这种结构:
某个函数(另一个函数)
你都要立刻想到:
那个“另一个函数”很可能不是你马上自己调用,而是被外层函数留着,等会儿内部再调用。
比如:
items.sort(key=...)
map(...)
filter(...)
这类写法都有一个共同点:
- 你提供规则
- 系统内部去反复调用这个规则
所以参数是“内部机制”传进去的,不是你手动一个个传的。
用“老师点名”来理解也很直观
你可以把 items 想成一排学生:
[('a', 2), ('b', 3), ('c', 2)]
你把一个规则交给老师:
lambda x: (-x[1], x[0])
然后老师开始点名:
- 第一个同学上来,记作
x - 第二个同学上来,记作
x - 第三个同学上来,记作
x
每次 x 都是“当前正在处理的那一个”。
所以 x 并不是某个永远不变的人,而是一个临时称呼:
谁当前被传进来,谁就叫 x。
回到你最想知道的那句话
为什么 key=lambda x: ... 里写的是 x,但我们从来没有手动给它传参数?
因为这个 lambda 函数不是你自己在外面调用的,而是 sort() 在内部调用的。
它到底是谁传进来的?
是 sort() 在排序过程中,把列表里的每个元素依次传进去的。
那 x 每次代表什么?
代表当前正在被排序规则处理的那个元素。
在这道题里,items 中的每个元素都是一个元组:
(字符, 次数)
所以 x 每次就是其中一个这样的元组。
本节小结
这节最重要的一句话是:
函数参数的值,不一定非要你手动写在括号里;谁调用这个函数,谁就负责给参数赋值。
在:
items.sort(key=lambda x: (-x[1], x[0]))
里,
- 你负责提供函数
sort负责调用函数sort调用时把items中的元素传进去- 所以
x就自动有值了
你可以把这件事记成一个固定思维:
key 后面写的是“规则函数”,而规则函数里的参数,来自 Python 内部在排序时传进来的当前元素。
练习
这次还是只给题目和提示,你先自己想。
练习 1
看下面代码:
words = ['apple', 'banana', 'kiwi']
words.sort(key=lambda s: len(s))
问题:这里的 s 是谁传进去的?
提示:想清楚是谁在调用 lambda s: len(s)。
练习 2
看下面代码:
nums = [30, 12, 25]
nums.sort(key=lambda n: n % 10)
问题:这里 n 每次可能分别是什么?
提示:它会依次变成列表里的每个元素。
练习 3
把这句代码:
items.sort(key=lambda x: (-x[1], x[0]))
改写成“先把 lambda 赋值给变量 rule,再传给 sort”的形式。
提示:你一旦能写出这一步,就说明你已经开始真正理解“函数是可以先存起来,再交给别人调用”的。
为什么这又是一个关键问题
你现在问的这个问题,已经非常接近本质了:
items.sort(key=lambda x: (-x[1], x[0]))
这里明明写了一个参数 x,但我们自己从头到尾都没有手动写过:
lambda(某个东西)
那这个 x 到底是谁给的?
答案是:
不是你传的,是 sort() 在内部调用这个函数时传进去的。
也就是说,x 的值来自 sort 排序过程中,依次拿出来的列表元素。
这节我们就把这件事彻底讲透。
先给一句最直接的人话版
当你写:
items.sort(key=lambda x: (-x[1], x[0]))
你其实是在对 Python 说:
“我给你一个规则函数。你在排序 items 的时候,每拿到一个元素,就把这个元素传给这个函数。函数参数名我这里写成 x。”
所以:
x不是你手动传的x是sort()内部帮你传的x每次都会变成列表中的一个元素
先回忆一下:函数参数不一定非要你亲手传
很多初学者脑子里默认有一种印象:
“函数参数,一定要我自己在括号里写出来。”
比如:
def add_one(n):
return n + 1
print(add_one(5))
这里确实是你手动传了 5 给 n。
所以你会形成一个印象:
n的值是我写进去的
这个印象没错,但它只是“最常见情况”,不是唯一情况。
因为还有另一种情况:
你把函数交给别的函数,那个别的函数会替你调用它。
这时,参数就不是你亲手传,而是“外层函数”帮你传。
先看一个最简单的类比
我们先自己写一个函数,模拟这种“函数里面再调用函数”的感觉。
def use_function(f):
print(f(10))
这里 use_function 接收一个参数 f,而且它假设 f 是一个函数。
然后它内部做了这件事:
f(10)
也就是说,是 use_function 在调用 f,并且给它传了参数 10。
现在我们这样用:
use_function(lambda x: x + 1)
你会发现这里我们没有手动写:
(lambda x: x + 1)(10)
但程序依然能运行。
为什么?
因为调用过程实际上是这样的:
第一步
你把这个函数交给 use_function:
lambda x: x + 1
第二步
use_function 内部自己调用它:
f(10)
第三步
于是这个 10 就变成了 lambda 里的 x
也就是:
x = 10
所以结果是:
11
这个例子非常重要,因为它和 sort(key=...) 的关系几乎一模一样。
sort(key=...) 本质上也在做同样的事
当你写:
items.sort(key=lambda x: (-x[1], x[0]))
这里并不是你在自己调用这个 lambda。
而是你把这个 lambda 函数交给了 sort。
然后 sort 在内部会做类似这样的事情:
key(某个列表元素)
也就是说,真正调用这个函数的是 sort。
所以 x 的来源就是:
sort 每次拿出来处理的那个元素。
回到原题,x 到底可能是谁
假设:
items = [('a', 2), ('b', 3), ('c', 2)]
那么排序时,sort 会依次处理这些元素。
于是某一次调用时,可能相当于做了:
(lambda x: (-x[1], x[0]))(('a', 2))
这时:
x = ('a', 2)
再下一次,可能相当于:
(lambda x: (-x[1], x[0]))(('b', 3))
这时:
x = ('b', 3)
再下一次:
(lambda x: (-x[1], x[0]))(('c', 2))
这时:
x = ('c', 2)
所以这里的 x 并不是一个固定值,而是:
排序过程中,当前正在被处理的那个元素。
为什么我们看不到这个“传参动作”
因为这个动作发生在 sort() 的内部。
这就像你用洗衣机洗衣服:
- 你只负责把衣服放进去、按模式
- 至于洗衣机内部什么时候进水、什么时候旋转、什么时候甩干,你看不到
同样的道理:
- 你只负责把规则函数交给
sort - 至于
sort内部什么时候调用这个函数、调用多少次、按什么顺序调用,这些都由sort自己完成
所以你会感觉:
“我没传啊,怎么 x 就有值了?”
其实不是没传,而是:
不是你直接传的,是 sort 替你传的。
把这句代码拆成“隐藏版”来看
你看到的是:
items.sort(key=lambda x: (-x[1], x[0]))
但你可以把它脑补成:
第一步:先把函数交给 sort
key_function = lambda x: (-x[1], x[0])
items.sort(key=key_function)
第二步:sort 内部开始工作
它会做类似这样的事:
key_function(('a', 2))
key_function(('b', 3))
key_function(('c', 2))
于是就分别得到:
(-2, 'a')
(-3, 'b')
(-2, 'c')
第三步:按这些结果排序
最后得到正确顺序。
这样一展开,你就会看得很清楚:
x 之所以有值,是因为 sort 在内部调用 key_function 时,把列表元素传进去了。
这和 for x in items 其实很像
你可以把它和循环对比着理解。
比如:
for x in items:
print(x)
这里你也没有手动写:
x = ('a', 2)
x = ('b', 3)
x = ('c', 2)
但是循环执行时,x 会依次变成每个元素。
为什么?
因为 for 这套语法机制会自动从 items 里一个一个取元素,赋给 x。
同样地,在:
key=lambda x: ...
这里你也没有手动给 x 赋值。
但是 sort 会自动把每个元素传给这个函数,于是 x 就依次代表每个元素。
所以这两种感觉很像:
循环里
for x in items:
是“循环机制”自动给 x 赋值。
排序里
key=lambda x: ...
是“sort 机制”自动把元素传给 x。
参数名为什么偏偏写 x
其实不一定非得写 x。
你写成别的名字也行,比如:
items.sort(key=lambda item: (-item[1], item[0]))
或者:
items.sort(key=lambda pair: (-pair[1], pair[0]))
这些都可以。
因为参数名只是一个“占位名字”,用来表示:
“将来别人调用这个函数时,传进来的那个东西,我在函数内部怎么称呼它。”
所以:
- 写
x可以 - 写
item可以 - 写
pair也可以
之所以很多示例爱写 x,只是因为短、方便。
但从学习角度来说,初学者其实写成 item 往往更清楚:
items.sort(key=lambda item: (-item[1], item[0]))
这样你更容易看懂:
“哦,item 就是列表里的一个元素。”
再看一个你自己能完全控制的例子
为了把“谁在传参数”这件事彻底看清,我们自己写一个简化版。
def my_sort_like(data, key_func):
for item in data:
print('当前元素:', item)
print('key结果:', key_func(item))
然后这样调用:
items = [('a', 2), ('b', 3), ('c', 2)]
my_sort_like(items, lambda x: (-x[1], x[0]))
执行过程可以理解成:
第一次循环
item = ('a', 2)
key_func(item)
这时相当于:
(lambda x: (-x[1], x[0]))(('a', 2))
所以 x = ('a', 2)
第二次循环
item = ('b', 3)
key_func(item)
所以 x = ('b', 3)
第三次循环
item = ('c', 2)
key_func(item)
所以 x = ('c', 2)
你看,只要自己模拟一次,就很清楚了:
参数是谁传的,取决于是谁在调用这个函数。
在这个例子里,是 my_sort_like 传的。
在真正的排序里,是 sort 传的。
一个非常重要的原则
以后只要你看到这种结构:
某个函数(另一个函数)
你都要立刻想到:
那个“另一个函数”很可能不是你马上自己调用,而是被外层函数留着,等会儿内部再调用。
比如:
items.sort(key=...)
map(...)
filter(...)
这类写法都有一个共同点:
- 你提供规则
- 系统内部去反复调用这个规则
所以参数是“内部机制”传进去的,不是你手动一个个传的。
用“老师点名”来理解也很直观
你可以把 items 想成一排学生:
[('a', 2), ('b', 3), ('c', 2)]
你把一个规则交给老师:
lambda x: (-x[1], x[0])
然后老师开始点名:
- 第一个同学上来,记作
x - 第二个同学上来,记作
x - 第三个同学上来,记作
x
每次 x 都是“当前正在处理的那一个”。
所以 x 并不是某个永远不变的人,而是一个临时称呼:
谁当前被传进来,谁就叫 x。
回到你最想知道的那句话
为什么 key=lambda x: ... 里写的是 x,但我们从来没有手动给它传参数?
因为这个 lambda 函数不是你自己在外面调用的,而是 sort() 在内部调用的。
它到底是谁传进来的?
是 sort() 在排序过程中,把列表里的每个元素依次传进去的。
那 x 每次代表什么?
代表当前正在被排序规则处理的那个元素。
在这道题里,items 中的每个元素都是一个元组:
(字符, 次数)
所以 x 每次就是其中一个这样的元组。
本节小结
这节最重要的一句话是:
函数参数的值,不一定非要你手动写在括号里;谁调用这个函数,谁就负责给参数赋值。
在:
items.sort(key=lambda x: (-x[1], x[0]))
里,
- 你负责提供函数
sort负责调用函数sort调用时把items中的元素传进去- 所以
x就自动有值了
你可以把这件事记成一个固定思维:
key 后面写的是“规则函数”,而规则函数里的参数,来自 Python 内部在排序时传进来的当前元素。
练习
这次还是只给题目和提示,你先自己想。
练习 1
看下面代码:
words = ['apple', 'banana', 'kiwi']
words.sort(key=lambda s: len(s))
问题:这里的 s 是谁传进去的?
提示:想清楚是谁在调用 lambda s: len(s)。
练习 2
看下面代码:
nums = [30, 12, 25]
nums.sort(key=lambda n: n % 10)
问题:这里 n 每次可能分别是什么?
提示:它会依次变成列表里的每个元素。
练习 3
把这句代码:
items.sort(key=lambda x: (-x[1], x[0]))
改写成“先把 lambda 赋值给变量 rule,再传给 sort”的形式。
提示:你一旦能写出这一步,就说明你已经开始真正理解“函数是可以先存起来,再交给别人调用”的。



