01 / 本课学习路线
本课学习路线
阅读与推演约 115 分钟,练习约 55 分钟,进阶练习另需约 38 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
一次遍历建计数表,把并列规则翻译成排序键,再按题目规定的分隔符输出。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 用一次遍历建出「元素 → 次数」的计数表,并说明为什么比逐个重数快 | 第 03、04 节 | 自查第 2 条、练习 1 |
| 把「次数多在前、同次数小写在前、再按字母」翻译成排序键元组,并用两个同次数元素验证方向 | 第 05 节 | 自查第 3 条、练习 4 |
| 区分「去重」「按首次出现排序」「按次数排序」三件事,通过(AC)P2560、P2562 | 第 06、07 节 | 必做任务 1、2 |
| 用元组做键、用两张表分别计数(P2524、P2569) | 第 08 节 | 进阶练习 1、2 |
| 知道 list 的 in 是 O(n)、遍历时不能增删 | 第 03、09 节 | 自查第 4、5 条 |
下面 5 题先试着写答案,再展开对照。读入输出、字典和列表操作不熟悉的,可按链接复习;计数与排序键的写法不熟悉也可以继续下面的讲解,学完后再做一次。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | d = {},执行 d["a"] = d.get("a", 0) + 1 两次后,d 是什么? | 字典 |
| 自测 2 | sorted([3, 1, 2], key=lambda x: -x) 得到什么?key 返回元组时按什么顺序比较? | 列表常用操作,含排序与 元组 |
| 自测 3 | "a".isupper()、"A".isupper() 各返回什么?False < True 成立吗? | 布尔与比较 |
| 自测 4 | "/a/b".split("/") 得到什么?第 0 个元素是什么? | 上一课第 02 节自测 1(分隔符切分) |
| 自测 5 | "1,2".split(",") 与 "1".split(",") 的长度各是多少? | 上一课第 02 节自测 1 |
补看入口(本站内,可直接点开):表里的链接都是「看动画学 Python」的独立课页(总入口 看动画学 Python);两数之和的哈希表动画(本节第 3 步的按钮直达);上一课:输入输出规则、数据范围与精度。
展开先修自测答案
自测 1:{"a": 2}。get("a", 0) 在键不存在时返回 0,所以第一次得到 1,第二次得到 2——这就是计数表的核心一行。
自测 2:[3, 2, 1]。key 返回元组时先比第 0 个分量,相等再比第 1 个,依此类推——多级并列规则就是靠这一点写成一个元组的。
自测 3:False、True;False < True 成立(False 当 0、True 当 1)。所以把 ch.isupper() 放进排序键,小写(False)会排在大写(True)之前。
自测 4:["", "a", "b"],第 0 个元素是空串,因为字符串以 / 开头。P2524 正好利用这一点:第 i 级就在下标 i。
自测 5:2 和 1。没有分隔符时 split 返回只含原串的列表——P2569 里没有反对票的选票就是这种情况,取下标 1 之前必须先看长度。
03 / 概念与术语
计数表解决什么问题:从 O(n²) 到 O(n)
「数一数每个元素出现几次」最直接的写法是对每个元素再扫一遍全体:n 个元素各扫 n 次,就是 n² 次比较。计数表把「找到这个元素的计数位置」变成平均一次操作,整体只扫一遍。
| 术语 | 含义 | Python 里对应什么 |
|---|---|---|
| 键(key)与值(value) | 计数表里「元素」是键,「次数」是值 | freq[ch]:ch 是键,右边是值 |
| 哈希表 / 字典 | 按键直接定位到存储位置的表,查找、写入平均 O(1) | dict |
| 集合 | 只存键不存值的哈希表,用于去重和存在性判断,平均 O(1) | set |
| 可哈希 | 能当键的对象:值不可变且能算哈希值(int、str、元组);列表不行 | hash(x) 可算;list 会抛出 TypeError |
| 计数表 | 键是元素、值是出现次数的字典 | freq = {} + freq[ch] = freq.get(ch, 0) + 1 |
| 并列规则 | 两个元素在主要指标上相同时,题目规定的先后顺序 | 排序键元组的第 2、3 个分量 |
| 排序键 | sorted(..., key=f) 里的函数 f,返回一个可比较的值或元组 | key=lambda kv: (-kv[1], kv[0].isupper(), kv[0]) |
| 首次出现下标 | 元素第一次出现在数组里的位置,P2560 的并列依据 | first.setdefault(x, i) 或「不在表里才写」 |
| 做法 | n = 1,000 | n = 100,000 | 结论 |
|---|---|---|---|
| 对每个元素再扫一遍全体 | 10⁶ 次,约 0.1 秒 | 10¹⁰ 次,约 1000 秒 | 10 万规模直接超时 |
| 一次遍历建计数表 | 10³ 次 | 10⁵ 次,约 0.01 秒 | 一趟扫描 |
| 对 k 种不同元素排序 | k log k,k 不超过字符集大小 | 同左 | k 很小时可忽略 |
「每秒 10⁷ 次」是上一课的粗估说法,只用来排除明显超时的方案。本课四道题的数据规模都不大,但 O(n²) 的写法会在后面的课真正超时,习惯从这里养成。
补充学习(选学)字典为何能实现平均 O(1) 查询,以及哪些对象可作为键约 5 分钟哈希槽位、冲突退化、可哈希类型、set 与 dict
dict 拿到一个键(key),先用哈希函数把它变成一个整数,再用这个整数直接定位到一个槽位;查找和写入都跳过了逐个比较,所以平均 O(1)。两个不同的 key 落到同一个槽位叫冲突,冲突多了查找会退化,极端情况到 O(n);Python 的哈希函数和扩容策略把这种情况压得很少,题目里按平均 O(1) 估算即可。
能当 key 的对象要能算哈希值,并且算出来的值不会变:int、str,以及所有成员都可哈希的元组(tuple)可以;列表(list)这类可变对象不行(TypeError: unhashable type: 'list')。集合(set)和字典(dict)使用相同的哈希机制:dict 保存键值映射,set 只保存元素,存在性查询同样平均 O(1)。
补充学习(选学)两个经典误用:线性容器当哈希用、边遍历边改约 4 分钟list 的 in 是 O(n);遍历 dict 时删键会报错
第一个误用:对列表做成员判断(x in some_list)是逐个比较的 O(n),套在循环里就是 O(n²)——循环中频繁做存在性查询、且不需要保留重复项时,改用 set,平均查询复杂度降为 O(1)。第二个误用:遍历 dict/set 的同时增删元素,Python 会抛出运行时错误(RuntimeError: dictionary changed size during iteration);要删就先收集再删,或者遍历它的副本(list(d.items()))。
04 / 从数组到计数表
baNaNa 逐字符建表:每一步的表状态
计数表三行就能写对:建空表、逐个元素取旧值加一写回、遍历结束。下面把 baNaNa 六个字符逐个处理,每一步的表都列出来。
| 第几个字符 | 字符 | freq.get(ch, 0) 取到 | 写回后的表 |
|---|---|---|---|
| 1 | b | 0 | {b: 1} |
| 2 | a | 0 | {b: 1, a: 1} |
| 3 | N | 0 | {b: 1, a: 1, N: 1} |
| 4 | a | 1 | {b: 1, a: 2, N: 1} |
| 5 | N | 1 | {b: 1, a: 2, N: 2} |
| 6 | a | 2 | {b: 1, a: 3, N: 2} |
区分大小写:a 和 N 是不同的键;如果题目要求不区分,就在写入前统一转成小写。表里键的顺序是插入顺序(Python 3.7 起字典保持插入顺序),与排序无关——排序是下一步的事。
同一段逻辑的三行代码
freq = {} # 建空表
for ch in s: # 逐个元素
freq[ch] = freq.get(ch, 0) + 1 # 取旧值(没有就是 0)加一写回
遍历结束:freq == {"b": 1, "a": 3, "N": 2}05 / 并列规则与排序键
P2562 的规则怎么翻译成排序键,两种错误规则各输出什么
P2562 题目页的规则:按出现次数从大到小;次数相同按自然顺序,且小写字母在大写字母之前。题目页示例:xyxyXX → x:2;y:2;X:2;。这个示例决定了「小写在前」的准确含义:同次数时所有小写字母排在所有大写字母之前,再按字母顺序。
| 字母 | 排序键 (−次数, 是否大写, 字母) | 在元组比较中的位置 |
|---|---|---|
| x | (−2, False, 'x') | 第 1 位 |
| y | (−2, False, 'y') | 第 2 位(与 x 同为小写,按字母 x < y) |
| X | (−2, True, 'X') | 第 3 位(大写排在所有小写之后) |
输出 x:2;y:2;X:2;,与题目页示例一致。每项后面都有一个分号,包括最后一项——这是题目页示例给出的格式,不是可选写法。
| 排序键 | 含义 | 输出 | 与示例 |
|---|---|---|---|
| (−次数, 字母) | 默认字母顺序:ASCII 里大写 X(88) 小于小写 x(120) | X:2;x:2;y:2; | 不一致 |
| (−次数, 字母转小写, 是否大写) | 同一个字母的大小写相邻,小写在前(交错) | x:2;X:2;y:2; | 不一致 |
| (−次数, 是否大写, 字母) | 所有小写在前,再按字母;然后所有大写 | x:2;y:2;X:2; | 一致 |
第二种规则听起来也像「小写在大写之前」,但它把 X 排到了 y 前面,与题目页示例不符。并列规则的含义要用题目页示例核对,不能只凭一句话的字面理解——这也是本课自测第三条断言用 x、y、X 的原因。
并列规则翻译成排序键
Python# P2562 的并列规则翻译成一个排序键:
# 次数多在前 → -cnt;次数相同时小写字母全部排在大写之前 → ch.isupper()(False 排在 True 前面);
# 再按字母顺序 → ch
items = sorted(freq.items(), key=lambda kv: (-kv[1], kv[0].isupper(), kv[0]))
print("".join(f"{ch}:{cnt};" for ch, cnt in items)) # 题目页示例:xyxyXX → x:2;y:2;X:2;为什么不能用默认排序:ASCII 里 'B'(66) < 'a'(97),默认顺序会把大写排到小写前面,与 P2562 的题目要求不同——依赖默认顺序的代码,样例都可能无法通过。isupper() 返回的布尔值参与比较时 False 当 0、True 当 1。
手算:baNaNa 的频次表与排序
频次表: {b: 1, a: 3, N: 2}
排序键: (-次数, 是否大写, 字母)
a → (-3, False, 'a')
N → (-2, True, 'N')
b → (-1, False, 'b')
输出: a:3;N:2;b:1;
并列验证 aAbB(各 1 次): a(-1,F,'a') < b(-1,F,'b') < A(-1,T,'A') < B(-1,T,'B') → a:1;b:1;A:1;B:1;06 / 去重、存在性与首次出现
P2560 不只是去重:按次数降序、同次数按首次出现
P2560「数组去重和排序」:一行逗号分隔的整数,去掉重复元素,按出现次数从多到少输出;次数相同的按第一次出现的先后。题目页参考题解的样例:1,3,3,3,2,4,4,4,5 → 3,4,1,2,5。
| 元素 | 出现次数 | 首次出现下标(从 0 数) | 排序键 (−次数, 首次下标) |
|---|---|---|---|
| 1 | 1 | 0 | (−1, 0) |
| 3 | 3 | 1 | (−3, 1) |
| 2 | 1 | 4 | (−1, 4) |
| 4 | 3 | 5 | (−3, 5) |
| 5 | 1 | 8 | (−1, 8) |
按排序键从小到大:(−3,1)=3,(−3,5)=4,(−1,0)=1,(−1,4)=2,(−1,8)=5 → 输出 3,4,1,2,5。「首次出现」要在建表时记录:元素不在表里才写下标,已在表里就不动。
| 需求 | 做法 | 对样例的结果 |
|---|---|---|
| 只去重、保持首次出现顺序 | list(dict.fromkeys(nums)) | 1,3,2,4,5 |
| 去重后按数值升序 | sorted(set(nums)) | 1,2,3,4,5 |
| 去重后按次数降序、同次数按首次出现(P2560) | 计数表 + 首次下标表 + 排序键 (−次数, 首次下标) | 3,4,1,2,5 |
三种结果都不同。题目页说明写的是哪一种,动手前先在题目要求清单里记下。存在性判断(「x 在不在表里」)用 in 对字典或集合是平均 O(1),对列表是 O(n)。
07 / 从步骤到程序
统计、规则、输出三段分开写:两道必做题的完整参考程序
先对照步骤表,再自己写完整程序提交;完整参考程序在展开区里,写完并提交过一次再展开对照。
| 步骤 | 代码 | 说明 |
|---|---|---|
| 读入 | s = sys.stdin.readline().strip() | 仅含字母、无空格,整行读后去掉换行 |
| 统计 | freq[ch] = freq.get(ch, 0) + 1 | 第 04 节的三行 |
| 规则 | key=lambda kv: (-kv[1], kv[0].isupper(), kv[0]) | 第 05 节的排序键 |
| 输出 | "".join(f"{ch}:{cnt};" for ch, cnt in items) | 每项后一个分号,包括最后一项 |
展开完整参考程序 1:P2562 字符统计及重排(先自己写完并提交一次,再展开对照)
完整程序:P2562(标准输入 → 标准输出)
Pythonimport sys
s = sys.stdin.readline().strip() # 仅含字母,无空格
freq = {}
for ch in s:
freq[ch] = freq.get(ch, 0) + 1 # 键不存在时取 0,再加一
# 排序键:次数多在前 → 同次数时小写全部在大写之前 → 再按字母顺序
items = sorted(freq.items(), key=lambda kv: (-kv[1], kv[0].isupper(), kv[0]))
print("".join(f"{ch}:{cnt};" for ch, cnt in items)) # 每项后面都有一个分号,包括最后一项自测用例:题目页示例 xyxyXX → x:2;y:2;X:2;,abababb → b:4;a:3;,以及练习 1 的 Mississippi。题目页参考题解的排序键写法是 (-次数, int(字母 < "a"), 字母),与 isupper() 等价。
展开完整参考程序 2:P2560 数组去重和排序
完整程序:P2560(标准输入 → 标准输出)
Pythonimport sys
nums = [int(x) for x in sys.stdin.readline().split(",")] # 一行,逗号分隔
count = {} # 元素 → 出现次数
first = {} # 元素 → 首次出现的下标
for i, x in enumerate(nums):
count[x] = count.get(x, 0) + 1
if x not in first:
first[x] = i
# 次数多的在前;次数相同按首次出现的先后
order = sorted(count, key=lambda x: (-count[x], first[x]))
print(",".join(str(x) for x in order))自测用例:样例 1,3,3,3,2,4,4,4,5 → 3,4,1,2,5;第 09 节的 8,9,9,8 → 8,9。first 表只在元素第一次出现时写入;如果每次都写,记下的就是最后一次出现的位置,并列顺序会错。
08 / 进阶两题
带层级的键(P2524)与两张计数表(P2569)
两道进阶题各练一个固定动作:键可以是元组;一个人可以同时出现在两张表里。都用题目页样例逐步手算。
P2524「API 集群负载统计」:N 行 URL,每行以 / 分隔若干层级;最后一行给层级 L 和名字,问该名字在第 L 级出现了几次(大小写敏感,未出现为 0)。题目页样例:5 条 URL(/huawei/computing/no/one、/huawei/computing、/huawei、/huawei/cloud/no/one、/huawei/wireless/no/one),查询 2 computing → 2;查询 4 two → 0。
| URL | split("/") 结果 | 第 2 级 | 写入的键 |
|---|---|---|---|
| /huawei/computing/no/one | ['', 'huawei', 'computing', 'no', 'one'] | computing | (1,'huawei') (2,'computing') (3,'no') (4,'one') |
| /huawei/computing | ['', 'huawei', 'computing'] | computing | (1,'huawei') (2,'computing') |
| /huawei | ['', 'huawei'] | 无 | (1,'huawei') |
| /huawei/cloud/no/one | ['', 'huawei', 'cloud', 'no', 'one'] | cloud | (2,'cloud') … |
| /huawei/wireless/no/one | ['', 'huawei', 'wireless', 'no', 'one'] | wireless | (2,'wireless') … |
以 / 开头的字符串切分后第 0 个元素是空串,所以第 i 级正好在下标 i,不用再减一。计数表 (2,'computing') = 2,(4,'two') 不存在 → get 默认 0。键要带层级:同名字出现在不同层级要分开计数。
展开完整参考程序:P2524 API 集群负载统计
完整程序:P2524(标准输入 → 标准输出)
Pythonimport sys
lines = sys.stdin.read().split("\n")
n = int(lines[0])
urls = lines[1:1 + n]
level, word = lines[1 + n].split()
level = int(level)
count = {} # 键是 (层级, 名字)
for url in urls:
parts = url.split("/") # "/A/B" → ["", "A", "B"]:第 i 级正好是下标 i
for i in range(1, len(parts)):
key = (i, parts[i])
count[key] = count.get(key, 0) + 1
print(count.get((level, word), 0)) # 查不到就是 0自测用例:题目页两组样例(2 与 0)。一次读完按行切,第 0 行是 N,接下来 N 行是 URL,再一行是查询。
P2569「明日之星选举」:M 张选票,每张「赞成者,反对者」(反对者可省略);输出赞成票最多的 N 个人,赞成相同则反对少的在前,再相同按姓名字典序。题目页样例 2:4 张票 zhangsan,lisi / lisi,wangwu / wangwu,qianliu / qianliu,zhangsan,N=2 → lisi,qianliu。
| 姓名 | 赞成 | 反对 | 排序键 (−赞成, 反对, 姓名) | 名次 |
|---|---|---|---|---|
| zhangsan | 2 | 0 | (−2, 0, 'zhangsan') | 1 |
| lisi | 2 | 1 | (−2, 1, 'lisi') | 2 |
| wangwu | 1 | 0 | (−1, 0, 'wangwu') | 3 |
| hanmei | 1 | 1 | (−1, 1, 'hanmei') | 4 |
六张票:zhangsan,hanmei / zhangsan,lisi / lisi / lisi / wangwu / hanmei。输出前两名 zhangsan,lisi(与题目页样例一致)。样例 2 四人都是 1 赞成 1 反对,全靠姓名字典序:lisi < qianliu < wangwu < zhangsan → lisi,qianliu。只被反对、从未被赞成的人也要进表(赞成 0 票),否则排序时找不到他。
展开完整参考程序:P2569 明日之星选举
完整程序:P2569(标准输入 → 标准输出)
Pythonimport sys
lines = sys.stdin.read().split("\n")
m = int(lines[0])
votes = lines[1:1 + m]
n = int(lines[1 + m])
support = {} # 姓名 → 赞成票
oppose = {} # 姓名 → 反对票
for vote in votes:
parts = vote.split(",") # 反对票可选:可能只有一段
yes = parts[0]
support[yes] = support.get(yes, 0) + 1
oppose.setdefault(yes, 0)
if len(parts) == 2:
no = parts[1]
oppose[no] = oppose.get(no, 0) + 1
support.setdefault(no, 0)
# 赞成多在前;同赞成则反对少在前;再同则姓名字典序
order = sorted(support, key=lambda name: (-support[name], oppose[name], name))
print(",".join(order[:n]))自测用例:题目页两组样例(zhangsan,lisi 与 lisi,qianliu)。setdefault 保证每个出现过的名字在两张表里都有条目(默认 0),排序键才不会因缺键抛出 KeyError。反对数是升序(少的在前)、姓名是字典序升序,方向不要弄反。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| P2562 用默认字母顺序做并列 | xyxyXX | X:2;x:2;y:2; | x:2;y:2;X:2; | 答案错误(WA) |
| P2562 用「同字母大小写相邻」的交错规则 | xyxyXX | x:2;X:2;y:2; | x:2;y:2;X:2; | 答案错误(WA) |
| P2562 漏掉最后一个分号 | xyxyXX | x:2;y:2;X:2 | x:2;y:2;X:2; | 答案错误(WA)或格式错误(PE),取决于判题器 |
P2560 只去重保序(dict.fromkeys) | 1,3,3,3,2,4,4,4,5 | 1,3,2,4,5 | 3,4,1,2,5 | 答案错误(WA) |
| P2560 每次出现都覆盖首次下标 | 8,9,9,8 | 9,8(8 的下标被覆盖成 3、9 的成 2,并列时 9 排前) | 8,9(首次下标 8→0、9→1) | 答案错误(WA) |
| P2569 只被反对的人没进表 | 1 张票 a,b,N=2 | 只输出 a(或对 b 取键时抛出 KeyError) | a,b | 答案错误(WA)或运行错误(RE) |
P2569 没有反对段时直接取 parts[1] | 一张票 lisi | 下标越界异常(IndexError) | 正常计数 | 运行错误(RE) |
第五行的错误输出计算:8 和 9 都出现 2 次,并列看首次下标——正确程序记 8→0、9→1 输出 8,9;每次覆盖的程序最后记的是 8→3、9→2,于是 9 排前输出 9,8。像 9,8,9,8,7,7,7 这种首次与末次顺序一致的输入分辨不出这个错误。凡是「记录首次」的表,都只在键不存在时写入。
| 题目 | 输入规模 | 参考程序的复杂度 | 结论 |
|---|---|---|---|
| P2560 | 一行整数(以题目页为准) | O(n) 建表 + O(k log k) 排序 | 一趟扫描加一次排序 |
| P2562 | 仅含字母的字符串 | O(n) + O(k log k),k ≤ 52 | 排序部分可忽略 |
| P2524 | N ≤ 100 条 URL,每条最多 10 级 | O(总层级数) 建表,O(1) 查询 | 常数级 |
| P2569 | M ≤ 500 张票 | O(M) 建两张表 + O(k log k) | 常数级 |
四道题按量级估算都远小于时限。会超时的是把「x 在不在」写成对列表的 in、或在循环里反复调用 count()——那是 O(n²),在后面的课会真正超出时限。
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):对字符串 Mississippi 建计数表,再按 P2562 的规则写出输出。
展开练习 1 答案
计数表 {M: 1, i: 4, s: 4, p: 2}。排序键:i (−4, False, 'i'),s (−4, False, 's'),p (−2, False, 'p'),M (−1, True, 'M') → 输出 i:4;s:4;p:2;M:1;。做错最常见的原因:把 M 排到了 p 前面(默认字母顺序)。
练习 2(改一个条件):P2560 输入改成 2,2,1,1,3,输出什么?如果题目改成「同次数按数值升序」,输出又是什么?
展开练习 2 答案
计数 {2: 2, 1: 2, 3: 1},首次下标 {2: 0, 1: 2, 3: 4}。原规则:(−2,0)=2,(−2,2)=1,(−1,4)=3 → 2,1,3。改成同次数按数值升序:排序键变成 (−次数, 数值),(−2,1)=1,(−2,2)=2,(−1,3)=3 → 1,2,3。两条规则只差排序键的第二个分量。
练习 3(改一个条件):P2569 样例 1 的 N 改成 3,输出什么?第三名为什么是 wangwu 而不是 hanmei?
展开练习 3 答案
zhangsan,lisi,wangwu。wangwu 与 hanmei 都是 1 张赞成票,wangwu 0 张反对、hanmei 1 张反对——反对少的在前,所以 wangwu 第三。如果把反对数的方向写反(多的在前),第三名就会错成 hanmei。
练习 4(独立实现):完成「代码自测」任务的 count_chars 与 rank,让四条断言全部通过;然后说出第三条断言(x、y、X)检验的是哪一条规则。
展开练习 4 答案
计数表与并列排序的完整参考实现
Python# 计数表与并列排序的完整参考实现(做完上面的自测再看)
def count_chars(s):
freq = {}
for ch in s:
freq[ch] = freq.get(ch, 0) + 1 # 键不存在时取 0,再加一
return freq
def rank(freq):
# 排序键三元组:次数多在前、小写全部在大写之前、再按字母顺序
items = sorted(freq.items(), key=lambda kv: (-kv[1], kv[0].isupper(), kv[0]))
return [ch for ch, _ in items]
assert count_chars("baNaNa") == {"b": 1, "a": 3, "N": 2}
assert count_chars("") == {} # 空串得到空表,不是报错
assert rank({"b": 1, "a": 3, "N": 2}) == ["a", "N", "b"]
assert rank({"x": 2, "y": 2, "X": 2}) == ["x", "y", "X"] # 题目页示例:小写 x、y 都在大写 X 之前
assert rank({"a": 1, "A": 1, "b": 1, "B": 1}) == ["a", "b", "A", "B"] # 四项同次数:并列规则本身
assert rank(count_chars("baNaNa")) == ["a", "N", "b"] # 两个函数接起来跑一遍六组断言分别覆盖:常规统计、空串、常规排序、题目页示例的并列顺序(小写 x、y 都在大写 X 之前)、四项同次数的并列规则,以及两个函数串起来的整体结果。第三条断言检验的是「同次数时所有小写在所有大写之前」,用交错规则会得到 [x, X, y] 而失败。
练习 5(迁移):P2524 样例的 5 条 URL 不变,把查询改成 1 huawei,输出什么?改成 3 no 呢?
展开练习 5 答案
1 huawei → 5(五条 URL 的第 1 级都是 huawei);3 no → 3(第 1、4、5 条的第 3 级是 no;第 2 条只有 2 级,第 3 条只有 1 级)。键带层级的好处在这里体现:同一个名字在不同层级分开计数。
11 / 读题要求与复习自评
四道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。P2560 的题面与样例在题目页查看,本表按题目页参考题解整理。
| 项目 | P2560 数组去重和排序 | P2562 字符统计及重排 | P2524 API 集群负载统计 | P2569 明日之星选举 |
|---|---|---|---|---|
| 输入 | 一行逗号分隔的整数 | 一行仅含字母的字符串 | N;N 行 URL;一行「层级 名字」 | M;M 行选票;N |
| 计数表的键 | 整数 | 字符(区分大小写) | (层级, 名字) 元组 | 姓名(两张表) |
| 并列规则 | 次数降序 → 首次出现先后 | 次数降序 → 小写全部在前 → 字母顺序 | 无(查询) | 赞成降序 → 反对升序 → 姓名字典序 |
| 输出格式 | 逗号分隔 | 字符:次数; 连写,末尾也有分号 | 一个整数 | 前 N 个姓名,逗号分隔 |
| 样例 | 1,3,3,3,2,4,4,4,5 → 3,4,1,2,5 | xyxyXX → x:2;y:2;X:2; | 2 computing → 2;4 two → 0 | 样例 2 → lisi,qianliu |
题解入口:需要对照解法时,先展开本课第 07、08 节的完整参考程序;四道题的题目页另有思路与 Python / Java / C++ 参考代码,可在题目页查看。复习与自评:本课算完成 = 两道必做题 P2560、P2562 都通过判题,并勾选全部六条「学习完成检查」;进阶练习与复习题不影响完成状态。六条检查是自评,不改变题目的通过(AC)状态。复习时用三个问题自测:① 不看正文,写出计数表的三行代码;② 不看表格,写出 xyxyXX 在三种排序键下的输出并指出哪种与示例一致;③ 说出 P2560 的「首次出现下标」为什么只能在键不存在时写入。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
基础加练(选做)
同一主题的 3 道基础题
这组题目采用标准输入输出(ACM 模式):程序从标准输入读取数据、把结果写到标准输出。它们难度低于必做题,适合在必做题之前热身,或在未通过时回来巩固;不计入本课完成标准。每道题给出练习重点、完成标准与提示;已在本站通过的题目会直接显示为已通过。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/2 道
xyxyXX)验证方向,再写代码。参考程序在各节的展开区里:先自己写完并提交一次,再展开对照。写 count_chars 和 rank:统计与并列分开测
代码自测自主练习练习重点:先把频次表写对,再把三元组排序键写对;预计用时:15 分钟
完成标准:第三、四个断言(xyX → x y X,aAbB → a b A B)通过,并能说明它们检验的是并列规则
需要时查看提示
排序函数(rank)的排序键是 (-cnt, ch.isupper(), ch):False < True,所以同次数时所有小写排在所有大写之前,再按字母顺序。四个断言分别测统计、常规排序、题目页示例的并列顺序、四项同次数——哪个失败就修哪段。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def count_chars(s):
# 请在这里实现:返回 {字符: 出现次数},区分大小写
...
def rank(freq):
# 请在这里实现:按「次数降序 → 小写全部在大写之前 → 字母顺序」返回字符列表
...
assert count_chars("baNaNa") == {"b": 1, "a": 3, "N": 2}
assert rank({"b": 1, "a": 3, "N": 2}) == ["a", "N", "b"]
assert rank({"x": 2, "y": 2, "X": 2}) == ["x", "y", "X"] # 题目页示例 xyxyXX 的顺序
assert rank({"a": 1, "A": 1, "b": 1, "B": 1}) == ["a", "b", "A", "B"]
# 后两个断言就是并列规则本身:同次数时所有小写在前、再按字母顺序P2560 · 数组去重和排序
必做任务 1练习重点:计数表 + 首次出现下标表;按次数降序、同次数按首次出现输出;预计用时:15 分钟
完成标准:能说出「去重保序」「按数值排序」「按次数排序、同次数按首次出现」三种结果对样例各是什么
需要时查看提示
先建计数表,再建「首次出现下标」表(只在元素第一次出现时写入),排序键 (-次数, 首次下标),逗号连接输出。第 06 节用样例 1,3,3,3,2,4,4,4,5 手算出 3,4,1,2,5。输出顺序以题目页说明为准。
P2562 · 字符统计及重排
必做任务 2练习重点:区分大小写统计+三层并列规则排序输出;预计用时:25 分钟
完成标准:能整段说出排序键三元组,以及为什么默认 ASCII 顺序是错的
需要时查看提示
统计区分大小写,排序键 (-次数, 是否大写, 字母),即先按次数降序,同次数时所有小写在所有大写之前,再按字母顺序——用题目页示例 xyxyXX → x:2;y:2;X:2; 核对方向。输出格式每项后一个分号、包括最后一项,对照样例逐字符核对。
P2524 · API 集群负载统计
进阶练习 1进阶练习练习重点:按层级建计数表:键是(层级, 名字);预计用时:18 分钟
完成标准:能解释为什么键要带层级——同名字出现在不同层级要分开计数
需要时查看提示
把每条 URL 按 / 切开,以 / 开头的字符串切分后第 0 个是空串,第 i 段就在第 i 级。计数表的键用元组 (层级, 名字),查询时同样拼元组查,查不到输出 0。第 08 节的表给了样例的切分与键。
P2569 · 明日之星选举
进阶练习 2进阶练习练习重点:赞成、反对两张计数表;三级并列规则排序取前 n;预计用时:20 分钟
完成标准:能逐项说明排序键 (-赞成, 反对, 姓名) 三级如何对应题目要求
需要时查看提示
每张票按逗号切:第一段计入赞成表,第二段(可能没有)计入反对表;只被反对的人也要进赞成表(0 票)。排序键 (-赞成数, 反对数, 姓名)——注意反对数是升序(少的在前),姓名是字典序升序,方向不要弄反。第 08 节用样例 1 手算了四人的名次。
提交结果
提交结果说明与处理方法
- WA
答案错误
先检查并列规则:确认排序键已显式写出、没有依赖语言默认顺序,再用题目页示例核对大小写方向(P2562 是小写全部在前)和反对数方向——第 09 节的表给出了每种错误的具体输出。
- PE
格式错误
字符与次数的分隔符、末尾是否也有分号、每行一个还是同行输出,对照样例逐字符核对
- RE
运行错误
P2569 的票可能没有反对段——切分后先判段数再取下标;只被反对的人要先进表,排序键才不会缺键
- TLE
超时
查两处:存在性判断是不是用了 list 的 in;是不是在循环里反复调用计数方法(count)
- AC
通过
说一遍键和值各是什么,整体复杂度是多少
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。