LeetCode 609. 在系统中查找重复文件
题目描述


题意分析
输入已经给出每个目录中的文件名和完整内容,不需要真的访问磁盘。把内容完全相同的文件分在一组,返回它们的完整路径,只保留至少有两个文件的组。
判重依据是内容,而不是文件名:不同目录、不同名字的文件也可能重复,同名文件的内容则可能不同。
解法:哈希表按内容分组
核心思路
[!blue]
哈希表的键保存内容字符串,值保存拥有该内容的完整路径列表。每处理一个文件,就把它的路径追加到对应内容组。相同内容必然落入同一组,不同内容仍由字符串相等性区分,无需枚举文件对。按题目的单空格分隔格式,第一段是目录,后面每段是
文件名(内容)。找到文件段中的第一个左括号,之前是文件名,之后到最后一个右括号之前是内容;完整路径由目录、斜杠和文件名拼接,不包含括号或内容。所有文件处理完后,内容相同的路径已经完整聚合。组大小大于一就表示重复,大小为一则丢弃。题目允许任意输出顺序,因此组内、组间都不需要额外排序。
解题步骤
- 逐条输入取出目录。
- 解析每个文件段的文件名和内容。
- 按内容分组,并加入完整路径。
- 筛掉只有一个文件的组。
代码实现
class Solution {
public List<List<String>> findDuplicate(String[] paths) {
// 以内容为键:相同内容自动落入同一桶,无需两两比较。
Map<String, List<String>> contentMap = new HashMap<>();
for (String path : paths) {
String[] parts = path.split(" ");
String dir = parts[0];
// 从 1 开始跳过目录段;只有目录时循环不进,边界自然成立。
for (int i = 1; i < parts.length; i++) {
// 文件名不含括号,第一个 '(' 即分界点。
int idx = parts[i].indexOf('(');
String fileName = parts[i].substring(0, idx);
// 右端取 length - 1,正好剥掉结尾的 ')'。
String content = parts[i].substring(idx + 1, parts[i].length() - 1);
String fullPath = dir + "/" + fileName;
contentMap.computeIfAbsent(content, key -> new ArrayList<>()).add(fullPath);
}
}
List<List<String>> res = new ArrayList<>();
for (List<String> group : contentMap.values()) {
// 只有一个路径说明内容不重复,按题意不输出。
if (group.size() > 1) {
res.add(group);
}
}
return res;
}
}
import "strings"
func findDuplicate(paths []string) [][]string {
// 以内容为键:相同内容自动落入同一桶,无需两两比较。
contentMap := make(map[string][]string)
for _, path := range paths {
parts := strings.Split(path, " ")
dir := parts[0]
// 跳过目录段;只有目录时切片为空,循环不进。
for _, file := range parts[1:] {
// 文件名不含括号,第一个 '(' 即分界点。
idx := strings.Index(file, "(")
fileName := file[:idx]
// 右端取 len(file)-1,正好剥掉结尾的 ')'。
content := file[idx+1 : len(file)-1]
fullPath := dir + "/" + fileName
contentMap[content] = append(contentMap[content], fullPath)
}
}
res := [][]string{}
for _, group := range contentMap {
// 只有一个路径说明内容不重复,按题意不输出。
if len(group) > 1 {
res = append(res, group)
}
}
return res
}
复杂度分析
- 时间复杂度:期望 $O(L+P)$,L 为输入总字符量,P 为生成全部完整路径的总字符量。
- 空间复杂度:$O(L+P)$,保存内容键、完整路径及解析中间结果。
进阶:真实文件系统与大文件
- 如何遍历目录:DFS 和 BFS 都能找到全部文件。使用目录迭代器逐项 DFS 时,只需保留当前目录链,栈深度随目录深度增长;BFS 要保存整层待访问目录,目录宽时队列可能很大。两者都不能省掉对每个文件的检查。
- 内容达到 GB 级别:不要把完整内容作为内存中的键。先按文件大小分组,只有一个文件的大小组直接排除;同大小组可以先比较少量固定位置的数据,仍可能相同的文件再计算完整内容摘要。抽样相同只是候选,不能直接认定重复。
- 每次只能读 1 KB:循环读取固定大小的块,增量更新摘要状态,直到文件末尾。只保留缓冲区和摘要,不必把所有块拼回内存;最后一块可以少于 1 KB。
- 成本与优化:设文件数为
F、目录数为D、实际读取的总字节数为R,其中包含最后的内容复核。遍历和内容处理的期望时间为 $O(F+D+R)$,另计路径字符串处理成本;大文件场景主要耗时在磁盘读取。内存主要用于路径、文件大小、摘要和分组信息,内容只需固定大小的读缓冲。先按大小和抽样过滤,可以减少完整读取次数。- 如何避免误报:摘要相同不等于内容必然相同。对摘要相同的候选,再分块逐字节比较,按实际内容进一步分组,只有长度与每个字节都一致才认定重复。摘要碰撞只会增加比较工作,不会让不同内容进入同一结果组。
关键点总结
[!green]
- 判重键是内容,不是名字或路径。
- 完整路径需要目录、斜杠和文件名,不包含内容括号。
- 重复目录前缀会在多条完整路径中重复出现,复杂度计入生成路径量。
易错点总结
[!yellow]
- 按文件名分组:同名异内容文件被误判重复。
- 把目录段当文件解析:它没有文件内容括号。
- 输出单元素组:没有满足重复条件。
- 完整路径漏掉分隔斜杠:返回格式错误。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 49. 字母异位词分组 | 中等 | 同样按规范键分组,本题键是文件内容,不能按文件名分组。 |
| 652. 寻找重复的子树 | 中等 | 相同子树需要结构和值的签名,相同文件只需内容键,都是用签名聚合同类对象。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!