模糊去重需用编辑距离归一化相似度聚类,避免o(n²)暴力比较;应分桶、贪心簇心、长度保护、标准化预处理,并按业务规则过滤关键差异。

用 std::string 做模糊去重,别直接比 ==
字符串“模糊去重”本质是聚类:把编辑距离小、语义相近的串归为一类,每类留一个代表。C++ 标准库不提供相似度函数,必须自己选算法并控制阈值。硬用 std::set 或 std::unordered_set 只能做精确去重,对 “用户中心” 和 “用户中心页” 这类完全无效。
常见错误是先写个 levenshtein 函数,再两两比较——O(n²) 复杂度,1000 个字符串就跑几秒,实际项目里根本不能忍。
- 优先用
similarity = 1 - (edit_distance / max_len)归一化,比纯编辑距离更适配阈值判断 - 阈值建议从
0.8起调;0.9太严(同义词可能被拆开),0.7太松(“登录” 和 “灯录” 也可能被合并) - 长度差超过阈值一半的串可提前跳过,比如
abs(a.size() - b.size()) > 0.5 * max(a.size(), b.size()),省掉大量无谓计算
用 std::vector + 简单聚类代替暴力两两比较
不建图、不调用 DBSCAN 这类重量级库,用“首串当簇心”的贪心策略足够应付预处理场景。时间复杂度降到 O(n·m),m 是平均串长,实测 5000 条日志字段能在 200ms 内完成。
关键不是追求最优聚类结果,而是让后续 NLP 或规则匹配少喂脏数据。
立即学习“C++免费学习笔记(深入)”;
- 把原始
std::vector<:string></:string>按长度分桶,同桶内才比较,避免“abc”和“authentication”这种无意义计算 - 每轮选未访问的首个字符串作
cluster_center,遍历剩余串,满足similarity >= threshold就标记为已访问,不加入结果 - 结果只保留每个簇的
cluster_center,顺序和原输入一致,方便 debug 对齐
levenshtein 实现要防栈溢出和越界
网上抄的递归版 levenshtein 在长串(>50 字符)下极易爆栈或超时;循环版若没限制行列大小,遇到中文混合数字的长 URL 会分配巨量内存。
真实业务中字符串常含 URL、错误码、JSON 片段,长度不可控,必须加保护。
- 入口加长度检查:
if (a.size() > 200 || b.size() > 200) return std::max(a.size(), b.size()); - 用一维数组滚动计算,空间从 O(m·n) 降到 O(min(m, n)),注意索引偏移和初始化
- 中文字符按 UTF-8 字节算(不是 wchar_t),否则
"你好".size()返回 6,但编辑距离应基于字(需先转std::u32string或用 ICU)——多数日志去重场景直接按字节处理即可,一致性比“正确性”更重要
阈值行为受编码和空格影响极大
看起来一样的字符串,"user_id" 和 "user_id "(末尾空格)、"user_id" 和 "user_id\u200b"(零宽空格)在 levenshtein 下距离为 1,但人眼几乎无法区分。这类噪声不清洗,阈值再准也没用。
聚类前必须做轻量标准化,否则相似度计算全在拟合噪声。
- 统一 trim:用
std::string::find_first_not_of和find_last_not_of去首尾空白 - 替换连续空白为单空格,防止
"a b"和"a b"被判远 - 小写转换(
std::tolower配 locale)——但注意不要对 Base64、Hex 码等做此操作,得按字段类型分流处理
真正难的不是算相似度,是搞清哪些差异该忽略、哪些差异必须保留。比如 HTTP 接口路径里的 /user/123 和 /user/456 编辑距离小,但业务上绝不能去重——得靠前缀规则或正则白名单兜底。










