最近后台好几个准备秋招的同学来问华为那道“相册重复图片检测”的机考题,尤其集中在通用软件和嵌入式方向。这道题被放在非AI方向的第一题,100分,题目本身不算难,但很能看出一个人的基本功:会不会用哈希表、能不能稳准狠地处理输入输出、知不知道什么时候该排序。很多人一看名字里有“图片”两个字就开始慌,其实它考的根本不是图像处理。这篇文章我会把题目还原成可复现的版本,然后用Java、C++、Python各写一遍完整题解,再讲讲我实际操作中踩过的各种坑。不管你是刚配好Java环境的新手,还是正在VSCode里调C/C++环境准备刷题的选手,这篇解析都能直接用上。
1. 题目场景与考点拆解
1.1 题目长什么样
华为机考的题目描述一般不会特别长,但会包一层现实场景的外壳。这道“相册重复图片检测”,核心场景就是手机相册里存了大量照片,某一张照片可能在相册里重复出现多次,要求你把所有重复的照片找出来。
因为机考环境不可能真的给你图片文件,所以题目通常会把图片抽象成一个字符串ID。下面是我按历年机考常见形式还原出来的题目描述,各批次细节可能略有出入,但核心逻辑是一样的:
text复制输入描述:
第一行一个正整数 N,表示图片数量。
接下来 N 行,每行一个字符串,表示一张图片的ID。
ID由小写字母和数字组成,长度不超过20。
限制:1 <= N <= 100000
输出描述:
按字典序升序输出所有出现次数大于1的图片ID,每个ID占一行。
如果没有任何重复图片,输出 NO。
比如输入:
text复制6
abc123
def456
abc123
ghi789
def456
abc123
输出应该是:
text复制abc123
def456
因为 abc123 出现3次,def456 出现2次,而 ghi789 只出现1次,不算重复。注意输出顺序必须是字典序升序,不是按出现次数排,也不是按输入顺序排。
这种题目在机考里非常常见,它把“图片重复检测”抽象成了“字符串频率统计”。所以你别被“相册”两个字带偏,本质上就是给一堆字符串,让你统计频次。后面如果题目描述变成“文件名重复检测”“URL重复检测”,做法一模一样。
1.2 这道题到底在考什么
我后来复盘这道题,发现它作为第一题是有用意的,考察点非常集中,几乎不整花活:
- 哈希表的基本使用。能不能想到用哈希表统计频次,是这道题的第一道分水岭。
- 字符串处理能力。读入字符串、判断相等、按字典序排序,都是最基础的字符串操作。
- 时间复杂度的估计。如果只会两层循环暴力比较,N到10万时直接超时,这题就废了。
- 输出格式的严谨性。有没有重复都要有输出,重复ID不能重复输出,顺序必须是字典序。
另外,题目括号里写了“通软&嵌软&测试&算法&数据科学”,很多同学看到“数据科学”就觉得要考机器学习、图像识别,其实完全不是。非AI方向的机考第一题,不管投的是哪个岗位,考察的都是通用编程能力。数据科学岗位也不例外,第一题照样是基础数据结构,不会让你上模型。这一点想清楚,心态就能稳住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路的演进与选择
2.1 最直白的暴力法为什么不行
正常人看到“找重复”的第一反应是两两比较:拿第一张图片和后面所有图片比,第二张再往后比,遇到相同就记下来。这个思路没有错,但复杂度完全不可接受。
假设每次比较两个ID字符串平均需要花L的时间,L大概是20以内,那总的比较次数是 N*(N-1)/2。当N=100000时,比较次数接近50亿次。哪怕每次比较很快,50亿这个量级在现代评测机上也是铁定超时的。
我用一个生活化类比解释给朋友听:这就像让一万个人互相握手,每个人都要问对方“你手里的照片和我一样吗”,最后要问接近五千万次。题目只给你1到2秒运行时间,根本不可能完成。所以暴力法虽然能想出来,但永远不该出现在提交代码里。
2.2 哈希表解法:一次遍历搞定
正确做法是把所有ID放进哈希表里,以ID为键、出现次数为值,边遍历边统计。
- 第一步,遍历所有图片ID,执行
map[id]++。 - 第二步,遍历哈希表,把次数大于1的ID收集到一个列表里。
- 第三步,对列表按字典序排序。
- 第四步,如果列表为空输出
NO,否则逐行输出。
时间复杂度是 O(N + U log U),其中 U 是去重后的ID数量。最坏情况下 U 最多等于 N,所以整体是 O(N log N) 级别。空间复杂度 O(N),用来存哈希表,完全在题目限制内。
有人可能会问:先对所有ID排序,然后扫描相邻位置找重复,不是也能做吗?当然能,而且排序解法的时间复杂度是 O(N log N),在这个数据量下也能通过。但我更推荐哈希表,原因是它更灵活。如果题目进一步问“哪个ID重复次数最多”“重复次数总和是多少”,哈希表一行代码就能扩展,而排序解法就得重写。面试培训时我经常强调:能用哈希表解决的就别用排序,因为哈希表才是这类频次题的标准做法。
2.3 别被“图片检测”带偏
这里必须单独说一点,因为它害了不少人。真实场景里的“相册重复图片检测”,其实有两层含义:
第一层是完全相同的重复,比如同一张照片被复制了两份,字节内容完全一致。这种用哈希摘要(MD5、SHA-1)就能解决。
第二层是相似重复,比如同一张照片被微信发过一次被压缩了,或者被截图工具裁剪过,尺寸、清晰度都变了,但肉眼看起来是同一张图。这种就需要感知哈希,比如 aHash、dHash、pHash,提取图片指纹,再用汉明距离判断相似度。
但是,这道机考第一题完全是第一层抽象——“重复”被定义成字符串ID相同。试卷里根本没有图片文件,也不可能让你调图像库。所以看到“图片检测”千万别往感知哈希、深度网络上想,老老实实做字符串频率统计。笔试考的是你能不能识别出底层数据结构,不是考你会不会图像处理。
3. 三种语言实现与逐行解析
3.1 Java 实现
Java 在华为机考里属于主流选择,写起来中规中矩,关键是把 Scanner 读入和 HashMap 统计用对。
java复制import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = Integer.parseInt(sc.nextLine().trim());
Map<String, Integer> countMap = new HashMap<>();
for (int i = 0; i < n; i++) {
String id = sc.nextLine().trim();
countMap.put(id, countMap.getOrDefault(id, 0) + 1);
}
List<String> result = new ArrayList<>();
for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
if (entry.getValue() > 1) {
result.add(entry.getKey());
}
}
Collections.sort(result);
if (result.isEmpty()) {
System.out.println("NO");
} else {
for (String id : result) {
System.out.println(id);
}
}
}
}
几个关键点:
- 读入第一行 N 时,我用的是
sc.nextLine()而不是sc.nextInt(),因为后面每一行都要读字符串。如果先用nextInt()读了整数,输入缓冲区里会残留一个换行符,紧接着的第一次nextLine()会读到空字符串,导致第一个ID丢失。这个坑我见过好几个人踩,统一用nextLine()再trim()最省心。 getOrDefault(id, 0) + 1是 Java 8 之后才有的方法,用起来非常方便。如果评测平台的 JDK 版本太老不支持,可以改成Integer cnt = countMap.get(id); countMap.put(id, cnt == null ? 1 : cnt + 1);。- 最后收集结果时,遍历的是
entrySet(),不是keySet(),因为我们需要判断 value 是否大于1。
3.2 C++ 写法与性能关键点
C++ 在机考里的优势是执行速度快,但输入输出函数选不好反而会拖后腿。
cpp复制#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
unordered_map<string, int> cnt;
string id;
for (int i = 0; i < n; i++) {
cin >> id;
cnt[id]++;
}
vector<string> ans;
for (auto &p : cnt) {
if (p.second > 1) {
ans.push_back(p.first);
}
}
sort(ans.begin(), ans.end());
if (ans.empty()) {
cout << "NO\n";
} else {
for (auto &s : ans) {
cout << s << '\n';
}
}
return 0;
}
C++ 版本有几个细节值得单独说:
ios::sync_with_stdio(false); cin.tie(nullptr);这两行一定要写。它们能关掉 C++ 标准流和 C 标准流之间的同步,让cin/cout速度快很多。虽然这道题的 N 只有10万,不写也可能过,但养成习惯没有坏处。- 统计用
unordered_map而不是map。map底层是红黑树,插入是 O(log N),在数据量大时有额外开销。unordered_map底层是哈希表,平均 O(1),更合适。 unordered_map的遍历顺序是不确定的,所以结果必须收集进 vector 之后再sort,这一步不能省。- 我用的是标准头文件
#include <iostream>、<string>、<unordered_map>、<vector>、<algorithm>,没有用bits/stdc++.h。如果你在 VSCode 里配置的是 MinGW 或 MSVC 环境,有些编译器对bits/stdc++.h支持不好,直接用标准头文件最稳,到哪里都能编译。
3.3 Python 写法与输入性能
Python 代码是最简洁的,但要特别注意读取方式。
python复制import sys
def main():
data = sys.stdin.read().split()
if not data:
return
n = int(data[0])
cnt = {}
for i in range(1, n + 1):
s = data[i]
cnt[s] = cnt.get(s, 0) + 1
res = [k for k, v in cnt.items() if v > 1]
res.sort()
if res:
print("\n".join(res))
else:
print("NO")
if __name__ == "__main__":
main()
这段代码里最关键的是 sys.stdin.read().split()。它会把所有输入一次性读进来,再按空白字符切成 token 列表,所以换行、空格都不用管。有些人习惯写 input() 循环:
python复制n = int(input())
for i in range(n):
id = input().strip()
...
这种写法在小数据量下没问题,但在机考里如果输入达到10万行,input() 的逐行调用会有额外开销。虽然10万次可能也不至于超时,但 sys.stdin.read() 是更稳健的写法,值得刻意练习。
cnt.get(s, 0) + 1 是 Python 字典统计频率的标准写法。如果某个 key 不存在,get 会返回默认值0,然后加1再赋值。最后筛选结果用了列表推导式,一行搞定,非常 Pythonic。
3.4 三种写法横向对比
| 语言 | 时间复杂度 | 空间复杂度 | 代码量 | 主要风险 |
|---|---|---|---|---|
| Java | O(N log N) | O(N) | 中等 | Scanner 读入坑多,内存开销相对大 |
| C++ | O(N log N) | O(N) | 中等 | 忘了加速 IO,或用了 map 导致性能下降 |
| Python | O(N log N) | O(N) | 最少 | 输入方式不当,极端情况下超时 |
三种语言在 N=10万这个数据量下都能稳定通过。我的建议很直接:平时用哪个顺手就选哪个,千万别在考场上临时换语言。很多人觉得 C++ 性能最好、Java 最保险,结果自己最熟的是 Python,反而选了不熟的语言写出一堆低级 bug,得不偿失。
4. 常见问题与排查技巧实录
4.1 输出顺序错乱
这是这道题最常见的丢分点。HashMap、unordered_map、dict 都是无序结构,如果你直接遍历哈希表然后逐个输出,得到的结果顺序是完全不可预期的。很多同学本地跑样例时碰巧顺序对了,一提交就是错。
正确的做法一定是在收集完结果之后,单独排序再输出。Java 用 Collections.sort,C++ 用 sort(ans.begin(), ans.end()),Python 用 res.sort()。排序规则是字典序,也就是字符串默认的从小到大排序。注意在 ASCII 字典序里,数字字符排在字母字符前面,比如 "123" 会排在 "abc" 之前,这是题目要求的,不要自己改成别的规则。
4.2 输入输出相关的经典坑
我整理了几条高频翻车点,基本都是我在本地调试时真实遇到过的:
- Java 的
nextInt()后接nextLine(),会先把残留换行读掉,导致第一个ID变成空串。解决办法是读整数时也用nextLine()再Integer.parseInt。 - C++ 的
cin/cout没有加sync_with_stdio(false)。在 N 达到10万甚至更高时,输入输出会成为瓶颈,加上之后通常能快一个数量级。 - Python 的
input()循环在超大输入下偏慢。改用sys.stdin.read().split()以后,性能差距非常明显。 - 输出格式里多打了空格。如果每个 ID 后面跟一个空格再换行,评测系统可能判错。最简单的输出方式就是逐行
print或用'\n'连接。
4.3 边界用例速查表
写代码容易,边界条件才是真正决定过不过的关键。我建议任何题写完都要跑一遍这些用例:
| 用例场景 | 输入示例 | 期望输出 |
|---|---|---|
| 只有一张图片 | 1 + abc |
NO |
| 所有图片都不重复 | 3 + abc def ghi |
NO |
| 所有图片全部重复 | 3 + abc abc abc |
abc |
| 重复的ID出现多次 | 5 + abc def abc abc ghi |
abc |
| 多个ID重复且顺序乱 | 6 + ghi abc abc ghi def def |
abc def ghi |
特别要注意第四个场景,很多新手会在结果里把 abc 输出三次。正确做法是只要它出现过重复,就输出一次,而不是输出它的出现次数次。
5. 在线测试与笔试实战建议
5.1 如何在评测系统里自测
华为机考一般用的是牛客网或类似的自研平台,提交界面通常有个“自测”功能,可以在输入框里粘贴测试数据,点击运行后看输出。这个功能一定要善用,尤其是样例过了但提交不过的时候,自测功能能帮你快速定位问题。
本地开发环境也可以用命令行重定向来做批量测试。比如环境是 Linux 或 macOS,可以先准备好一个 input.txt 文件,里面放测试输入,然后分别执行:
- Java:
java Main < input.txt - C++:
./a.out < input.txt - Python:
python main.py < input.txt
这样就不用每次手工复制粘贴输入,也能方便地对比多个用例的输出。我在刷题时习惯准备三五个输入文件,覆盖正常用例、边界用例和超大数据量用例,提交前全部跑一遍。
5.2 考场上如何稳拿这100分
这类100分的第一题,实际目标不是“做出最优解”,而是“在15分钟内稳定AC”。我给自己定的流程是:
先把输入输出框架写好,不管核心逻辑对不对,先把数据读进来、再把结果打出去。这样至少保证了程序能跑。
然后写核心逻辑,第一遍就用哈希表,别一开始想花式优化。等 AC 了以后,如果还有时间再考虑是否优化。
接着构造边界用例。最危险的情况是样例过了、提交却0分,多半就是某个边界没处理。所以样例跑完以后,一定要测 N=1、全部不重复、全部重复这三种情况。
最后检查输出格式。看看有没有多余空格、有没有漏输出空行,确认没有重复时才输出 NO。
另外,如果考场上发现题目给的样例和平时刷的题不太一样,不需要慌,拿样例反推输入输出的格式就行。只要数据结构是字符串,大概率还是哈希表的老套路。
5.3 工程延伸:把题解变成真实的相册查重工具
笔试部分讲完了,但既然题目叫“相册重复图片检测”,我也想多聊一句工程上的延伸。如果你真的想做一个相册查重工具,思路其实可以分两级:
第一级,用内容哈希去掉完全相同的图片。比如计算每张图片的 MD5 或 SHA-1,相同哈希就是相同文件。
第二级,用感知哈希去相似图片。我经常用 dHash,它的思路很简单:把图片缩放到9x8像素,转成灰度图,然后逐行比较相邻像素的亮度大小关系,生成64位二进制指纹。两张图片指纹的汉明距离越小,说明越相似。
python复制from PIL import Image
def dhash(img, hash_size=8):
img = img.convert("L").resize(
(hash_size + 1, hash_size),
Image.LANCZOS
)
diff = []
for row in range(hash_size):
for col in range(hash_size):
left = img.getpixel((col, row))
right = img.getpixel((col + 1, row))
diff.append(1 if left > right else 0)
return sum(bit << i for i, bit in enumerate(diff))
然后计算两个指纹的汉明距离:
python复制def hamming_distance(h1, h2):
return bin(h1 ^ h2).count("1")
如果汉明距离小于阈值(比如5),就认为两张图可能是同一张图的不同压缩版本。
但这里我必须强调:这是工程实现方案,不是这道理科机考第一题要考的东西。考试时千万别去写 dHash,因为题目根本没有图片文件给你。我之所以把它写在最后,是想让你看到一个更完整的技术脉络——从一道简单的哈希表题,延伸到真实生产环境的图片判重方案,这些知识是一脉相承的。
我在实际带人刷题时发现,能把这层工程关系想明白的同学,对“哈希”这个工具的理解会深很多。机考只要求你 AC,但理解为什么这么写,能让你在后续面试环节聊出真正的技术深度。
