Coincidencia de cadenas de múltiples patrones
Los escenarios de coincidencia de cadenas de múltiples patrones son comunes cuando algunas plataformas bloquean términos sensibles en los discursos de ciertos usuarios.
Utilice un algoritmo de coincidencia de cadenas para buscar términos confidenciales en el texto y reemplazarlos con "***". Aunque se puede utilizar un algoritmo de coincidencia de cadenas de patrón único para encontrar términos confidenciales uno por uno y luego reemplazarlos, en escenarios reales, si la base de datos de términos confidenciales es grande y hay mucho contenido de texto para comparar, el tiempo de coincidencia será ser demasiado largo, lo que puede llevar a que se tarde mucho en enviar un mensaje. Obviamente, esto conducirá a una experiencia de usuario degradada.
Por lo tanto, se necesita un algoritmo de coincidencia eficiente bajo múltiples cadenas de patrones para hacer frente a este escenario.
Filtrar palabras sensibles según el árbol Trie
El árbol Trie en sí es un algoritmo basado en la coincidencia de cadenas de múltiples patrones, que construye múltiples cadenas de patrones en un árbol Trie. Cuando la cadena de patrón cambia, solo es necesario cambiar el árbol Trie.
Al hacer coincidir la cadena principal, hacemos coincidir el árbol Trie uno por uno comenzando desde el primer carácter de la cadena principal. Cuando coincide un carácter incorrecto, movemos el carácter inicial de la cadena principal hacia atrás un carácter y continuamos haciendo coincidir desde la raíz de el árbol Trie, por lo que primero, solo necesitamos escanear la cadena principal una vez para completar la coincidencia de múltiples cadenas de patrones . La eficiencia es mucho mayor que utilizar la coincidencia de cadenas de patrón único.
Principio del autómata de CA
El algoritmo de coincidencia de cadenas de patrones múltiples basado en el árbol Trie mencionado anteriormente es similar al algoritmo de coincidencia de fuerza bruta en la coincidencia de cadenas de un solo patrón. Sabemos que el algoritmo de coincidencia de fuerza bruta puede mejorar la eficiencia al introducir la siguiente matriz, es decir , el algoritmo KMP, en esta coincidencia de cadenas de múltiples patrones, ¿Deberíamos agregarle la idea de la siguiente matriz?
La respuesta es obviamente sí, solo necesita transformar ligeramente el árbol Trie. Por supuesto, no se trata de agregar la siguiente matriz, sino de agregar una siguiente matriz a cada nodo del árbol Trie.siguiente puntero,Ahora mismopuntero de falla。
c
Como se muestra en la figura, el puntero de falla del carácter está bcf
en la cadena c
. Cuando lo emparejamos abc
, encontramos que d
no coincide con el carácter. En este momento, podemos c
saltar al puntero de falla bcd
y continuar haciendo coincidir f
.
De esta manera, ya no es necesario comenzar a hacer coincidir nuevamente cuando no hay coincidencia. La idea es la misma que la siguiente matriz. Si no comprende el principio del algoritmo KMP, se recomienda comprender primero la siguiente matriz. Y luego observe el puntero de falla para comprenderlo fácilmente (lectura recomendada del algoritmo KMP: Algoritmo de coincidencia de cadenas famoso: análisis de principios del algoritmo KMP e implementación de código )
La siguiente pregunta es, ¿cómo encontrar el siguiente nodo señalado por el puntero de falla de cada nodo?
El árbol Trie en realidad incluye todas las cadenas de patrón. Supongamos que ahora requerimos c
el puntero de falla del nodo en la figura anterior. La condición conocida es que cuando coincide c
en todas partes , abc
es el prefijo el que ha coincidido con éxito con la cadena principal, y luego la cadena de patrón. que debe coincidir Deben ser abc
otras cadenas de patrón con la subcadena de sufijo como subcadena de prefijo, y debe ser la subcadena de prefijo coincidente más larga .
abc
Las subcadenas de sufijo de c
y solo pueden coincidir las subcadenas de prefijo y sufijo bc
de otras cadenas de patrón , por lo que el puntero de error debe apuntar a .bcf
bc
abc
c
bcf
c
construir un autómata
Las condiciones para construir un autómata son las siguientes:
- Construir un árbol Trie
- Inicializar el puntero de falla del nodo
Primero, echemos un vistazo a la estructura de datos de cada nodo:
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;
}
}
Se puede encontrar que, en comparación con el árbol Trie, solo hay uno más.puntero de falla
Por lo tanto, el primer paso en la construcción de un autómata es construir un árbol Trie, que no se discutirá en detalle aquí (consulte Principios de construcción del árbol Trie, escenarios de aplicación y análisis de complejidad ).
La pregunta que debemos considerar ahora es, después de construir el árbol Trie, ¿cómo podemos obtener los indicadores de falla de todos los nodos?
A través del análisis anterior, ya sabemos que para que el puntero de falla de un nodo apunte al nodo, en realidad necesitamos encontrar la subcadena de prefijo más larga que coincida con la subcadena de sufijo de la parte anterior de la cadena de patrón donde se encuentra el nodo .
En un árbol Trie, el puntero de falla de un nodo apunta al nodo solo en su nivel superior. Por lo tanto, el puntero de falla se puede obtener usando el mismo método que la siguiente matriz, es decir, el puntero de falla del nodo actual se puede deducir del nodo donde se obtuvo el puntero de falla.
El puntero de falla de la raíz del nodo raíz es nulo, es decir, apunta a sí mismo, entonces, después de obtener el p
puntero de falla de un determinado nodo q
, ¿cómo obtener el puntero de falla de su nodo secundario?
Situación uno: Compare p
los nodos secundarios de y q
los nodos secundarios entre sí. Si son iguales, se encontrará el puntero de falla correspondiente.
Situación 2: Si p
los nodos secundarios de q
no son iguales a los nodos secundarios de , pasamos q
el puntero de falla para obtener el nodo correspondiente y continuamos buscando nodos secundarios hasta encontrar nulo, que es el nodo raíz.
Aquí está el código para construir el puntero fallido:
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);
}
}
}
Después de construir el puntero de falla, como se muestra en la figura:
Utilice la combinación de autómatas de CA
Suponiendo la cadena principal str
, la coincidencia comienza desde el primer carácter de la cadena principal y el autómata p=root
comienza a coincidir desde el puntero.
- Supongamos
p
que el nodo secundariox
es igual astr[0]
, luegop
actualice ax
y luego verifique si el puntero de fallap
(actualmente apuntado ax
) es el final de una cadena de patrón. Si es así, busque una cadena de patrón coincidente. Después del procesamiento, continúe haciendo coincidir str [2]. - Si después de alcanzar un determinado paso,
p
no se encuentran caracteres coincidentes en los nodos secundarios, entonces el puntero de falla es útil, es decir, buscar en los nodos secundarios del nodo señalado por el puntero de falla.
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;
}
}
}
Eficiencia de coincidencia de autómatas de CA
- La complejidad de la construcción del árbol Trie es
O(m*len)
, dondem
es el número de cadenas de patrones ylen
es la longitud promedio de las cadenas de patrones. - Al construir un puntero de falla, lo que lleva más tiempo es buscar el puntero de falla capa por capa en el ciclo while. Cada ciclo sube al menos una capa y la altura del árbol no excede, por lo que el tiempo
len
La complejidad esO(K*len)
, K es el nodo en el árbol Trie. - Los dos pasos anteriores solo deben ejecutarse una vez para completar la construcción, lo que no afecta la eficiencia de la coincidencia con la cadena principal. Durante la coincidencia, lo que consume más tiempo es también el código para el siguiente puntero de falla en el ciclo while. Entonces, la complejidad del tiempo es, si
O(len)
la longitud de la cadena principal esn
, entonces la complejidad del tiempo total de coincidencia esO(n*len)
De hecho, al hacer coincidir palabras sensibles, la longitud promedio de las palabras sensibles no será muy larga. Por lo tanto, la eficiencia de coincidencia del autómata AC es muy cercana. Sólo en casos extremos, la eficiencia se O(n)
degradará a la misma que la eficiencia de coincidencia del árbol Trie. .
Los casos extremos son los siguientes: