LeetCode 609中等数组 · 哈希
在系统中查找重复文件 图解题解
这道题到底在问什么
给定目录信息列表 paths。每条形如「目录 文件名1(内容1) 文件名2(内容2) …」。请返回所有重复文件组,每组是内容相同的若干文件的完整路径「目录/文件名」。组的顺序与组内顺序任意。
- 输入
- paths=[ "root/a 1.txt(abcd) 2.txt(efgh)", "root/c 3.txt(abcd)" ]
- 输出
- [ ["root/a/1.txt","root/c/3.txt"] ] (abcd 内容相同)
- 输入
- paths=[ "root/a 1.txt(x)", "root/b 2.txt(y)" ]
- 输出
- [] (内容各不相同)
最优解:一步一步想明白
- 3记住这套「内容当 key、路径进桶、最后挑 ≥2 的桶」,下面每一帧都在套它。
- 4一共 5 条目录信息,拍平后是 6 个文件。桶现在是空的,等着内容相同的文件落进同一个桶。
- 5先看拆一个文件的招式:找到左括号,前面是文件名,括号里是内容。再把目录拼上文件名,就得到完整路径 root/a/1.txt。下面对每个文件都这么拆。
- 6解析出第 1 个文件 root/a/1.txt,内容是 abcd。这是个没见过的新内容,下一帧给它开一个新桶。
- 7root/a/1.txt 进了 0 号桶(内容 abcd),这个桶现在有 1 个文件。内容一样的文件,会一直往同一个桶里堆。
- 8解析出第 2 个文件 root/a/2.txt,内容是 efgh。这是个没见过的新内容,下一帧给它开一个新桶。
- 9root/a/2.txt 进了 1 号桶(内容 efgh),这个桶现在有 1 个文件。内容一样的文件,会一直往同一个桶里堆。
- 10解析出第 3 个文件 root/c/3.txt,内容是 abcd。这个内容之前出现过,对应的桶亮起来了,下一帧把当前路径也丢进去。
- 11root/c/3.txt 进了 0 号桶(内容 abcd),这个桶现在有 2 个文件。内容一样的文件,会一直往同一个桶里堆。
- 12解析出第 4 个文件 root/c/d/4.txt,内容是 efgh。这个内容之前出现过,对应的桶亮起来了,下一帧把当前路径也丢进去。
- 13root/c/d/4.txt 进了 1 号桶(内容 efgh),这个桶现在有 2 个文件。内容一样的文件,会一直往同一个桶里堆。
- 14解析出第 5 个文件 root/4.txt,内容是 efgh。这个内容之前出现过,对应的桶亮起来了,下一帧把当前路径也丢进去。
- 15root/4.txt 进了 1 号桶(内容 efgh),这个桶现在有 3 个文件。内容一样的文件,会一直往同一个桶里堆。
- 16解析出第 6 个文件 root/e/5.txt,内容是 zzzz。这是个没见过的新内容,下一帧给它开一个新桶。
- 17root/e/5.txt 进了 2 号桶(内容 zzzz),这个桶现在有 1 个文件。内容一样的文件,会一直往同一个桶里堆。
- 186 个文件都归桶了。可以看到 abcd 桶和 efgh 桶各装了好几个路径,zzzz 桶只有一个。接下来就看哪些桶里装了不止一个。
- 19归桶完成,现在从头到尾扫每个桶。判定规则很简单:桶里装了至少两个文件,这一桶就是一组重复;只装了一个的,跳过。
- 20看 0 号桶,内容 abcd,里面有 2 个文件:root/a/1.txt、root/c/3.txt。数量到了 2 个,这就是一组重复文件,收进答案。
- 21看 1 号桶,内容 efgh,里面有 3 个文件:root/a/2.txt、root/c/d/4.txt、root/4.txt。数量到了 2 个,这就是一组重复文件,收进答案。
- 22看 2 号桶,内容 zzzz,里面只有 1 个文件 root/e/5.txt。孤零零一个,凑不成重复组,跳过它。
- 23扫完三个桶,桶 0 和桶 1 都满足条件,一共 2 组重复文件;zzzz 那桶被跳过。
- 24最终答案是这 2 组:root/a/1.txt、root/c/3.txt 和 root/a/2.txt、root/c/d/4.txt、root/4.txt。每一组里的文件内容都完全相同,正是题目要找的重复文件。
⚠️ 容易写错的地方
✗ 错:用文件名当 key 来分组
✓ 对:必须用「内容」当 key
文件名不同也可能内容相同;只有内容才是判重依据
✗ 错:忘了过滤桶里只有 1 个的
✓ 对:只收文件数 ≥ 2 的桶
单独一个文件不构成重复组,会多输出一堆单元素组
✗ 错:截内容时把右括号也带进去
✓ 对:取左括号到右括号之间
内容是括号内的部分,末尾那个右括号要去掉,否则 key 不一致
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
from string import *
from operator import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def findDuplicate(self, paths: List[str]) -> List[List[str]]:
groups = defaultdict(list)
for entry in paths:
parts = entry.split()
root = parts[0]
for file in parts[1:]:
name, rest = file.split("(", 1)
content = rest[:-1]
groups[content].append(root + "/" + name)
ans = [sorted(v) for v in groups.values() if len(v) > 1]
ans.sort()
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
vector<vector<string>> findDuplicate(vector<string>& paths) {
unordered_map<string, vector<string>> groups;
for (auto& entry : paths) {
stringstream ss(entry);
string root, file;
ss >> root;
while (ss >> file) {
int p = file.find('(');
string name = file.substr(0, p);
string content = file.substr(p + 1, file.size() - p - 2);
groups[content].push_back(root + "/" + name);
}
}
vector<vector<string>> ans;
for (auto& [_, v] : groups) if (v.size() > 1) {
sort(v.begin(), v.end());
ans.push_back(v);
}
sort(ans.begin(), ans.end());
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public List<List<String>> findDuplicate(String[] paths) {
Map<String, List<String>> groups = new HashMap<>();
for (String entry : paths) {
String[] parts = entry.split(" ");
String root = parts[0];
for (int i = 1; i < parts.length; i++) {
String file = parts[i];
int p = file.indexOf('(');
String name = file.substring(0, p);
String content = file.substring(p + 1, file.length() - 1);
groups.computeIfAbsent(content, k -> new ArrayList<>()).add(root + "/" + name);
}
}
List<List<String>> ans = new ArrayList<>();
for (List<String> v : groups.values()) if (v.size() > 1) {
Collections.sort(v);
ans.add(v);
}
Collections.sort(ans, (a, b) -> {
int n = Math.min(a.size(), b.size());
for (int i = 0; i < n; i++) {
int c = a.get(i).compareTo(b.get(i));
if (c != 0) return c;
}
return a.size() - b.size();
});
return ans;
}
}复杂度
归桶
O(N)
N 为所有文件路径与内容的总字符数:解析每个字符各一次,按内容哈希入桶是常数操作
排序
另算
参考代码为输出稳定额外对组内路径、组间顺序排序,按路径数量与字符串比较成本计;排序不是本题必需步骤(题目说顺序任意)
空间
O(N)
哈希表把每条路径都存了一份
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 在系统中查找重复文件 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果文件内容非常大(GB 级别),还能直接拿内容当 key 吗?+
不合适,把整份大文件内容当字符串塞进哈希表会爆内存。实战里先比文件大小,大小相同的再算内容的哈希摘要(如 MD5、SHA)当 key,只在哈希摘要相同时才逐字节比对,避免把整文件读进内存。
如何确保你找出的重复不是误报?+
哈希摘要可能碰撞,两个不同内容也可能算出同一个摘要。最稳妥是在摘要相同后再做一次逐字节内容比对确认,或选用碰撞概率极低的强哈希,把误报压到可忽略。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 在系统中查找重复文件 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。