字符串遍历+字典统计频次

题目考点

这道题主要考这几个知识点:

  1. 字符串遍历
  2. 字典统计频次
  3. 排序规则设计
  4. 按要求输出结果

这题的核心其实就一句话:

先统计每个字符出现了几次,再按照“次数降序、字母升序”排序,最后取前三个。

所以它不是一道“难在语法”的题,而是一道很典型的“先把题意翻译成步骤”的题。


审题

输入是什么

输入只有一行,是一个字符串 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)]

第三步:排序

按规则:

  1. b 出现 3 次,最多,排第一
  2. ac 都出现 2 次,但 a 字母更小,所以 a 在前
  3. c 排第三
  4. de 都是 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 个,那就说明:

排完序后只取前几项。


方法总结

这道题的标准套路可以总结成一句模板:

遍历统计 -> 按规则排序 -> 取前几个输出

以后遇到同类题,你就按这个顺序拆。

可以把这题记成下面这个做题模板:

  1. 输入一个字符串
  2. 用字典统计每个字符次数
  3. 把字典转成 (字符, 次数) 的列表
  4. 用排序规则处理
  5. 输出前 3 个

补充说明:为什么这里用字典最合适

因为我们要做的是:

“一个字符” 对应 “它出现的次数”

这正是字典最擅长的事:

键   -> 值
字符 -> 次数

比如:

{'a': 2, 'b': 3, 'c': 2}

如果不用字典,你就得反复数很多次,会更麻烦。

所以看到“统计每个元素出现次数”,优先想到字典。


练习

练习题

输入一个小写字符串,输出其中出现次数最多的 2 个字符。

要求:

  1. 先按出现次数降序
  2. 如果次数相同,按字母升序
  3. 每个结果单独占一行输出

例如输入:

banana

你可以先自己试着想。

提示

先思考这三步:

  1. 怎么统计每个字符出现次数
  2. 怎么把统计结果排序
  3. 怎么只输出前 2 个

为什么要单独讲这句代码

这句代码确实是这道题里最关键的一句:

items.sort(key=lambda x: (-x[1], x[0]))

很多初学者看到它会觉得“每个符号都认识,但放在一起就看不懂”。这很正常,因为它其实把三个知识点压在了一行里:

  1. sort() 是排序
  2. key= 是告诉 Python “按什么规则排”
  3. 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]

题目要求是:

  1. 先按次数降序
  2. 如果次数相同,再按字母升序

那前面的 -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 里是“第一类对象”
也就是说,它可以像普通值一样:

  1. 赋值给变量
  2. 作为参数传给别的函数
  3. 作为返回值返回出去

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 的人”。


sortlambda 是怎么配合的

现在把两者接起来看。

假设:

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 单独写出来。


lambdadef 的真正关系

很多初学者会以为:

  • def 是定义函数
  • lambda 是另外一种奇怪东西

其实更准确的理解是:

  • deflambda 都能产生函数
  • 只是使用场景不同

def 更适合:

  1. 逻辑比较长
  2. 需要多行代码
  3. 需要写注释
  4. 以后还要重复使用
  5. 想给函数一个正式名字

例如:

def is_even(x):
    return x % 2 == 0

lambda 更适合:

  1. 逻辑很短
  2. 只是一句简单表达式
  3. 只用一次
  4. 写在参数位置更方便

例如:

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 本来就只需要一个“从元素提取比较依据”的简单函数。


一个非常关键的区分:传函数 和 调函数

这是理解 lambdakey 的核心。

传函数

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”的形式。

提示:这道练习非常重要,因为一旦你能在 lambdadef 之间来回改写,说明你真的理解了。

为什么 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 不是你手动传的
  • xsort() 内部帮你传的
  • x 每次都会变成列表中的一个元素

先回忆一下:函数参数不一定非要你亲手传

很多初学者脑子里默认有一种印象:

“函数参数,一定要我自己在括号里写出来。”

比如:

def add_one(n):
    return n + 1

print(add_one(5))

这里确实是你手动传了 5n

所以你会形成一个印象:

  • 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 不是你手动传的
  • xsort() 内部帮你传的
  • x 每次都会变成列表中的一个元素

先回忆一下:函数参数不一定非要你亲手传

很多初学者脑子里默认有一种印象:

“函数参数,一定要我自己在括号里写出来。”

比如:

def add_one(n):
    return n + 1

print(add_one(5))

这里确实是你手动传了 5n

所以你会形成一个印象:

  • 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”的形式。

提示:你一旦能写出这一步,就说明你已经开始真正理解“函数是可以先存起来,再交给别人调用”的。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇