Algoritmo de correspondência de strings multipadrões: princípio do autômato AC, análise de complexidade e implementação de código

Correspondência de sequência de padrões múltiplos

Cenários de correspondência de strings com vários padrões são comuns quando algumas plataformas bloqueiam termos confidenciais nas falas de determinados usuários.

Use um algoritmo de correspondência de string para encontrar termos sensíveis no texto e substitua-os por "***". Embora um único algoritmo de correspondência de cadeia de padrões possa ser usado para encontrar termos sensíveis um por um e depois substituí-los, em cenários reais, se o banco de dados de termos sensíveis for grande e houver muito conteúdo de texto a ser correspondido, o tempo de correspondência será ser muito longo, o que pode levar a Demora muito para enviar uma mensagem. Obviamente, isso levará a uma experiência de usuário degradada.

Portanto, um algoritmo de correspondência eficiente sob múltiplas cadeias de padrões é necessário para lidar com este cenário.

Filtrar palavras sensíveis com base na árvore Trie

A própria árvore Trie é um algoritmo baseado na correspondência de cadeias de vários padrões, que constrói várias cadeias de padrões em uma árvore Trie. Quando a cadeia de padrões muda, apenas a árvore Trie precisa ser alterada.

Ao combinar a string principal, combinamos a árvore Trie uma por uma, começando no primeiro caractere da string principal. Quando um caractere incorreto é correspondido, movemos o caractere inicial da string principal um caractere para trás e continuamos a corresponder a partir da raiz da árvore Trie, então, primeiro, precisamos apenas varrer a string principal uma vez para completar a correspondência de várias strings de padrão . A eficiência é muito maior do que usar a correspondência de string de padrão único.

Princípio do autômato AC

O algoritmo de correspondência de strings multipadrão acima baseado na árvore Trie é semelhante ao algoritmo de correspondência de força bruta na correspondência de strings de padrão único.Sabemos que o algoritmo de correspondência de força bruta pode melhorar a eficiência introduzindo a próxima matriz, ou seja, o Algoritmo KMP. Nesta correspondência de strings com vários padrões, devemos adicionar a ideia do próximo array a ele?

A resposta é obviamente sim, você só precisa transformar ligeiramente a árvore Trie. Claro, não é para adicionar o próximo array, mas para adicionar um próximo array a cada nó da árvore Trie.próximo ponteiro,Agora mesmoponteiro de falha

cConforme mostrado na figura, o ponteiro de falha do caractere está bcfna string c. Quando o combinamos abc, descobrimos que dele não corresponde ao caractere. Neste momento, podemos cpular para o ponteiro de falha bcde continuar a combinar f.
ponteiro de falha
Desta forma, não é mais necessário iniciar a correspondência novamente quando não há correspondência. A ideia é a mesma da próxima matriz. Se você não entende o princípio do algoritmo KMP, é recomendável entender primeiro a próxima matriz. e, em seguida, observe o ponteiro de falha para entendê-lo facilmente (leitura recomendada do algoritmo KMP: Famoso algoritmo de correspondência de string: análise do princípio do algoritmo KMP e implementação de código )

A próxima questão é: como encontrar o próximo nó apontado pelo ponteiro de falha de cada nó?
A árvore Trie na verdade inclui todas as strings de padrão. Suponha que agora exijamos co ponteiro de falha do nó na figura acima. A condição conhecida é que, ao combinar cem todos os lugares , abcseja o prefixo que correspondeu com sucesso à string principal e, em seguida, à string de padrão que precisa ser correspondido.Devem ser abcoutras strings de padrão com o sufixo substring como prefixo substring, e deve ser o prefixo substring de correspondência mais longo .

abcAs substrings de sufixo de ce , apenas as substrings de prefixo e sufixo bcde outras strings de padrão podem corresponder, portanto , o ponteiro de falha deve apontar para .bcfbcabccbcfc

Construa um autômato

As condições para construir um autômato são as seguintes:

  1. Construa uma árvore Trie
  2. Inicializar ponteiro de falha do nó

Primeiro, vamos dar uma olhada na estrutura de dados de cada nó:

public class AcNode {
    
    
    public char data; //数据域
    public AcNode[] children = new AcNode[26]; //字符集只包含a~z这26个字符
    public boolean isEndingChar = false; //记录模式串结尾字符
    public int length = -1; //记录模式串长度
    public AcNode fail; // 失败指针
    public AcNode(char data) {
    
    
      this.data = data;
    }
}

Pode-se descobrir que, em comparação com a árvore Trie, existe apenas mais umaponteiro de falha

Portanto, o primeiro passo na construção de um autômato é construir uma árvore Trie, que não será discutida em detalhes aqui (ver Princípios de construção de árvore Trie, cenários de aplicação e análise de complexidade ).

A questão que devemos considerar agora é: depois de construir a árvore Trie, como podemos obter os indicadores de falha de todos os nós?

Através da análise acima, já sabemos que para fazer com que o nó seja apontado pelo ponteiro de falha de um nó, na verdade precisamos encontrar a substring de prefixo mais longa que corresponda à substring de sufixo da parte anterior da string padrão onde o nó está localizado .

Em uma árvore Trie, o ponteiro de falha de um nó aponta apenas para o nó em seu nível superior. Portanto, o ponteiro de falha pode ser obtido usando o mesmo método do próximo array, ou seja, o ponteiro de falha do nó atual pode ser deduzido do nó onde o ponteiro de falha foi obtido.

O ponteiro de falha do nó raiz raiz é nulo, ou seja, aponta para si mesmo.Então, após obter o pponteiro de falha de um determinado nó q, como obter o ponteiro de falha de seu nó filho?

Situação um: Compare pos nós filhos e qos nós filhos entre si. Se forem iguais, o ponteiro de falha correspondente será encontrado.
Insira a descrição da imagem aqui
Situação 2: Se pos nós filhos de qnão forem iguais aos nós filhos de , então passamos qo ponteiro de falha para obter o nó correspondente e continuamos a procurar por nós filhos até que nulo seja encontrado, que é o nó raiz.
Insira a descrição da imagem aqui

Aqui está o código para construir o ponteiro com falha:

public void buildFailurePointer(AcNode root) {
    
    
    Queue<AcNode> queue = new LinkedList<>();
    root.fail = null;
    queue.add(root);
    while (!queue.isEmpty()) {
    
    
        AcNode p = queue.remove();//拿到节点p
        for (AcNode pc : p.children) {
    
    //遍历节点p的子节点
            if (pc == null) continue;
            if (p == root) {
    
    //root的子节点失败指针为root
                pc.fail = root;
            } else {
    
    
                AcNode q = p.fail;//找到p的失败指针节点q
                while (q != null) {
    
    
                	//查找p的子节点是否存在q的子节点
                    AcNode qc = q.children[pc.data - 'a'];
                    if (qc != null) {
    
    //存在则找到失败指针
                        pc.fail = qc;
                        break;
                    }
                    q = q.fail;//否则继续找下一个失败指针
                }
                if (q == null) {
    
    //直到找到null,则失败指针为root
                    pc.fail = root;
                }
            }
            queue.add(pc);
        }
    }
}

Após construir o ponteiro de falha, conforme mostrado na figura:
Insira a descrição da imagem aqui

Use correspondência de autômato AC

Assumindo a string principal str, a correspondência começa a partir do primeiro caractere da string principal e o autômato p=rootcomeça a combinar a partir do ponteiro

  1. Suponha pque o nó filho xseja igual a str[0], patualize para xe verifique se o ponteiro de falha p(atualmente apontado para x) é o fim de uma sequência de padrões. Em caso afirmativo, encontre uma sequência de padrões correspondente. Após o processamento, continue a combinar str[2].
  2. Se, após atingir uma determinada etapa, pnenhum caractere correspondente for encontrado nos nós filhos, então o ponteiro de falha será útil, ou seja, pesquisar nos nós filhos do nó apontado pelo ponteiro de falha.
public void match(char[] str, AcNode root) {
    
     // str是主串,root是自动机
    AcNode p = root;
    for (int i = 0; i < str.length; i++) {
    
    
        int idx = str[i] - 'a';
        //p的子节点中没有,就往p的失败节点的子节点中找,直到失败指针指向null为止
        while (p.children[idx] == null && p != root) {
    
    
            p = p.fail; // 失败指针发挥作用的地方
        }
        p = p.children[idx];//找到匹配的字符后,p更新指向到这个节点
        if (p == null)// 如果没有匹配的,从 root 开始重新匹配
            p = root; 
        AcNode tmp = p;
        while (tmp != root) {
    
     // 找到已经匹配到的模式串
            if (tmp.isEndingChar == true) {
    
    
                int pos = i - tmp.length + 1;
                System.out.println(" 匹配起始下标 " + pos + "; 长度 " + tmp.length);
            }
            tmp = tmp.fail;
        }
    }
}

Eficiência de correspondência do autômato AC

  1. A complexidade da construção da árvore Trie é O(m*len), onde mestá o número de sequências de padrões e lené o comprimento médio das sequências de padrões.
  2. Ao construir o ponteiro de falha, o mais demorado é procurar o ponteiro de falha camada por camada no loop while. Cada loop sobe pelo menos uma camada e a altura da árvore não excede. Portanto, o tempo lencomplexidade é O(K*len), K é o nó na árvore Trie. número.
  3. As duas etapas acima só precisam ser executadas uma vez para completar a construção, o que não afeta a eficiência da correspondência com a string principal.Durante a correspondência, a coisa mais demorada também é o código para o próximo ponteiro de falha no loop while , então a complexidade do tempo é, se o O(len)comprimento da string principal for n, então a complexidade total do tempo correspondente éO(n*len)

Na verdade, ao combinar palavras sensíveis, o comprimento médio das palavras sensíveis não será muito longo. Portanto, a eficiência de correspondência do autômato AC é muito próxima. Somente em casos extremos, a eficiência será degradada O(n)para a mesma eficiência de correspondência da árvore Trie .

Os casos extremos são os seguintes:
Autômato AC extremo

Acho que você gosta

Origin blog.csdn.net/m0_37264516/article/details/86177992
Recomendado
Clasificación