
LeetCode-Go 题解609. Find Duplicate File in System——以文件内容为键的哈希分组解法【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go本文以 leetcode/0609.Find-Duplicate-File-in-System/README.md 为主体结合仓库中的 Go 源码与测试用例展开。题目要求给定一组目录信息字符串把文件系统里内容完全相同的文件路径分组返回。读完本文你将掌握如何用字符串切分解析目录 文件名(内容)这种复合格式、如何用map[string][]string以“文件内容”为键完成去重分组以及面对真实文件系统时大文件、分块读取、误报校验该如何演进你的方案。1. 题目回顾英文原题LeetCode 609. Find Duplicate File in SystemGiven a listpathsof directory info, including the directory path, and all the files with contents in this directory, returnall the duplicate files in the file system in terms of their paths. You may return the answer inany order.A group of duplicate files consists of at least two files that have the same content.中文题目大意来自原文档给定一个目录信息列表包括目录路径以及该目录中的所有包含内容的文件你需要找到文件系统中的所有重复文件组的路径。一组重复的文件至少包括两个具有完全相同内容的文件。输入列表中的单个目录信息字符串的格式如下root/d1/d2/.../dm f1.txt(f1_content) f2.txt(f2_content) ... fn.txt(fn_content)。这意味着有 n 个文件f1.txt, f2.txt ... fn.txt的内容分别是f1_content, f2_content ... fn_content在目录root/d1/d2/.../dm下。注意n 1 且 m 0。如果 m 0则表示该目录是根目录。输出是重复文件路径组的列表对于每个组它包含具有相同内容的所有文件路径。文件路径的格式为directory_path/file_name.txt。2. 输入输出格式与约束2.1 单条目录信息的格式一条目录信息字符串由目录路径与若干文件条目组成中间以单个空格分隔root/d1/d2/.../dm f1.txt(f1_content) f2.txt(f2_content) ... fn.txt(fn_content)其中root/d1/d2/.../dm是目录路径若m 0表示该目录就是根目录每个文件条目形如f1.txt(f1_content)即文件名(文件内容)n 1即每条目录信息至少包含一个文件。输出的每个文件路径格式为directory_path/file_name.txt重复文件分组之间及组内顺序均任意any order。2.2 示例示例 1来自原文档Input: paths [root/a 1.txt(abcd) 2.txt(efgh),root/c 3.txt(abcd),root/c/d 4.txt(efgh),root 4.txt(efgh)] Output: [[root/a/2.txt,root/c/d/4.txt,root/4.txt],[root/a/1.txt,root/c/3.txt]]该示例中内容为efgh的文件有root/a/2.txt、root/c/d/4.txt、root/4.txt三个内容为abcd的文件有root/a/1.txt、root/c/3.txt两个因此按内容得到两组重复文件。示例 2来自原文档Input: paths [root/a 1.txt(abcd) 2.txt(efgh),root/c 3.txt(abcd),root/c/d 4.txt(efgh)] Output: [[root/a/2.txt,root/c/d/4.txt],[root/a/1.txt,root/c/3.txt]]去掉了示例 1 中的最后一条root 4.txt(efgh)因此内容为efgh的组只剩两个文件依然构成一组重复文件。2.3 约束条件原文档给出的约束如下1 paths.length 2 * 10^4目录信息条数1 paths[i].length 3000单条字符串长度1 sum(paths[i].length) 5 * 10^5所有字符串总长度paths[i]仅由英文字母、数字、/、.、(、)与空格组成可假设同一目录下不会出现同名文件或子目录每条目录信息代表唯一目录目录路径与文件信息之间由单个空格分隔注意sum(paths[i].length) 5 * 10^5这条约束它保证了直接使用字符串内容作为 map 的键是可行的——即使把全部内容都放进内存作为键值总规模也只有 500KB 量级这正是本题解法可以“朴素地以内容做键”的前提。3. 解题思路以内容为键的哈希分组原文档给出的核心思路非常清晰属于典型的“字符串解析 map 聚合”解析对每条目录信息字符串先用空格切分得到目录路径首段与若干文件条目后续各段分组对每个文件条目再定位(切出文件名与文件内容以文件内容为 key、以完整文件路径为 value 追加进 map筛选遍历 map凡是某个 key 对应的路径列表长度 2说明该内容出现重复把该列表加入答案。为什么用“内容”而不是“文件名”做键因为重复文件的判定标准是内容完全相同文件名相同不代表内容相同而内容相同则无论文件名如何都算重复。以内容为键天然把同内容的文件聚到同一个桶里这是整道题最核心的洞察。3.1 算法过程示意以示例 1 为例map 的构建过程如下文件内容key完整路径列表valueabcd[root/a/1.txt, root/c/3.txt]efgh[root/a/2.txt, root/c/d/4.txt, root/4.txt]两个 key 的列表长度都大于 1全部进入答案若某个 key 只出现一次如示例 2 去掉root 4.txt(efgh)前的情形需要对比则该键不会输出。4. 源码实现与逐行拆解仓库中该题的完整实现位于 609. Find Duplicate File in System.go与 README.md 中的代码完全一致package leetcode import strings func findDuplicate(paths []string) [][]string { cache : make(map[string][]string) for _, path : range paths { parts : strings.Split(path, ) dir : parts[0] for i : 1; i len(parts); i { bracketPosition : strings.IndexByte(parts[i], () content : parts[i][bracketPosition1 : len(parts[i])-1] cache[content] append(cache[content], dir/parts[i][:bracketPosition]) } } res : make([][]string, 0, len(cache)) for _, group : range cache { if len(group) 2 { res append(res, group) } } return res }逐行拆解如下parts : strings.Split(path, )按单个空格切分整条目录信息。首段parts[0]即目录路径后续段parts[1:]即各文件条目。这里选择Split而非strings.Fields是因为题目明确约束“目录路径与文件信息之间由单个空格分隔”按单个空格切分即可精确还原字段。dir : parts[0]取出目录路径后续拼接文件全路径时复用。bracketPosition : strings.IndexByte(parts[i], ()定位文件条目中(的位置。IndexByte只扫描单个字节字符比Index略高效且本题文件条目中只可能出现一个(。content : parts[i][bracketPosition1 : len(parts[i])-1](之后到字符串末尾前一个字符之间即为文件内容末尾是)用切片直接截取避免额外的字符串复制开销Go 的切片共享底层数组。cache[content] append(cache[content], dir/parts[i][:bracketPosition])以内容为键追加完整路径。parts[i][:bracketPosition]是(之前的文件名部分与目录拼接成directory_path/file_name.txt格式若键不存在append对 nil 切片同样有效。res : make([][]string, 0, len(cache))按 map 大小预分配答案切片容量减少扩容次数。if len(group) 2一组重复文件至少包含两个内容相同的文件因此只输出长度不小于 2 的组。5. 测试用例验证仓库为该题提供了配套测试文件 609. Find Duplicate File in System_test.go其中Test_Problem609直接以原文档的两个示例作为输入构造question609结构体对findDuplicate进行调用qs : []question609{ { para609{[]string{root/a 1.txt(abcd) 2.txt(efgh), root/c 3.txt(abcd), root/c/d 4.txt(efgh), root 4.txt(efgh)}}, ans609{[][]string{{root/a/2.txt, root/c/d/4.txt, root/4.txt}, {root/a/1.txt, root/c/3.txt}}}, }, { para609{[]string{root/a 1.txt(abcd) 2.txt(efgh), root/c 3.txt(abcd), root/c/d 4.txt(efgh)}}, ans609{[][]string{{root/a/2.txt, root/c/d/4.txt}, {root/a/1.txt, root/c/3.txt}}}, }, }该测试通过fmt.Printf打印输入与输出结果便于人工核对返回值与预期是否一致。两个示例覆盖了“多个重复组”与“单个重复组”两条路径使求解函数的全部语句块都得到执行——这一点在仓库根目录的 coverage.txt 中可以验证0609.Find-Duplicate-File-in-System/609. Find Duplicate File in System.go的每个代码块如5.47,7.29、7.29,10.35、10.35,14.4、16.2,17.30、17.30,18.22、18.22,20.4、22.2,22.12均记录了非零的执行次数语句覆盖率达到 100%。仓库通过根目录的 gotest.sh 统一生成覆盖率报告其核心命令为go test -covermodeatomic -coverprofilecoverage.txt ./leetcode/...若只想单独跑本题的测试可以在仓库根目录执行go test -v -run Test_Problem609 ./leetcode/0609.Find-Duplicate-File-in-System/6. 复杂度分析设P sum(paths[i].length)为所有输入字符串的总长度N为文件总数。时间复杂度O(P)。外层遍历每条目录信息内层对每个文件条目执行IndexByte与两次切片截取均为线性时间strings.Split整体也是 O(P)。最后的 map 遍历只访问键值对总代价仍是 O(P) 量级。空间复杂度O(P)。cache中保存了所有文件的内容键与完整路径键和值的字符串都来自对原字符串的切片共享底层数组不产生额外复制但 map 本身的结构开销与追加切片仍与总输入规模线性相关。在原文档的约束sum(paths[i].length) 5 * 10^5下这个解法无论是时间还是空间都足够宽裕它足够简单也因此成为本题的标准答案。7. Follow up从“玩具题”走向真实文件系统原文档特别指出这道题真正的价值在Follow up的五个问题上。下面结合计算机系统常识逐一展开以下均为工程层面的合理推演供读者结合实际场景验证7.1 面对真实文件系统如何搜索文件DFS 还是 BFS真实文件系统是一棵目录树遍历它既可以用 DFS递归或显式栈也可以用 BFS队列。两者的时间复杂度相同——都必须访问每个目录与文件区别在于DFS实现简单、内存占用与目录树深度成正比适合目录层次深的场景BFS可以按层级推进但需要队列保存同一层的所有目录句柄内存占用与目录树宽度成正比。无论选哪种遍历本身都不是瓶颈真正该做的是在遍历过程中先按文件大小分桶大小不同的文件内容必然不同再对同大小的文件做内容比对从而大幅减少需要深入比对的候选对。7.2 文件内容非常大GB 级别时如何修改方案题目版解法把完整内容直接作为 map 的 key这在 GB 级文件下不可行——内存会被撑爆。常见做法是先按文件大小分组只有大小完全一致的文件才有可能是重复的对同大小的文件计算哈希摘要如 MD5、SHA-256作为比较键而不是保存原始内容哈希相同的文件再进入下一步校验见 7.5。这样 map 中保存的是定长的摘要值而非变长的原始内容内存占用大幅下降。7.3 每次只能读 1KB 时如何修改方案分块读取是标准做法每次read最多 1KB用增量/滚动哈希在读取过程中边读边更新摘要例如按块累计哈希或使用 Rabin-Karp 一类的滚动哈希读完整个文件即可得到与内容一一对应的哈希值而无需把文件整体载入内存。具体实现时要注意缓冲区复用固定 1KB 的[]byte循环填充哈希更新逻辑必须与“顺序读取”严格对应保证同一文件无论读几次结果一致文件末尾不足 1KB 的残块也要纳入哈希计算。7.4 修改后的复杂度如何最耗时、最占内存的部分在哪推演一个基于“大小分桶 哈希摘要”的方案时间复杂度读文件是 I/O 密集的总体复杂度大致为 O(文件总字节数 哈希比较开销)其中磁盘读取与哈希计算是最耗时的部分空间复杂度最占内存的是存放文件路径与哈希摘要的表路径数量 × 路径平均长度 文件数量 × 摘要长度相比题目版原始内容不再驻留内存优化方向先用文件大小过滤绝大多数文件、对超大文件只取首/中/尾若干块的采样哈希做粗筛再对粗筛命中的候选做全量哈希最后逐字节比对确认以换取吞吐与内存的平衡。7.5 如何确保找到的重复文件不是误报哈希碰撞尤其是 MD5 这类非抗碰撞哈希可能把不同文件判成相同。工程上消除误报的通用链路是大小必须一致必要条件强哈希一致如 SHA-256逐字节比对确认对哈希相同的候选文件做流式逐字节比较只有完全一致才算重复。即哈希只做“快速排除与粗筛”最终以内容比对为准。这样既能保证正确性又能把昂贵的逐字节比对限制在极少数候选对上。8. 小结LeetCode 609 是一道“简单难度、但值得深挖”的题目核心解法字符串切分解析 以内容为键的map[string][]string分组再筛出长度不小于 2 的组时间复杂度 O(P)、空间复杂度 O(P)仓库配套完整实现见 609. Find Duplicate File in System.go示例用例见同名_test.go且 coverage.txt 记录其语句覆盖率为 100%延伸价值Follow up 系列问题把这道题从“字符串处理”引向真实文件系统的工程实现——文件大小分桶、流式哈希、分块读取、碰撞与误报处理这些思路正是生产环境重复文件检测工具的核心骨架。掌握本题你既巩固了 Go 的字符串操作与 map 使用也为理解真实文件去重系统的设计打下了基础。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考