Algoritmo KMP: una comprensión exhaustiva de KMP de principio a fin

Uno: fondo

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

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

Antes de continuar, es necesario introducir dos conceptos aquí: Verdadero prefijo y sufijo adecuado .

De la figura anterior, "Prefijo verdadero" se refiere a todas las combinaciones de cabeza de una cadena de caracteres, excepto a sí mismo; "Sufijo verdadero" se refiere a todas las combinaciones de cola de una cadena de caracteres, excepto a sí mismo. (Muchos blogs en Internet, debería decirse que casi todos los blogs, incluidos los que escribí antes, son "prefijos". Estrictamente hablando, "prefijos verdaderos" y "prefijos" son diferentes. Dado que son diferentes, no los confunda. !)

Dos: algoritmo de coincidencia de cadenas ingenuo

El problema de coincidencia de patrones de la primera cadena de encuentro, la primera reacción en nuestra mente es la simple coincidencia de cadenas (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 temporal de la coincidencia de la fuerza bruta es \ (O (nm) \) , donde \ (n \) es la longitud de S y \ (m \) es la longitud de P. Obviamente, tal complejidad de tiempo es difícil de satisfacer nuestras necesidades.

A continuación, ingresaremos al tema: Algoritmo KMP con complejidad de tiempo \ (Θ (n + m) \) .

Tres: algoritmo de coincidencia de cadenas KMP

3.1 Algoritmo de flujo

Lo siguiente se toma del algoritmo KMP de la coincidencia de cadenas de Ruan Yifeng , y se modifica ligeramente.

(1)

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 y A no coinciden, la cadena de patrón se desplaza un bit hacia atrás.

(2)

Como B y A no vuelven a coincidir, la cadena del patrón retrocede.

(3)

De esta manera, hasta que la cadena principal tenga un carácter, es el mismo que el primer carácter de la cadena del patrón.

(4)

Luego compare el siguiente carácter de la cadena principal y la cadena del patrón, sigue siendo el mismo.

(5)

Hasta que haya un carácter en la cadena principal, el carácter correspondiente a la cadena del patrón no es el mismo.

(6)

En este momento, la reacción más natural es mover la cadena del patrón hacia atrás un bit, y luego comparar uno por uno desde el principio. Aunque esto es factible, la eficiencia es muy baja, ya que tiene que mover la "posición de búsqueda" a la posición que se ha comparado y pesarla nuevamente.

(7)

El 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 usar esta información conocida en lugar de mover la "posición de búsqueda" a la posición que se ha comparado, pero continuar moviéndola hacia atrás, lo que mejora la eficiencia.

(8)

yo 0 0 1 2 3 4 4 5 5 6 6 7 7
Cadena de patrón UN si C re UN si re '\ 0'
siguiente yo] -1 0 0 0 0 0 0 0 0 1 2 0 0

Como hacer esto Puede establecer una matriz de salto para la cadena de patrón. Más int next[]adelante se describirá cómo se calcula esta matriz, siempre que se use aquí.

(9)

Cuando se sabe que el espacio no coincide con D, los primeros seis caracteres "ABCDAB" coinciden. De acuerdo con la matriz de salto, el siguiente valor de D en la falta de coincidencia es 2, por lo que la coincidencia comienza desde la posición donde el índice de la cadena de patrón es 2 .

(10)

Como el espacio no coincide con C, el siguiente valor en C es 0, por lo que la cadena de patrón coincidirá con el índice 0.

(11)

Debido a que el espacio no coincide con A, el siguiente valor aquí es -1, lo que significa que el primer carácter de la cadena del patrón no coincide, por lo que retrocede un bit directamente.

(12)

Compare poco a poco hasta que encuentre que C y D no coinciden. Luego, el siguiente paso comienza con la coincidencia del subíndice 2.

(13)

Compare bit por bit hasta que el último bit de la cadena del patrón encuentre una coincidencia exacta, por lo que la búsqueda se ha completado.

3.2 Cómo encontrar la siguiente matriz

La siguiente matriz se resuelve en base a "prefijo verdadero" y "sufijo verdadero", que es next[i]igual a la P[0]...P[i - 1]longitud más larga del mismo prefijo y sufijo verdadero (ignore la situación cuando i es igual a 0, como se explicará a continuación). Todavía utilizamos la tabla anterior como ejemplo. Para facilitar la lectura, la he copiado a continuación.

yo 0 0 1 2 3 4 4 5 5 6 6 7 7
Cadena de patrón UN si C re UN si re '\ 0'
siguiente yo ] -1 0 0 0 0 0 0 0 0 1 2 0 0
  1. i = 0, para el primer carácter de la cadena de patrón, estamos unificados como next[0] = -1;

  2. i = 1, la cadena de caracteres anterior es A, el prefijo y la longitud de sufijo y prefijo verdaderos más largos e iguales es 0, a saber next[1] = 0;

  3. i = 2, la cadena anterior es ABla más larga y la misma longitud de sufijo verdadero es 0, es decir next[2] = 0;

  4. i = 3, la cadena de caracteres anterior es ABC, el prefijo verdadero más largo y la misma longitud de sufijo es 0, a saber next[3] = 0;

  5. i = 4, la cadena anterior es ABCDla más larga y la misma longitud de prefijo y sufijo verdadero es 0, es decir next[4] = 0;

  6. i = 5, la cadena anterior es ABCDAel prefijo y sufijo verdadero más largos y el mismo A, a saber next[5] = 1;

  7. i = 6, la cadena anterior es ABCDABla más larga y el mismo prefijo y sufijo verdadero son AB, a saber next[6] = 2;

  8. i = 7, la cadena de caracteres anterior es ABCDABD, la longitud de sufijo verdadera más larga y la misma es 0, es decir next[7] = 0.

Entonces, ¿por qué podemos saltar en caso de desajuste basado en la longitud del sufijo verdadero más largo y el mismo? Para dar un ejemplo representativo: si el i = 6tiempo no coincide, en este momento conocemos la cadena frente a su posición ABCDAB, observe cuidadosamente esta cadena, hay una al principio y al final AB, ya que i = 6la D en el lugar no coincide, ¿por qué no ponemos el i = 2lugar? Tome la C y continúe comparándola, porque hay una AB, y este ABes ABCDABel sufijo verdadero más largo y el mismo, y su longitud 2 es solo la posición del subíndice del salto.

Algunos lectores pueden tener dudas. Si la i = 5coincidencia falla en el momento, de acuerdo con la forma en que lo expliqué, i = 1los personajes en el lugar deben ser traídos para continuar la comparación, pero los personajes en estas dos posiciones son iguales, ambos B. Como son iguales, tome ¿No es inútil venir? De hecho, no es un problema lo que expliqué, ni un problema con este algoritmo, sino que el algoritmo no ha sido optimizado. El problema se explicará en detalle a continuación, pero se recomienda que los lectores no tengan dificultades aquí y se salten esto. Naturalmente lo entenderán a continuación.

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

/* 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];
	}
}

Una cara de estupefacción, ¿no? . . El código anterior se usa para resolver el next[]valor de cada posición en la cadena del patrón .

El siguiente análisis específico, dividí el código en dos partes:

(1): ¿Cuál es el papel de i y j?

i y j son como dos "punteros": muévase uno tras otro para encontrar el sufijo verdadero idéntico más largo.

(2): ¿Qué se hace en la declaración if ... else ...?

Suponiendo que las posiciones de i y j son como se muestran en la figura anterior next[i] = j, se puede ver que, para la posición i, los sufijos verdaderos idénticos más largos de la sección [0, i-1] son ​​[0, j-1] e [i-j , i-1], es decir, el contenido de las dos secciones es el mismo .

De acuerdo con el flujo del algoritmo, if (P[i] == P[j])entonces i++; j++; next[i] = j;; si no es igual j = next[j], vea la siguiente figura:

next[j]Representa la longitud del sufijo verdadero idéntico más largo en la sección [0, j-1]. Como se muestra en la figura, las dos elipses largas de la izquierda se usan para representar el sufijo verdadero más largo y el mismo, es decir, las dos elipses representan el mismo contenido del segmento; de la misma manera, también hay dos elipses idénticas a la derecha. Por lo tanto, la instrucción else utiliza el mismo contenido de la primera elipse y la cuarta elipse para acelerar la longitud del mismo prefijo y sufijo verdadero de la sección [0, i-1].

Los amigos atentos preguntarán cuál j == -1es el significado de la declaración if ? Primero, cuando el programa se está ejecutando, j se establece inicialmente en -1, y el P[i] == P[j]juicio directo indudablemente desbordará el límite; segundo, en la instrucción else j = next[j], j se retira constantemente, si a j se le asigna un valor de -1 en el retiro (es decir, j = next[0]), El P[i] == P[j]límite se desbordará en el juicio. Para resumir los dos puntos, el significado es para un juicio de límite especial.

Cuatro: código completo

#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;
}

Cinco: optimización KMP

yo 0 0 1 2 3 4 4 5 5 6 6 7 7
Cadena de patrón UN si C re UN si re '\ 0'
siguiente yo] -1 0 0 0 0 0 0 0 0 1 2 0 0

Tome la tabla de 3.2 como ejemplo (copiado arriba), si la i = 5coincidencia falla en ese momento, de acuerdo con el código de 3.2, i = 1los caracteres en el lugar deben ser traídos para continuar la comparación, pero los caracteres en estas dos posiciones son iguales, ambos son B, Dado que es lo mismo, ¿no es inútil obtenerlo? Lo expliqué en 3.2, la razón por la cual esto se debe a que KMP no se ha optimizado. ¿Cómo se puede reescribir para resolver este problema? Es muy simple

/* 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];
	}
}

Seis: referencias

Siete: Agradecimientos

-¡Este artículo está especialmente agradecido a Senior EthsonLiu por su ayuda!

Supongo que te gusta

Origin www.cnblogs.com/RioTian/p/12686870.html
Recomendado
Clasificación