Qué es el algoritmo KMP (explicación detallada)

¿Qué es el algoritmo KMP?

KMP fue descubierto por tres vacas grandes: DEKnuth, JHMorris y VRPratt al mismo tiempo. ¡El primero de ellos es el autor de "El arte de la programación informática"! !

El problema que debe resolver el algoritmo KMP es ubicar el patrón en la cadena (también llamada cadena principal). En pocas palabras, es la búsqueda de palabras clave que solemos decir. La cadena de patrón es la palabra clave (en adelante denominada P). Si aparece en una cadena principal (en adelante llamada T), devuelve su posición específica, de lo contrario, devuelve -1 (medio comúnmente utilizado).
Inserte la descripción de la imagen aquí

En primer lugar, tengo una idea muy simple sobre este problema: haga coincidir uno por uno de izquierda a derecha. Si hay un carácter que no coincide en el proceso, salte hacia atrás y mueva la cadena del patrón hacia la derecha. ¿Qué tiene de difícil esto?

Podemos inicializar así:
Inserte la descripción de la imagen aquí

Después de eso, solo necesitamos comparar si el carácter apuntado por el puntero i es consistente con el carácter apuntado por el puntero j. Si son consistentes, retroceda, si no son consistentes, como se muestra a continuación:

Inserte la descripción de la imagen aquí

A y E no son iguales, luego mueva el puntero i de regreso a la primera posición (asumiendo que el subíndice comienza desde 0), mueva j a la posición 0 de la cadena del patrón, y luego reinicie este paso:
Inserte la descripción de la imagen aquí

Basándonos en esta idea, podemos obtener el siguiente programa:

/**

 * 暴力破解法

 * @param ts 主串

 * @param ps 模式串

 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1

 */

public static int bf(String ts, String ps) {
    
    

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    while (i < t.length && j < p.length) {
    
    

       if (t[i] == p[j]) {
    
     // 当两个字符相同,就比较下一个

           i++;

           j++;

       } else {
    
    

           i = i - j + 1; // 一旦不匹配,i后退

           j = 0; // j归0

       }

    }

    if (j == p.length) {
    
    

       return i - j;

    } else {
    
    

       return -1;

    }

}

El programa anterior no es un problema, ¡pero no lo suficientemente bueno!

Si se busca artificialmente, definitivamente no volveré al primer lugar, porque no hay una A delante de la posición de falla de coincidencia de la cadena principal, excepto la primera A, ¿por qué podemos saber que solo hay una A delante de la cadena principal? ? ¡Porque ya sabemos que los tres primeros caracteres coinciden! (Esto es muy importante). ¡Moverse en el pasado definitivamente no es un partido! Hay una idea, no puedo moverme, solo necesitamos mover j, como se muestra a continuación:

Inserte la descripción de la imagen aquí

La situación anterior sigue siendo relativamente ideal y, en el mejor de los casos, la compararemos de nuevo. Pero si busca "SSSSB" en la cadena principal "SSSSSSSSSSSSSSA", encontrará que no coincide hasta que se compare el último, y luego retrocedo, la eficiencia de esto es obviamente la más baja.

Las vacas grandes no podían soportar el método ineficaz de "agrietamiento por fuerza bruta", por lo que las tres desarrollaron el algoritmo KMP. La idea es la misma que vimos anteriormente: "Utilizando la información válida que se ha emparejado parcialmente, evitando que el puntero i retroceda y modificando el puntero j, la cadena del patrón se mueve a una posición válida tanto como sea posible".

Por lo tanto, el punto de todo el KMP es que cuando un determinado carácter no coincide con la cadena principal, ¿deberíamos saber dónde mover el puntero j?

A continuación, descubramos la ley de movimiento de j por nosotros mismos:
Inserte la descripción de la imagen aquí

Como se muestra en la figura: C y D no coinciden, ¿dónde queremos mover j? Obviamente el número uno. ¿por qué? Porque el frente A es el mismo:
Inserte la descripción de la imagen aquí

La misma situación se muestra en la siguiente figura:

Inserte la descripción de la imagen aquí

Puede mover el puntero j a la segunda posición, porque las dos letras al frente son iguales:

Inserte la descripción de la imagen aquí

Hasta ahora podemos ver una pista, cuando el partido falla, la siguiente posición k que j se moverá. Existe tal propiedad: los primeros k caracteres son los mismos que los últimos k caracteres antes de j.

Si usa una fórmula matemática para expresarlo así

P[0 ~ k-1] == P[j-k ~ j-1]

Esto es muy importante. Si te cuesta recordarlo, puedes comprenderlo a través de la siguiente figura:
Inserte la descripción de la imagen aquí

Después de comprender esto, debería ser posible comprender por qué j se puede mover directamente a la posición k.

porque:

当T[i] != P[j]时
有T[i-j ~ i-1] == P[0 ~ j-1]
由P[0 ~ k-1] == P[j-k ~ j-1]
必然:T[i-k ~ i-1] == P[0 ~ k-1]

La fórmula es aburrida, puedes leerla y entenderla, no necesitas recordarla.
Inserte la descripción de la imagen aquí

Este párrafo es solo para demostrar por qué podemos mover directamente j a k sin comparar los k caracteres anteriores.

De acuerdo, el siguiente paso es el punto ¿Cómo encontramos este (estos) k? Debido a que puede ocurrir un desajuste en cada posición de P, es decir, tenemos que calcular la k correspondiente a cada posición j, entonces usamos una matriz al lado para guardar, siguiente [j] = k, lo que significa que cuando T [i]! = Cuando P [j], la siguiente posición del puntero j.

Muchos libros de texto o publicaciones de blogs son bastante vagos en este lugar, o simplemente se mencionan de un solo golpe, o incluso se publican un fragmento de código. ¿Cómo puedo pedir esto? No está nada claro. Y aquí está precisamente la parte más crítica de todo el algoritmo.

public static int[] getNext(String ps) {
    
    

    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {
    
    

       if (k == -1 || p[j] == p[k]) {
    
    

           next[++j] = ++k;

       } else {
    
    

           k = next[k];

       }

    }

    return next;

}

Esta versión del algoritmo para encontrar la siguiente matriz debería ser la más difundida y el código es muy conciso. Pero es realmente confuso ¿Cuál es la base de este cálculo?

Ok, dejemos esto a un lado, derivemos nuestras propias ideas, ahora debemos recordar siempre que el valor de next [j] (es decir, k) significa que cuando P [j]! = T [i], el puntero j Luego mueva la posición.

Veamos primero el primero: cuando j es 0, ¿qué pasa si no hay coincidencia en este momento?

Inserte la descripción de la imagen aquí

En el caso de la imagen de arriba, j ya está en el extremo izquierdo y es imposible moverlo. En este momento, el puntero i debería moverse hacia atrás. Entonces habrá next [0] = -1; esta inicialización en el código.

¿Y si es cuando j es 1?
Inserte la descripción de la imagen aquí

Obviamente, el puntero j debe volver a la posición 0. Porque solo hay este lugar frente a él ~~~

Lo siguiente es lo más importante, consulte la imagen a continuación:

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Compare estas dos cifras con cuidado.

Encontramos una regla:

当P[k] == P[j]时,
有next[j+1] == next[j] + 1

De hecho, esto se puede probar:

因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1

La fórmula aquí no es muy fácil de entender, pero será más fácil de entender mirando la imagen.

¿Y si P [k]! = P [j]? Por ejemplo, como se muestra en la siguiente figura:
Inserte la descripción de la imagen aquí

En este caso, si observa el código, debería ser esta oración: k = siguiente [k], ¿por qué es así? Debería ver a continuación.
Inserte la descripción de la imagen aquí

¡Ahora debería saber por qué k = siguiente [k]! Como en el ejemplo anterior, ya no podemos encontrar la cadena de sufijo más larga [A, B, A, B], pero aún podemos encontrar cadenas de prefijo como [A, B] y [B]. Entonces, este proceso parece estar posicionando la cadena [A, B, A, C], cuando C es diferente de la cadena principal (es decir, la posición de k es diferente), por supuesto, el puntero se mueve a la siguiente [k] .

Con la siguiente matriz, todo es fácil, podemos escribir el algoritmo KMP:

public static int KMP(String ts, String ps) {
    
    

    char[] t = ts.toCharArray();

    char[] p = ps.toCharArray();

    int i = 0; // 主串的位置

    int j = 0; // 模式串的位置

    int[] next = getNext(ps);

    while (i < t.length && j < p.length) {
    
    

       if (j == -1 || t[i] == p[j]) {
    
     // 当j为-1时,要移动的是i,当然j也要归0

           i++;

           j++;

       } else {
    
    

           // i不需要回溯了

           // i = i - j + 1;

           j = next[j]; // j回到指定位置

       }

    }

    if (j == p.length) {
    
    

       return i - j;

    } else {
    
    

       return -1;

    }

}

En comparación con el agrietamiento por fuerza bruta, se han cambiado 4 lugares. El punto más importante es que no es necesario retroceder.

Finalmente, echemos un vistazo a las fallas en el algoritmo anterior. Mira el primer ejemplo:
Inserte la descripción de la imagen aquí

Obviamente, cuando la siguiente matriz obtenida por el algoritmo anterior debería ser [-1, 0, 0, 1]

Entonces, el siguiente paso es mover j al primer elemento:
Inserte la descripción de la imagen aquí

No es difícil descubrir que este paso no tiene ningún sentido. Debido a que el último B ya no coincide, el primero B tampoco debe coincidir. La misma situación ocurre realmente en el segundo elemento A.

Obviamente, la razón del problema es que P [j] == P [siguiente [j]].

Entonces solo necesitamos agregar una condición de juicio:

public static int[] getNext(String ps) {
    
    
    char[] p = ps.toCharArray();

    int[] next = new int[p.length];

    next[0] = -1;

    int j = 0;

    int k = -1;

    while (j < p.length - 1) {
    
    

       if (k == -1 || p[j] == p[k]) {
    
    

           if (p[++j] == p[++k]) {
    
     // 当两个字符相等时要跳过

              next[j] = next[k];

           } else {
    
    

              next[j] = k;

           }

       } else {
    
    

           k = next[k];

       }

    }

    return next;

}

Supongo que te gusta

Origin blog.csdn.net/weixin_52622200/article/details/110563434
Recomendado
Clasificación