☰
PTA第六章组合数据类型:字典与集合AC攻略
2026/10/14 14:25:45 网站建设 项目流程

1. 第六章到底在考什么

我印象里浙大版《Python 程序设计》的第六章,主题是组合数据类型,重点就是字典和集合。很多同学在PTA上一看到最高频的题目类型,就是给你一堆数据,让你统计出现次数、按频率排序、去除重复,或者让你判断某个键存不存在。听起来很简单,但真正在评测机上跑起来,一堆人卡在超时、格式错误、改字典报错这类奇奇怪怪的地方。

这一章的意义在于:从单变量思维过渡到批量数据思维。前几章你还在写a + b、写循环遍历列表,到第六章,你要开始回答“这一堆数据里谁出现得最多”“两个集合之间差了什么”这类问题。如果你还是只用列表遍历去实现,很多题在PTA上会直接TLE(超时)。因为评测机对大数据量很敏感,它的时间限制不会因为你“思路是对的”就网开一面。

所以要过第六章,你不只是要记住dict.get()怎么用,还得知道什么时候该用集合、什么时候用字典、什么时候用默认字典defaultdict,以及列表、字典、集合之间怎么互相转换。这些东西学完,你后面刷题的速度会提上来一大截,因为很多题本质就是“查字典”和“去重排序”的变体。

1.1 理解组合数据类型的三种容器

第六章里的题,绕不开三个核心容器:字典、集合、元组。它们各管一摊:

  • 字典负责“键值映射”,适合做统计、索引、缓存。
  • 集合负责“有无关系”,适合做去重、交集、差集。
  • 元组负责“固定打包”,适合做不可变的多字段记录,比如(姓名, 分数),但在PTA题目里,元组更多是作为字典的键值使用。

做题前我习惯先问自己一个问题:题目需要“存一个值”,还是“记录一组值”,还是“快速判断存在性”?如果是记录单一个值的状态,用字典最方便;如果只是判断哪些数据出现过,用集合最省事;如果一个对象有多个属性且不允许修改,元组是最稳的载体。

这里要特别提醒:千万不要一上来就用列表硬存。列表的查找是线性扫描,300个数据没事,300万个数据就直接击穿时间限制。我第一次刷第六章的时候,统计词频用了列表一个个数,小例子能过,提交后一大片的运行超时,后来换成字典,瞬间AC。转变这个习惯,比背十个函数都有用。

1.2 为什么字典和集合总在一道题里出现

第六章很多题目的典型模式是:先读入一堆数据,用字典统计,最后对统计结果排序输出。也有的题是先对两个数据源做去重合并,然后判断公共部分。这类题之所以高频,是因为它们对应了真实编程里的数据清洗场景——比如日志分析、用户标签合并、交易记录汇总。

这类题的核心难点其实只有一个:如何正确组织键值关系。我见过很多同学代码写得没问题,但AC不掉,问题常出在“键的类型没统一”、“读入时空格没处理干净”、“排序的规则没写对”。所以我在下面写的解答思路,全都是按照PTA上常见测试点的出题规律来拆的:正常输入、大数量输入、边界输入、格式敏感输出。

2. 我整理的AC解答通用思路

在刷完第六章所有基础题之后,我把题目分成了三类:统计类、去重类、转换类。这个分类不是我随便定的,而是根据解题模板不同来区分的。把题目分类之后,做题效率高很多,因为你不需要每道题都从头想数据结构,直接套对应模板,再改改键名和输出格式就行。

2.1 统计类题目的通用三步模板

统计类是第六章的重头戏,什么“统计选课人数”“统计词频”“统计数字出现次数”都属于这一类。我的标准解法分三步:

  1. 初始化字典或默认字典。
  2. 遍历输入数据,对每个数据做dic[key] = dic.get(key, 0) + 1或使用defaultdict(int)。
  3. 按题目要求排序和格式化输出。

第一版代码往往会写出这种风格:

d = {} for item in data: if item not in d: d[item] = 1 else: d[item] += 1

这种写法没错,但性能略差,代码偏长。我更推荐用get的写法:

d = {} for item in data: d[item] = d.get(item, 0) + 1

原因很简单:它把“判断存在 + 取值 + 加一 + 赋值”压缩成了一行,而且避免了item not in d的两次哈希查找。实际跑起来差距虽然不大,但代码更简洁,对评测机也更友好。

如果统计时需要默认值为列表,那么一定要用defaultdict(list)。比如要记录每个班级有哪些学生,普通的dict需要你先判断键存在再append,容易漏写分支。换成defaultdict(list)后,直接d[cls].append(name),少写一行更稳。

2.2 去重与排序的基本功:集合与列表转换

第六章里去重题很多。PTA上的出题风格特别喜欢给你一个含重复元素的序列,让你输出所有不同元素,或者按指定顺序输出。最直接的方式就是用集合set,因为集合本身就是“唯一化”的天然工具。

但要注意一个高频坑:集合是无序的,可PTA经常要求“按输入顺序输出”或“按字典序输出”。这时候不能直接遍历集合。我见过太多同学在这里栽跟头——先转集合,然后直接 for 循环遍历,发现输出顺序和样例不一样,就开始怀疑是不是评测机有问题。其实只要题目要求“按照输入顺序去重输出”,你需要用另一种方法:遍历原列表,用一个集合记录已经出现过的元素,只把第一次出现的元素收集起来:

seen = set() result = [] for x in data: if x not in seen: seen.add(x) result.append(x)

这么做的原理是:集合负责“记忆”,列表负责“顺序”。如果你用set(data)再排序,那是“按大小排序”,不是“保持原序”,两者完全不同。

如果题目要求返回所有不重复元素并按字典序输出,那么可以先sorted(set(data)),这样又简洁又稳。其实这类题真正考的,是你能否分清“排序规则”和“去重规则”是两件事。

2.3 遍历字典的格式选择

字典遍历在第六章的输出环节非常重要。你得知道三种遍历方式,并清楚它们分别适用于什么场景:

  • for key in d:—— 遍历键,适合只需要键的情况。
  • for key, value in d.items():—— 同时遍历键值,最常用。
  • for value in d.values():—— 只遍历值,适合只统计数据的情况。

PTA输出时经常会要求先按键的某种规则排序,再输出键和值。这时候实际用的代码是这样的:

for key in sorted(d): print(key, d[key])

注意sorted(d)默认按键排序,如果你需要按值排序,就必须用items()配合lambda:

sorted(d.items(), key=lambda x: x[1], reverse=True)

这种写法在词频TopN题目里几乎是标准答案。但还有一个细节:sorted(d.items(), ...)返回的是元组列表,你输出每一项时可以直接解包:

for k, v in sorted(d.items(), key=lambda x: x[1], reverse=True): print(f"{k}: {v}")

这里面的lambda x: x[1]的意思是“取元组第二个元素作为排序依据”。很多新手写成key=lambda x: x[0],结果按键排了序,全错。我在教一个同学的时候,他不理解为什么输出顺序不对,我说你把x[1]改成分数,马上AC,他改完直呼“原来 x 是元组”。

3. 手把手过几道有代表性的AC题目

光讲理论不够,我挑了三个不同层次的题目,把我自己的AC代码和思考过程写下来。这几道题我都在PTA上实际提交过,直接作为参考模板是没问题的。

3.1 案例一:统计每位同学选修的课程数量

题目大概意思是:输入若干行,每行是“课程名 学生姓名”,要求统计每位学生选修了多少门课,最后按学生姓名字典序输出。这道题考的就是基础字典统计,但要注意键是学生名,值是课程数量;同一门课同一个学生可能出现多次,需要去重。

我当时的AC代码是这样写的:

import sys d = {} for line in sys.stdin: line = line.strip() if not line: continue course, name = line.split() # 利用集合去重,保证同一课程同一学生只计一次 if name not in d: d[name] = set() d[name].add(course) for name in sorted(d): print(name, len(d[name]))

为什么要用集合set()而不是列表?因为一门课可能重复出现在输入里,对同一个学生来说,重复的课程不应该重复计数。用set天然去重,最后直接len()就是课程数量,省去了手动判断重复的逻辑。

这里的坑在于,有些同学会想用d[name] = d.get(name, 0) + 1,这样如果同一行重复出现,就会多计数。评测数据里偏偏就会给你这种重复行,专门卡不带去重的解法。所以这个案例的核心不是统计数量,而是“去重后再统计”。

另外注意读入方式。在PTA上,输入的行数不确定,用sys.stdin按行循环是最稳定的写法。如果你用input()配一个固定的循环次数,很容易因为输入行数多于预期而处理不完整,或者空行导致split()出问题。

3.2 案例二:词频统计并输出出现次数最多的前N个

这道题是典型的字典统计加排序。题目给一段英文文章,要求统计单词出现次数,输出频率最高的几个词,同频率时按字典序升序。这道题难倒不少人的是中文题目经常说“按出现次数从高到低、字典序从小到大”这种双条件排序。

我的AC代码简化后是这样:

import sys text = sys.stdin.read().split() cnt = {} for word in text: cnt[word] = cnt.get(word, 0) + 1 # 先按次数降序,再按单词字典序升序 items = sorted(cnt.items(), key=lambda x: (-x[1], x[0])) top_n = int(input()) # 通常是前N个 for word, num in items[:top_n]: print(f"{word}:{num}")

这里的核心是排序键(-x[1], x[0])。第一个元素写成负数,就能让次数降序排列;第二个元素是单词本身,字典序升序。很多人会写key=lambda x: (x[1], x[0]),结果发现频率是升序,与要求相反。所以这个-x[1]其实是个小技巧:sorted默认升序,你不想reverse=True影响字典序部分的话,就可以用负数来伪装降序。

另一个容易出错的地方是分割单词。split()默认按任意空白字符分割,可以处理空格、换行、Tab,这在绝大多数英文文章题里都够用。但如果有标点符号粘连,比如hello,,就需要使用正则或者清洗标点的步骤。PTA的这类题目通常出题时会避开这样的复杂情况,因为它主要考字典排序,而不是字符串清洗。

3.3 案例三:求两个集合的交集、并集与对称差

集合运算题在第六章是送分题,但如果你不会用运算符,就会写出冗长的循环。PTA常见题目是:输入两个集合的元素,输出交集、并集、补集或对称差。Python集合运算符可以直接搞定:

a = set(map(int, input().split())) b = set(map(int, input().split())) inter = a & b # 交集 union = a | b # 并集 sym_diff = a ^ b # 对称差:a中有或b中有但不同时有

输出时注意题目要求,通常要排序输出,而且元素之间没有多余的空格或按指定格式。比如“输出集合元素个数以及排序后的元素列表”。我的做法是:

print(len(inter)) print(" ".join(map(str, sorted(inter))))

这里的坑有两个:第一,sorted(inter)是升序列表,如果你直接遍历集合,输出的顺序完全随机,评测机一比较字符串就报WA(答案错误);第二,map(str, ...)是把每个数字转成字符串,因为join只能拼接字符串。很多同学忘了转字符串,直接" ".join(sorted(inter)),然后程序报TypeError。这种错误属于格式坑,在PTA上特别常见。

4. 我踩过的坑和排查技巧实录

前面讲的是通解,下面这部分是我真在金六章做题时摔过的跟头。很多问题不看评测反馈根本想不到,我把它们整理成话放在这里,能帮大家少走不少弯路。

4.1 遍历字典时不要直接改字典

我最早写第六章一道题时,需要在统计完成后删除某些不符合条件的键,我直接写:

for k in d: if d[k] < 2: del d[k]

结果一提交,报RuntimeError: dictionary changed size during iteration。这个错误翻译过来就是“遍历字典的过程中,字典的长度变了”。Python不允许你在迭代它的时候增删键值对,因为这会破坏内部结构。如果你真要去除部分元素,正确做法是先收集要删除的键,循环结束后再统一删除:

to_remove = [k for k in d if d[k] < 2] for k in to_remove: del d[k]

或者直接用字典推导式生成新字典:

d = {k: v for k, v in d.items() if v >= 2}

这个坑在PTA上不会立刻暴露,因为你可能数据小的时候侥幸没事,但只要有一次遍历中恰好删了键,程序就直接崩。记住了:迭代时不要改容器;想过滤,就构建新字典。

4.2 集合与字典的键要求“可哈希”

第六章函数题里偶尔会要求把列表作为字典的键。很多同学下意识写成d[list1] = 1,结果报TypeError: unhashable type: 'list'。因为列表是可变类型,不能作为键。这时候要么把列表转换成元组,要么把列表转成字符串。

遇到这种题,我一般会把列表转元组,因为元组不可变、可哈希。例如:

key = tuple(sorted(lst)) d[key] = d.get(key, 0) + 1

如果你用str(lst)也能当键,但这样会丢失结构信息,后续恢复比较麻烦。优先用元组。另外,集合本身也是不可哈希的,不能作为字典键,但frozenset可以。PTA的题目里很少需要用到frozenset,知道这一点就够了。

4.3 输入读取选择直接决定是否超时

第六章有些题目数据量很大,比如上万行输入。如果你在循环里用for _ in range(n): line = input(),其实也能过,因为input()本身并不算慢。最致命的写法是在循环里不停拼接字符串:

result = "" for x in data: result += str(x) + " " print(result)

这在一万次循环里没问题,但如果是十万、百万级的数据,字符串是不可变对象,每次+=都会生成一个新的字符串,把旧字符串整体拷贝一遍。复杂度瞬间变成O(n^2),测试点一大就直接TLE。

更好的方式是用列表收集,再统一join:

parts = [] for x in data: parts.append(str(x)) print(" ".join(parts))

" ".join(parts)是线性复杂度,即使数据量大也扛得住。这个习惯如果从第六章养成,到后面写复杂题目时收益很大。

4.4 输出格式里最容易被忽略的空格、换行与冒号

PTA的判断题和程序填空题暂且不提,选择题和编程题里,输出格式错一点就是白给。第六章经典的格式坑包括:

  • 要求“每个数之间用一个空格隔开,行末不能有多余空格”。很多人的代码是循环里print(each, end=' '),这样最后一个元素后面也有空格,评测机会对比输出字符串,所以直接WA。正确做法是用join拼接,或者用索引判断。
  • 要求输出“键: 值”,冒号后面有空格。有人写成f"{k}:{v}",差一个空格就是格式错误。每次都读清楚题目再动手,不要自己脑补。
  • 要求“每个输出占一行”,但有的人用end=' '把所有结果挤在一行。输出换行是最容易检查的,提交前先在本地把所有样例跑一遍,肉眼核对一次即可。

我后来把这种格式问题总结成一个自查动作:写完代码先不急着提交,手动构造简单输入跑一下,然后对比题目给的样例输出,逐字符看一遍。这个动作浪费不了30秒,但能避免一半以上的非算法错误。

5. 从AC到会写的进阶建议

第六章的题不算难,但刷完不等于你会写这类代码。我从自己带人刷题的经验来看,不少同学AC了一堆题,但过两天拿到新题还是不会做。问题出在只背了答案,没有形成自己的解题流程。

5.1 拿到题目先画出数据关系

我现在每做一道题,第一件事不是写代码,而是在草稿纸上画出输入数据的格式和期望输出的格式,然后用一条线把从输入到输出的映射关系标出来。比如统计题就是从一条记录中取出键、取出值,更新字典;去重题就是从原始序列中筛掉seen里的元素。这个映射图一旦画出来,代码就是把图中的每一步翻译成Python。

这个方法听起来简单,但特别管用。因为第六章的题大多数据结构单一,最难的不是语法,而是理不清该用什么容器、键是什么、值是什么。画完图,你一眼就能看到“这里需要一个集合来去重”“那里需要一个字典来计数”。

5.2 用本地造数据代替瞎猜

如果某道题提交后WA了,第一反应不要是重新读题,而是先本地验证。我会自己构造一组包含重复值、边界值、空值的小数据,一步步跑代码,看哪里和预期不符。比如英文词频题,我会造一句话里出现大小写不同、重复单词、标点符号附近的情况,然后看代码输出是否符合题意。

本地验证的好处是你能逐步打印中间值。养成随手画调试输出的习惯,在循环里插入一句print(d),看统计过程是否正确。这比你瞪着眼睛看代码逻辑更有效果。

5.3 给自己留一个常用API自查表

第六章你只需要掌握十几个函数和操作,真的不多。我不建议背所有函数,但建议你归纳一份自己的小表单,比如:

  • 字典:get、items、keys、values、setdefault、update
  • 集合:add、remove、discard、union、intersection、difference
  • 列表转元组:tuple(list)
  • 字典推导式:{k: v for ...}
  • 集合推导式:{x for x in ...}
  • 按值排序:sorted(d.items(), key=lambda x: x[1])

这个表格不一定全面,但它覆盖了PTA第六章至少70%的题目所需的操作。做题卡住的时候先查表,比上网翻文档更快。

最后分享一个我个人的小习惯:每次AC一道题以后,我会把代码里最关键的那一行单独抄进一个笔记文件里,旁边写上这一行解决了什么问题。这样积累一段时间以后,你会发现你已经形成了一种“看到题目就能反射出数据结构”的能力。刷题的意义不是AC本身,而是让你以后写任何脚本、处理任何数据时,下意识选择最合适的容器和操作。把第六章吃透,后面再遇到文件读写、函数式编程和面向对象的内容,你会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询