Explicación detallada del algoritmo KMP, desde la búsqueda de fuerza bruta hasta la optimización de KMP y KMP

Dada una cadena principal (reemplazada por S) y una cadena de patrón (reemplazada por P), se requiere encontrar la posición de P en S. Este es el problema de la coincidencia de patrones de cadena.

El algoritmo Knuth-Morris-Pratt (KMP para abreviar) es uno de los algoritmos comúnmente utilizados para resolver este problema. Este algoritmo fue concebido por Donald Ervin Knuth y Vaughan Pratt en 1974, el mismo año en que James H. Morris también diseñó de forma independiente el algoritmo. , y finalmente los tres lo publicaron conjuntamente en 1977.

Antes de continuar, es necesario introducir dos conceptos aquí: prefijo verdadero y sufijo propio .
Inserte la descripción de la imagen aquí

De la figura anterior, "Prefijo verdadero" se refiere a la combinación de todas las cabezas de una cadena excepto él mismo; "Sufijo verdadero" se refiere a la combinación de todas las colas de una cadena excepto él mismo. (Muchos blogs en Internet, debería decirse que casi todos los blogs son "prefijo". Estrictamente hablando, "prefijo verdadero" y "prefijo" son diferentes. Como son diferentes, ¡es mejor no confundirse!)

Algoritmo de coincidencia de cadenas ingenuo Cuando nos
encontramos por primera vez con el problema de coincidencia de patrones de las cadenas, la primera reacción en nuestras mentes fue la coincidencia de cadenas ingenua (la llamada coincidencia de fuerza bruta). El código es el siguiente:

/* 字符串下标始于 0 */
int NaiveStringSearch(string S, string P)
{
    
    
    int i = 0;    // S 的下标
    int j = 0;    // P 的下标
    int s_len = S.size();
    int p_len = P.size();

    while (i < s_len && j < p_len)
    {
    
    
        if (S[i] == P[j])  // 若相等,都前进一步
        {
    
    
            i++;
            j++;
        }
        else               // 不相等
        {
    
    
            i = i - j + 1;
            j = 0;
        }
    }

    if (j == p_len)        // 匹配成功
        return i - j;

    return -1;
}

La complejidad del tiempo se corresponde con la violencia O(nm), que nes la Sduración mde Pla duración. Evidentemente, esta vez la complejidad es difícil de satisfacer nuestras necesidades.

A continuación, ingrese el tema: complejidad temporal del O(n+m)algoritmo KMP.

Algoritmo de coincidencia de cadenas KMP
Flujo del algoritmo
1) Inserte la descripción de la imagen aquí
Primero, compare el primer carácter de la cadena principal "BBC ABCDAB ABCDABCDABDE" con el primer carácter de la cadena de patrón "ABCDABD". Debido a que B no coincide con A, la cadena de patrón se desplaza un bit hacia atrás.
2)
Inserte la descripción de la imagen aquí
Debido a que B y A no vuelven a coincidir, la cuerda del patrón se mueve hacia atrás.
3)
Inserte la descripción de la imagen aquí
Entonces, hasta que haya un carácter en la cadena principal, que es el mismo que el primer carácter de la cadena del patrón.
4)
Inserte la descripción de la imagen aquí
Luego compare el siguiente carácter de la cadena principal y la cadena del patrón, y siguen siendo los mismos.
5)
Inserte la descripción de la imagen aquí
Hasta que la cadena principal tenga un carácter diferente del carácter correspondiente a la cadena del patrón.
6)
Inserte la descripción de la imagen aquí
En este momento, la reacción más natural es mover la cadena de patrón completa hacia atrás un bit y luego comparar una por una desde el principio. Aunque esto es factible, es muy ineficaz, porque hay que mover la "posición de búsqueda" a una posición que ya se ha comparado y repetir la comparación.
7)
Inserte la descripción de la imagen aquí
Un hecho básico es que cuando el espacio no coincide con D, en realidad ya sabe que los primeros seis caracteres son "ABCDAB". La idea del algoritmo KMP es intentar utilizar esta información conocida, no para mover la "posición de búsqueda" a la posición que ya se ha comparado, sino para continuar moviéndola hacia atrás, lo que mejora la eficiencia.
8)
Inserte la descripción de la imagen aquí
¿Cómo hacer esto? Puede establecer una matriz de salto para la cadena de patrón. La int next[]forma en que se calcula esta matriz se presentará más adelante, siempre que pueda usarla aquí.
9) Cuando se
Inserte la descripción de la imagen aquí
sabe que el espacio no coincide con D, se hacen coincidir los primeros seis caracteres "ABCDAB". Según la matriz de salto, el siguiente valor de D en la falta de coincidencia es 2, por lo que la siguiente coincidencia comienza en la posición donde el subíndice de la cadena del patrón es 2.
10)
Inserte la descripción de la imagen aquí
Debido a que el espacio no coincide con C, el siguiente valor en C es 0, por lo que la cadena del patrón comienza a coincidir desde el subíndice 0.
11)
Inserte la descripción de la imagen aquí
Debido a que el espacio no coincide con A, el valor de next aquí es -1, lo que significa que el primer carácter de la cadena de patrón no coincide, así que muévalo un bit hacia adelante.
12)
Inserte la descripción de la imagen aquí

Compare poco a poco hasta que C y D no coincidan. Entonces, el siguiente paso es comenzar a emparejar desde el lugar donde el subíndice es 2.
13)
Inserte la descripción de la imagen aquí

Comparando bit a bit, hasta el último bit de la cadena del patrón, se encuentra una coincidencia completa y se completa la búsqueda.

nextCómo obtener una matriz de
next matrices basada en resolver el "prefijo verdadero" y el "sufijo verdadero", es decir, next[i]igual a P[0]...P[i - 1]la longitud más larga del prefijo y el sufijo del mismo verdadero (ignorado temporalmente cuando i es igual a 0, se explicará a continuación ). Todavía usamos la tabla anterior como ejemplo. Para facilitar la lectura, la copié a continuación.
Inserte la descripción de la imagen aquí

  • i = 0, para el primer carácter de la cadena del patrón, lo unificamos como siguiente [0] = -1;
  • i = 1, la cadena anterior es A, el mismo prefijo verdadero más largo y la longitud del sufijo es 0, es decir, siguiente [1] = 0;
  • i = 2, la cadena anterior es AB, su prefijo verdadero idéntico más largo y la longitud del sufijo es 0, es decir, siguiente [2] = 0;
    i- = 3, la cadena anterior es ABC, su prefijo verdadero idéntico más largo y la longitud del sufijo es 0 , Es decir, siguiente [3] = 0;
  • i = 4, la cadena anterior es ABCD, el prefijo verdadero idéntico más largo y la longitud del sufijo es 0, es decir, siguiente [4] = 0;
  • i = 5, la cadena anterior es ABCDA, su prefijo y sufijo verdadero idéntico más largo es A, es decir, siguiente [5] = 1;
  • i = 6, la cadena anterior es ABCDAB, el prefijo y sufijo verdadero idéntico más largo es AB, es decir, siguiente [6] = 2;
  • i = 7, la cadena anterior es ABCDABD, el prefijo verdadero idéntico más largo y la longitud del sufijo es 0, es decir, siguiente [7] = 0.
    Entonces, ¿por qué se puede lograr el salto en el caso de un desajuste basado en la longitud del prefijo y sufijo verdadero idéntico más largo? Para ejemplos representativos: si i = 6no coincide, entonces conocemos su posición antes de la cuerda ABCDAB, la cuerda observación cuidadosa, tiene una cabeza y una cola AB, ya que la i = 6D no coincide en, por qué no vinculamos directamente i = 2la C continúa comparándola para asumir el control , debido a que hay un ABah, y este ABes ABCDABel prefijo más largo y el sufijo es realmente el mismo, la longitud del salto es solo 2 posiciones de subíndice.

Algunos lectores pueden dudar, si el i = 5partido falla, como explico la idea, en este momento debería ser i = 1el personaje en la comparación sigue tomando el relevo, pero el carácter de estas dos posiciones es el mismo, ah, todas B, ya que, como, tomar ¿No es inútil venir? De hecho, no es que haya un problema con mi explicación, ni hay un problema con este algoritmo, pero el algoritmo no se ha optimizado. Explicaré esto en detalle a continuación, pero se aconseja a los lectores que no se enreden aquí, omitir esto, y naturalmente lo entenderá a continuación.

La idea es tan simple, el siguiente paso es implementar el código, de la siguiente manera:

void GetNext(string P, int next[])
{
    
    
    int p_len = P.size();
    int i = 0;   // P 的下标
    int j = -1;  
    next[0] = -1;

    while (i < p_len)
    {
    
    
        if (j == -1 || P[i] == P[j])
        {
    
    
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}

Parece estupefacto, ¿no? . . El código anterior se utiliza para resolver el next[]valor de cada posición en la cadena del patrón .

Tras un análisis específico, divido el código en dos partes:

(1): ¿ iy jcuál es el rol?

iLa suma es jcomo dos "punteros", uno tras otro, moviéndolos para encontrar el prefijo y sufijo verdadero idéntico más largo.

(2): if...else...¿Qué se hace en la oración?
Inserte la descripción de la imagen aquí
Las suposiciones iy jposiciones anteriores, por lo next[i] = jdisponible, es decir, la posición i, la sección [0, i - 1]del prefijo y el sufijo más largos son realmente iguales [0, j - 1]y [i - j, i - 1]que el mismo contenido de dos secciones.

De acuerdo con el flujo del algoritmo, if (P[i] == P[j]),则 i++; j++; next[i] = j;si no es igual, j = next[j]consulte la figura siguiente:
Inserte la descripción de la imagen aquí
next[j]representa [0, j - 1]la longitud del prefijo y sufijo verdadero idéntico más largo del segmento. Como se muestra en la figura, use las dos elipses de la izquierda para representar el sufijo verdadero idéntico más largo, es decir, las dos elipses representan el mismo contenido de sección; por la misma razón, hay dos elipses idénticas a la derecha. Entonces, elsela declaración es el uso de una cuarta elipse y la elipse obtuvo el mismo contenido para acelerar [0, i - 1]la longitud del prefijo y el sufijo del mismo segmento verdadero.

Amigos atentos preguntarán ifdeclaraciones sobre j == -1el significado de la existencia, ¿qué es? Primero, simplemente ejecute el programa, jfue inicialmente -1, el P[i] == P[j]juicio directo sin duda se extenderá más allá de la frontera; segundo, las elsedeclaraciones j = next[j], jse alejan constantemente, si jse asigna en la parte posterior -1(es decir j = next[0]), el P[i] == P[j]desbordamiento determinará el límite. Para resumir los dos puntos anteriores, su significado es juzgar el límite especial.

#include <iostream>
#include <string>

using namespace std;

/* P 为模式串,下标从 0 开始 */
void GetNext(string P, int next[])
{
    
    
    int p_len = P.size();
    int i = 0;   // P 的下标
    int j = -1;  
    next[0] = -1;

    while (i < p_len)
    {
    
    
        if (j == -1 || P[i] == P[j])
        {
    
    
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];
    }
}

/* 在 S 中找到 P 第一次出现的位置 */
int KMP(string S, string P, int next[])
{
    
    
    GetNext(P, next);

    int i = 0;  // S 的下标
    int j = 0;  // P 的下标
    int s_len = S.size();
    int p_len = P.size();

    while (i < s_len && j < p_len) // 因为末尾 '\0' 的存在,所以不会越界
    {
    
    
        if (j == -1 || S[i] == P[j])  // P 的第一个字符不匹配或 S[i] == P[j]
        {
    
    
            i++;
            j++;
        }
        else
            j = next[j];  // 当前字符匹配失败,进行跳转
    }

    if (j == p_len)  // 匹配成功
        return i - j;
    
    return -1;
}

int main()
{
    
    
    int next[100] = {
    
     0 };

    cout << KMP("bbc abcdab abcdabcdabde", "abcdabd", next) << endl; // 15
    
    return 0;
}

Inserte la descripción de la imagen aquí

Tome la tabla en 3.2 como ejemplo (copiada arriba). Si la coincidencia falla cuando i = 5, siga el código en 3.2. En este momento, el carácter en i = 1 debe usarse para continuar la comparación, pero los caracteres en estas dos posiciones son iguales, ambas son B. Dado que son iguales, ¿no es inútil compararlas? Expliqué esto en 3.2 La razón de esto es que KMP no se ha optimizado. ¿Cómo se puede resolver este problema reescribiéndolo? Es muy sencillo.

/* P 为模式串,下标从 0 开始 */
void GetNextval(string P, int nextval[])
{
    
    
    int p_len = P.size();
    int i = 0;   // P 的下标
    int j = -1;  
    nextval[0] = -1;

    while (i < p_len)
    {
    
    
        if (j == -1 || P[i] == P[j])
        {
    
    
            i++;
            j++;
          
            if (P[i] != P[j])
                nextval[i] = j;
            else
                nextval[i] = nextval[j];  // 既然相同就继续往前找真前缀
        }
        else
            j = nextval[j];
    }
}

Supongo que te gusta

Origin blog.csdn.net/JACKSONMHLK/article/details/114168906
Recomendado
Clasificación