C++如何进行字符串的模糊去重(相似度阈值)?(聚类预处理)

2次阅读

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

C++如何进行字符串的模糊去重(相似度阈值)?(聚类预处理)

std::String 做模糊去重,别直接比 ==

字符串“模糊去重”本质是聚类:把编辑距离小、语义相近的串归为一类,每类留一个代表。c++ 标准库不提供相似度函数,必须自己选算法并控制阈值。硬用 std::setstd::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_idu200b"(零宽空格)在 levenshtein 下距离为 1,但人眼几乎无法区分。这类噪声不清洗,阈值再准也没用。

聚类前必须做轻量标准化,否则相似度计算全在拟合噪声。

  • 统一 trim:用 std::string::find_first_not_offind_last_not_of 去首尾空白
  • 替换连续空白为单空格,防止 "a  b""a b" 被判远
  • 小写转换(std::tolower 配 locale)——但注意不要对 Base64、Hex 码等做此操作,得按字段类型分流处理

真正难的不是算相似度,是搞清哪些差异该忽略、哪些差异必须保留。比如 http 接口路径里的 /user/123/user/456 编辑距离小,但业务上绝不能去重——得靠前缀规则或正则白名单兜底。

text=ZqhQzanResources