c++如何实现字符串过滤屏蔽词_c++ AC自动机算法与匹配查找【方法】

不用std::string::find因时间复杂度高(O(n×m))、无法处理重叠匹配与前缀复用;AC自动机通过Trie树+失败指针+BFS构建,支持高效多模式匹配与完整子串覆盖。

为什么不用 std::string::find 做敏感词过滤

直接循环调用 find 查每个屏蔽词,时间复杂度是 O(n × m)(n 是文本长度,m 是词典总长度),遇到长文本+大词库(比如 10 万词)会明显卡顿。更麻烦的是,它无法处理“重叠匹配”和“前缀复用”,比如词典含 "ab""abc",文本为 "abc"find 可能只返回一次匹配,漏掉子串关系。

AC 自动机核心三步:构建失败指针 + 多模式匹配 + 输出优化

AC 自动机本质是 Trie 树 + BFS 构建的失败指针(fail),让匹配失败时能快速跳转到最长可匹配后缀节点。实际编码中,最容易出错的是 fail 指针初始化顺序和输出链(output link)的构建逻辑。

  • 构建 Trie 时,每个节点存 children[256](或 unordered_map),并标记 is_endid(对应哪个屏蔽词)
  • BFS 构建 fail:根节点子节点的 fail 指向根;其余节点 u 的子节点 v,其 fail[v] = children[fail[u]][c],若不存在则回退到 fail[fail[u]],直到根或找到
  • 输出优化:每个节点额外存 out 指针,指向最近一个真实匹配的终端节点(避免每次沿 fail 链向上遍历),可通过 BFS 时同步设置:out[u] = is_end[fail[u]] ? fail[u] : out[fail[u]]
struct Node {
    Node* children[256] = {};
    Node* fail = nullptr;
    Node* out = nullptr;  // 指向最近的终结节点
    int id = -1;          // 屏蔽词索引,-1 表示非终点
};

void build_ac_automaton(vector& patterns) { // 步骤1:建Trie root = new Node(); for (int i = 0; i < patterns.size(); ++i) { Node* u = root; for (char c : patterns[i]) { if (!u->children[(unsigned char)c]) u->children[(unsigned char)c] = new Node(); u = u->children[(unsigned char)c]; } u->id = i; }

// 步骤2:BFS建fail和out
queuezuojiankuohaophpcnNode*youjiankuohaophpcn q;
root-youjiankuohaophpcnfail = root;
for (int c = 0; c zuojiankuohaophpcn 256; ++c) {
    if (root-youjiankuohaophpcnchildren[c]) {
        root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnfail = root;
        root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnout = root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnid != -1 ? root-youjiankuohaophpcnchildren[c] : nullptr;
        q.push(root-youjiankuohaophpcnchildren[c]);
    } else {
        root-youjiankuohaophpcnchildren[c] = root;
    }
}

while (!q.empty()) {
    Node* u = q.front(); q.pop();
    for (int c = 0; c zuojiankuohaophpcn 256; ++c) {
        Node* v = u-youjiankuohaophpcnchildren[c];
        if (!v) continue;
        Node* f = u-youjiankuohaophpcnfail;
        while (f != root && !f-youjiankuohaophpcnchildren[c]) f = f-youjiankuohaophpcnfail;
        v-youjiankuohaophpcnfail = f-youjiankuohaophpcnchildren[c];
        v-youjiankuohaophpcnout = v-youjiankuohaophpcnfail-youjiankuohaophpcnid != -1 ? v-youjiankuohaophpcnfail : v-youjiankuohaophpcnfail-youjiankuohaophpcnout;
        q.push(v);
    }
}

}

匹配时如何高效收集所有命中位置和词ID

单次扫描文本,每步更新当前节点,再沿 out 链收集所有匹配。注意:不能只检查当前节点 id,必须递归查 out,否则漏掉“abc”匹配时同时触发“bc”(如果词典里有)。

  • 匹配循环中,cur = cur->children[(unsigned char)s[i]],若为空则跳到 cur->fail 继续找,直到成功或回到根
  • 每次移动后,用临时指针 p = cur,while(p) { 记录 p->id;p = p->out; },这样能拿到所有后缀匹配项
  • 若只需判断是否含屏蔽词(不关心位置),可设布尔标志 early-exit,一命中就返回 true
vector> find_all(const string& s) { // {pos, pattern_id}
    vector> res;
    Node* cur = root;
    for (int i = 0; i < s.size(); ++i) {
        unsigned char c = s[i];
        while (cur != root && !cur->children[c])
            cur = cur->fail;
        cur = cur->children[c] ? cur->children[c] : root;
    for (Node* p = cur; p; p = p-youjiankuohaophpcnout) {
        if (p-youjiankuohaophpcnid != -1) {
            res.emplace_back(i - patterns[p-youjiankuohaophpcnid].size() + 1, p-youjiankuohaophpcnid);
        }
    }
}
return res;

}

内存与编码细节:中文、大小写、特殊字符怎么处理

AC 自动机本身不关心字符语义,只依赖字节值。所以 UTF-8 中文会占多个字节,直接按 unsigned char 索引会崩。常见解法是预处理:把 UTF-8 字符串转成 Unicode 码点序列(如用 std::wstring_convert 或 C++11 ,但后者已弃用),再用 map 替代数组。更轻量的做法是统一转小写+正则清洗后匹配,或用 ICU 库做标准化。

  • 大小写不敏感?在插入词典前对每个 pattern 调用 transform(..., ::tolower),匹配时也对输入文本做同样转换
  • 允许模糊匹配(如星号通配)?AC 自动机不支持,得换 Aho-Corasick + NFA 扩展,或改用正则引擎(std::regex 性能差,慎用)
  • 高频更新词典?每次重建 AC 自动机开销大,可考虑双层结构:热词走哈希表快速匹配,冷词走 AC,或用增量式 fail 更新(极少实用)

真正上线时,最常被忽略的是 out 指针的正确性验证——它必须指向 *某个真实终结节点*,而不是任意 fail 节点。建议加单元测试:用 {"a", "ab", "bc"} 和文本 "abc",确保返回三个匹配(位置 0/0、0/1、1/2)。