超大文件去重需分块+外部排序:先按行切分内存可控的块,每块用sort+unique去重后写盘;再用最小堆多路归并,边取最小值边跳重复,并保留跨块边界行比对。

超大文件去重不能全读进内存,必须分块+外部排序
直接用 std::unordered_set 或 std::set 读完整个文件会 OOM——哪怕文件只是 50GB、每行平均 100 字节,也意味着约 5 亿条字符串,内存占用轻松突破 20GB(含指针、哈希桶、字符串堆分配开销)。外部排序是唯一可行路径:把文件切分成内存可容纳的块,各自排序去重,再归并合并时跳过重复项。
分块排序阶段:用 std::sort + std::unique 处理每个 chunk
关键不是“快”,而是“可控内存 + 可复现顺序”。每块读入后必须做两件事:排序、原地去重。注意 std::unique 不真正删除元素,需配合 erase:
std::vector lines; // ... 读入当前 chunk(例如 100 万行) std::sort(lines.begin(), lines.end()); auto last = std::unique(lines.begin(), lines.end()); lines.erase(last, lines.end());
- 不要用
std::stable_sort——无必要且更慢 - 每块写回磁盘前建议用
std::ofstream以文本模式逐行写,避免二进制兼容性问题 - 块大小建议设为物理内存的 1/4~1/3(例如 16GB 内存,单块用 3~4GB),留足空间给 OS 缓冲和归并过程
多路归并去重:用 std::priority_queue 管理各 chunk 的当前最小行
归并不等于简单合并,必须边取最小值边跳过连续重复项。核心逻辑是:每次从 k 个 chunk 文件中取出当前首行,选最小者输出;若该行与上一个已输出行相同,则丢弃,不推进对应 chunk 的读取位置。
实现时用 std::priority_queue 存 std::tuple<:string int std::ifstream>(内容、chunk 编号、文件流指针),但注意:std::priority_queue 默认最大堆,需传入 std::greater 得到最小堆;且每次 pop 后,要从对应文件再读下一行并 push 回队列。
立即学习“C++免费学习笔记(深入)”;
- 必须预读每 chunk 的第一行,初始化队列;空 chunk 直接跳过
- 遇到 EOF 时,pop 后不再 push,同时减少活跃 chunk 数
- 重复判断只依赖「当前弹出值」与「上一轮输出值」的
==比较,不跨 chunk 缓存历史值
实际性能瓶颈往往在磁盘 IO,不是 CPU
SSD 上顺序读写速度可达 500MB/s,但随机小 IO(如频繁 seek + 单行读)会暴跌到 1MB/s 以下。因此:
- 所有 chunk 文件务必顺序写入、顺序读取;避免
seekg回退或跨行跳读 - 归并阶段对每个 chunk 流使用带缓冲的
std::ifstream(默认 buffer 小,可rdbuf()->pubsetbuf扩大,或直接用fgets+FILE*更稳) - 如果目标文件系统是 ext4/xfs,关闭 atime 更新(
mount -o noatime)能小幅提升吞吐 - 别用
std::endl写结果文件——它强制 flush,换成'n'+ 最后一次flush()
真正的难点不在算法逻辑,而在于 chunk 边界对重复项的切割:同一行可能被切在两个 chunk 末尾/开头,导致漏去重。必须确保分块按完整行切分(用 n 定位,而非固定字节数),且归并时保留上一块最后一行用于跨块比对——这点极易被忽略。