☰
机试集合类题目核心考点:从set/dict用法到去重计数避坑指南
2026/10/14 3:57:00 网站建设 项目流程

从几年前第一次上机试考场,到现在帮身边同学改题、讲思路,我最大的感受是:机试真正拉开差距的,往往不是那些炫酷的算法,而是最基础的集合类题目会不会做得干净利落。最近整理刷题记录时刚好把“集合篇”推进到第10篇,顺便写一篇笔记,把我这段时间刷题踩过的坑、总结出来的套路一次性说清楚。

先说清楚这篇笔记的定位:我不打算把题目原文抄一遍然后丢一段代码,那没什么意思。重点想聊三件事:集合类题目在机试里的真实考查方式、每类高频题的核心建模思路、还有实打实的避坑经验。内容偏基础,但如果你正在准备某大厂机试,或者刚开始刷OJ,看这篇应该能帮你省掉不少自己摸索的时间。

我这里统称“某厂机试”,大家理解成国内几家头部厂商的在线笔试就行。备考逻辑、题型风格其实挺接近的,核心数据结构也就那几个,集合与映射更是绕不开的重头戏。

1. 集合类题目的真实定位与备考思路

1.1 机试里“考集合”到底考什么

很多人一看到机试就默认要去啃动态规划、图论、线段树,结果真上考场才发现,简单题和中档题占比很大,而这里面有一半以上可以用集合思想直接解决。所谓集合类题目,我总结下来其实就是四个字:判重、计数。

判重类包括:去重后输出、判断是否存在重复元素、求两个集合的交集并集差集、字符串中不重复的字符等。计数类包括:频率统计、众数、按次数排序、首批出现次数达到阈值的元素等。还有一些题表面是模拟、字符串处理甚至数学题,但中间某一步一定会用到set或dict去做状态记录,这其实也是集合类考点的变体。

机试出题人特别喜欢把这类基础考点藏在业务场景里。比如给你一串员工ID,要求统计连续打卡满7天的人;给你一段日志,要求统计报错级别为“ERROR”的模块出现次数;给你两个文件路径列表,求被两边同时引用的公共文件。这些场景剥掉外壳之后,核心操作就是去重和计数。

所以备考阶段不用一上来就迷信“每天刷5道hard题”的策略,先把集合类这种基座打牢,后面做其他类型时才能在状态转移、剪枝、记忆化搜索里熟练使用哈希结构,那时候的收益才更明显。

1.2 为什么set和dict是机试解题的“万能胶”

集合和映射之所以在机试里那么常用,最根本的原因是它们的查找成本低。Python里list的in是线性扫描,数据量到十万级就会明显变慢;而set和dict底层是哈希表,平均复杂度O(1),这个差距在机试场景里是决定性的。

举一个常见例子:判断两个数组中是否有公共元素。直觉写法是两层for循环,外层遍历n,内层遍历m,最坏情况n*m。数据量一大必然超时。换用set以后,先把其中一个数组转成哈希集合,再逐个查另一个数组的每个元素是否在集合里,整体复杂度是O(n+m)。这个变化不是优化代码风格,而是从算法本质上降低了复杂度。

映射dict则能承担更多“记录”工作。比如统计一个单词流里每个词的频次,关键是“边遍历边更新”,dict天然支持按键读写,配合collections.Counter,几行代码就能完成计数。再比如两数之和类题目,传统思路是排序后双指针,但其实用dict记录“已经见过的数与目标值的差”,一遍遍历就能找到配对,连排序都省了。

从备考策略来说,我的建议是:把set和dict当作解题第一步优先考虑的“万能胶”。读题之后先问自己一句:“这里需不需要快速判断某个东西是否出现过?需不需要给每个东西记一个数?”如果答案是肯定的,那这道题大概率就是集合类考点,直接用哈希结构切入准没错。

2. 核心考点细节:去重、计数与排序

2.1 去重不丢序的三板斧

字符串或数组去重是送分题,但有三个变体特别容易扣分。第一是“无序去重”,只要求把重复的去掉,对输出顺序没要求,这种情况直接set完事。第二是“保持首次出现顺序去重”,比如日志里的模块名,要求按首次出现的顺序输出,这时不能只用set,因为set不保证迭代顺序(Python 3.7以后dict会保持插入顺序,set本身仍是无序的)。

处理这种题,最稳的做法是“集合负责判重,列表负责顺序”:一个seen = set(),一个result = [],遍历时如果item不在seen里,就加入seen并result.append(item)。这个组合方案时间复杂度仍然是O(n),而且可读性强,不容易写错。

第三是“去重后按字典序或自定义规则排序”,也就是在去重基础上加一层sort。有人会先把列表去重再排序,有人会先排序再去重,两条路线都能过,但我更推荐“去重后排序”的思路,因为排序时直接得到最终顺序,边界情况更少。排序规则特别需要注意:如果要按照“出现次数降序、字典序升序”这种复合规则,就涉及到计数器和排序函数配合的问题,放在2.2里一起说。

2.2 计数器类题目的建模套路

计数器类题目的核心数据模型是dict,键是元素,值是次数。最推荐直接使用collections.Counter,因为它自带most_common方法,可以一次拿到按次数排序的列表。这个用法机试写起来非常舒适。

来看一个典型需求:统计一段文本中各单词出现次数,按“次数降序、字典序升序”输出前K个。

from collections import Counter def top_k_words(text: str, k: int) -> list: words = text.lower().split() counter = Counter(words) # 先按次数降序,再按单词字典序升序 sorted_items = sorted(counter.items(), key=lambda x: (-x[1], x[0])) return [w for w, _ in sorted_items[:k]]

这里的关键在key=lambda x: (-x[1], x[0])这个写法。-x[1]表示次数取负,让高的排前面;x[0]作为第二关键字,让字典序小的排前面。这个复合排序模式在机试里频繁出现,必须背熟。

还有一个常见变体是“求出现次数最多的元素”,如果只需要输出那一个值,直接用counter.most_common(1)[0][0]。如果有些题目要求相同次数时按元素大小升序输出,那就还是要走sorted,别指望most_common替你处理平局规则,它只会先出现先排。

2.3 字符集合与“哈希+排序”组合题

第三类高频考点是字符集合。给一个字符串,统计所有出现过的字符,判断两个字符串是否由相同字符组成(字母异位词的弱化版),或者找到字符串中第一个不重复的字符。这类题看起来简单,但坑特别多。

判断两个字符串是否互为字母异位词的通用做法是用Counter做频次计数器,两个Counter直接相等等价于“字符种类和数量都相同”,这一步比排序后比较要快不少。找第一个不重复字符则要遍历两次:第一次用Counter统计频次,第二次从头遍历找第一个频次为1的字符,复杂度依然是O(n)。

还有一类题会把“哈希+排序”组合在一起,比如“给定一组字符串,把字母异位词分组”。新手容易想到两两比较字符集合,但那会变成O(n^2 * len),数据一大直接超时。正确做法是把每个字符串的内部字符排序后作为dict的键,分组归类,最后再按题目要求的顺序输出。这种“排序后的字符串当哈希键”的技巧,属于我建议重点掌握的第二类建模套路。

3. 实操题解:三道高频集合题拆解

3.1 场景一:去重排序后输出

题目场景通常是这样:某系统接收了一批用户ID,允许有重复,要求去除重复后按ID从小到大输出,ID可能是数字也可能是字符串。我们按数字ID来写。

import sys def process(): data = sys.stdin.read().strip().split() if not data: return n = int(data[0]) ids = list(map(int, data[1:1 + n])) unique_sorted = sorted(set(ids)) print(" ".join(map(str, unique_sorted))) if __name__ == "__main__": process()

几个值得注意的细节:

第一,输入可能有多余空白和换行,用sys.stdin.read().strip().split()可以把整块输入一次读进来,自动切分成token,比一行一行处理更稳。第二,sorted(set(ids))一句话同时完成去重和排序,这是整套方案的核心。第三,输出用空格连接,最后一个元素后面不能多出空格,否则在严格比对的OJ上会被判格式错误。

这题还有变体:要求按输入顺序去重输出,那就要用seen加result的两步方案,而不是直接set。我自己刷题时在这个变体上栽过一次跟头,因为直接set后输出顺序全乱,和预期结果对不上,所以后来固定了“按需选择顺序策略”的习惯,看到题目要求的第一步就是确认输出顺序规则。

3.2 场景二:求两个集合的交集并按指定顺序返回

机试里交集类题目一般会嵌套业务描述,比如“两个部门提交了项目文件编号列表,请找出两边共同提交的文件编号,按编号大小升序输出”。本质就是求两个数组的交集,难点在于输出顺序和重复元素处理。

def intersection(nums1: list, nums2: list) -> list: set1 = set(nums1) return sorted(set(x for x in nums2 if x in set1))

这里的方向很重要。我先遍历nums2,同时判断x是否存在于set1中,这样天然去掉了nums2里的重复项,又不用额外操作nums1的重复问题。如果要求保留重复元素(即交集元素按其在原数组中重复次数的最小值出现),就要改成计数器思路:

from collections import Counter def intersection_with_dups(nums1: list, nums2: list) -> list: c1 = Counter(nums1) c2 = Counter(nums2) result = [] for k in c1: if k in c2: result.extend([k] * min(c1[k], c2[k])) return sorted(result)

两种需求差一个字结果差很多,考场上务必要读清楚“去重后的交集”还是“保留重复的交集”。这是我见过同学栽得比较多的一处。

3.3 场景三:基于字符计数的分组问题

字母异位词分组是标准高频题,题目会给一组字符串,要求把“组成字母相同但排列不同”的字符串归到同一组。业务场景可以包装成“订单中包含的商品构成相同就算同一类订单”之类,本质上仍然是异位词判定。

from collections import defaultdict def group_anagrams(strs: list) -> list: groups = defaultdict(list) for s in strs: key = ''.join(sorted(s)) groups[key].append(s) return list(groups.values())

sorted(s)把字符串内部的字符排序,得到规范化形式,不管字母怎么排列,排序后都会变成同一个串。这个串当key,原始字符串挂到对应列表里,一组异位词就自动聚合了。defaultdict(list)非常关键,它省去了“判断key是否存在,不存在就先建一个空列表”的样板代码。

复杂度方面,设总字符数为N,每组内每个字符串排序最坏是L log L(L为单个字符串长度),整体是O(N log L)。如果不排序,也可以改用Counter当键,但Counter对象不可哈希,需要转换成不可变的元组结构,代码反而不如“排序后当key”清爽。所以机试环境里我推荐优先使用“排序键”方案。

4. 做题踩坑实录与排查技巧

4.1 集合顺序与输出顺序不一致

这是集合类题目最常见的坑。set本质上是一个哈希集合,迭代顺序受哈希值、插入顺序、扩容策略等多重因素影响,你不能假设它保持插入顺序,也不能假设它是字典序。凡是“输出顺序有要求”的题,一律显式调用sorted,或者用前面说的“列表维护顺序、集合维护判重”方案。

有一次我在本地跑结果和OJ的样例输出完全一致,但一提交就错,后来发现是数据量大时哈希冲突导致集合内部顺序变化,本地小样例看着没问题,但OJ的大数据下暴露出真实迭代顺序。从那以后,我的习惯是:凡是输出要求里包含“按顺序”“从小到大”“按原顺序”这类字眼,绝不依赖set自身的顺序,必须显式排序。

4.2 读入多行数据与结尾空行问题

OJ的输入有时最后一行没有换行符,有时又带一个空行。用sys.stdin.read()整体读取后做strip(),再split(),可以彻底规避这类问题。反之,如果逐行input(),遇到结尾空行时容易多处理一个空字符串,或因为最后一行的换行符导致解析错位。

还有一个小坑是:第一行是测试用例数量T,后面跟着T组数据。这时候如果前面用了read().split()切片,一定要注意T本身占掉一个token,不要顺手把它当成第一组数据的第一个元素读进去。我刷题早期犯过好几次这个错误,后来统一写成固定套路:读全部token,先取第一个作为T或n,再按需求切片。

4.3 性能坑:两两比较改成哈希查找

刷集合题时最典型的超时原因是“两两比较”。比如判断数组A的每个元素是否在数组B中出现过,如果A和B长度各50000,O(n*m)就是25亿次操作,机试必超时。此时把B转成set,然后对A的每个元素做一次哈希查询,降到O(n+m),性能天差地别。

判断一道题要不要用哈希,有一个很简单的方法:看题目数据范围。如果两个数组规模能到10^5量级,两重循环基本意味着超时,这时候直接心里敲响警钟,优先考虑集合与映射。数据量小的时候怎么写都能过,数据量大的时候才是真正考验建模能力的时候,这也是机试评分拉开差距的地方。

4.4 机试环境下的自查清单

每次交代码前,我习惯过一遍这套清单:

  • [ ] 输出是否有多余空格或换行
  • [ ] 输入是否考虑了空数据、单元素数据
  • [ ] 边界值是否处理(比如K大于集合实际大小)
  • [ ] 是否误用了可变对象当dict键
  • [ ] 去重时输出顺序是否符合题目要求
  • [ ] 大数据量下是否存在O(n^2)循环

特别是“可变对象当键”这个点,Python里list不能当dict的键,set也不能,但tuple可以。有些题需要把数组状态作为键,比如记忆化搜索或BFS状态去重,这时候必须转换成元组再存。第一次遇到这个问题时,报错信息会直接告诉你unhashable type,但考场上看到报错再回改就会浪费几分钟,提前记住能省不少时间。

5. 刷题节奏与应试策略

5.1 从“涮题”到“总结”的转化

很多人刷题是“题海战术”,做完一道看一道,看上去数量上去了,但遇到变体还是不会。我做“集合篇”第10篇这个系列时,给自己定的规矩是:每做完一组题,必须归纳出一个可复用的“套路”,而不是停留在“这题我会了”。比如去重可以归纳出三个模板,计数可以归纳出Counter模板,分组可以归纳出“排序键”模板。下次再遇到新题,第一步就是匹配模板,第二步才是动手写代码。

这样刷题的效果非常明显。前期可能一道题要想很久,但模板积累到一定程度后,中档题基本能一眼看出方向,剩下的只是代码实现。这个过程本质上就是从“会做题”变成“会做一类题”。

5.2 机试考场的时间分配与心态

我的建议是“读题3分钟,建模5分钟,写码15分钟,自查5分钟”。前几分钟不急着写代码,先在草稿纸上把数据模型画出来,想清楚用set还是dict,需不需要排序,输出顺序是什么。建模想清楚以后,写代码只是一次“翻译”过程;反过来,边写边想很容易写到一半发现方案走不通,那才是真浪费时间。

机试还有一个隐藏考点:抗压能力。考场上时间紧张,遇到不会的题很正常。这时候最忌讳的是卡在一道题上死磕。我的经验是先把所有题都读一遍,挑自己有把握的先做,把能拿的分数拿到手,再回头啃难题。集合类题目难度通常不高,一般出现在前两道或中档位置,是拿分主力,务必保证这类题稳、准、快。

从个人刷题感受来说,集合类题目像是一把钥匙,解锁的不只是那几道题本身,而是整套“查重、计数、状态记录”的思维方式。后面接触记忆化搜索、滑动窗口、并查集时,你会发现底层逻辑依然离不开集合思想。所以别小看这个基础考点,把这里吃透了,后面能省很多力气。

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

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

立即咨询