[Entrevista] Algoritmo de cadena

Uno, concreto

Requisito: Comprender y saber que, por lo general, no se requiere la escritura del código de la pizarra.

1. Solución violenta ( O (MN) O (MN)O ( M N )

// Java
public static int forceSearch(String txt, String pat) {
    
    
    int M = txt.length();
    int N = pat.length();

    for (int i = 0; i <= M - N; i++) {
    
    
        int j;
        for (j = 0; j < N; j++) {
    
    
            if (txt.charAt(i + j) != pat.charAt(j))
                break;
        }
        if (j == N) {
    
    
            return i;
        }
        // 更加聪明? 
        // 1. 预先判断 hash(txt.substring(i, M)) == hash(pat)
        // 2. KMP 
    }
    return -1;
}

2. Algoritmo de Rabin-Karp

// Java
public final static int D = 256;
public final static int Q = 9997;

static int RabinKarpSerach(String txt, String pat) {
    
    
    int M = pat.length();
    int N = txt.length();
    int i, j;
    int patHash = 0, txtHash = 0;

    for (i = 0; i < M; i++) {
    
    
        patHash = (D * patHash + pat.charAt(i)) % Q;
        txtHash = (D * txtHash + txt.charAt(i)) % Q;
    }
    int highestPow = 1;  // pow(256, M-1)
    for (i = 0; i < M - 1; i++) 
        highestPow = (highestPow * D) % Q;

    for (i = 0; i <= N - M; i++) {
    
     // 枚举起点
        if (patHash == txtHash) {
    
    
            for (j = 0; j < M; j++) {
    
    
                if (txt.charAt(i + j) != pat.charAt(j))
                    break;
            }
            if (j == M)
                return i;
        }
        if (i < N - M) {
    
    
            txtHash = (D * (txtHash - txt.charAt(i) * highestPow) + txt.charAt(i + M)) % Q;
            if (txtHash < 0)
                txtHash += Q;
        }
    }

    return -1;
}

3 KMP

Para obtener más información, consulte los videos y artículos en 1 y 2 y 3.

3.1, conceptos básicos

  • Prefijo: excepto el último carácter, todas las combinaciones de cabeza de una cadena;

  • Sufijo: excepto el primer carácter, todas las combinaciones finales de una cadena.

    Ejemplo: Cadena S = "ABCDAB", el prefijo es [A, AB, ABC, ABCD, ABCDA], el sufijo es [BCDAB, CDAB, DAB, AB, B], el elemento total es "AB" y la longitud es 2.

3.2, proceso de procesamiento de KMP

1: tabla de prefijos

Para una cadena, encuentre la longitud del elemento común más largo de los prefijos y sufijos de todas las cadenas.
Tabla de prefijos

Mueva la longitud pública calculada hacia atrás una posición, y la primera posición es -1.
Proceso 1
2. Correspondencia de cadenas

Paso 1: a partir de i = 0, j = 0, compare si son iguales uno por uno.
Proceso 1.1
Paso 2: cuando encuentre desigualdad, primero mire el número correspondiente debajo de la tabla de prefijos y luego mueva el puntero al subíndice correspondiente de la cadena del patrón. La cadena de patrón de efecto equivalente se mueve hacia atrás como un todo.
proceso
Paso 3: el efecto después del movimiento se muestra a continuación.
resultado

Código:

// C V2.0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void prefix_table(char pattern[], int prefix[], int n) {
    
    
	prefix[0] = 0;
	int len = 0;
	int i = 1;
	while (i<n) {
    
    
		if ( pattern[i]==pattern[len] ) {
    
      // ABA --> ABAB, 最长公共前缀长度由1 --> 2
			len++;
			prefix[i] = len;
			i++;
		}
		else {
    
    
			if (len > 0) {
    
      // 防止越界,
				// ABABCABA --> ABABCABAA。关键步骤,补充理解见下。
				len = prefix[len-1];
			}
			else {
    
      // pattern[i]!=pattern[len] && len==0, 防止死循环 A --> AB, 0 --> -1, 此时i = 0
				prefix[i] = len;
				i++;
			}
		}
	}
}

void move_prefix_table(int prefix[], int n) {
    
    
	int i;
	for (i=n-1; i>0; i--) {
    
    
		prefix[i] = prefix[i-1];
	}
	prefix[0] = -1;
}

void kmp_search(char text[], char pattern[]) {
    
    
	int n = strlen(pattern);
	int m = strlen(text);
	int* prefix = malloc(sizeof(int) * n);
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	
	// text[i]    , len(text)    = m 
	// pattern[i] , len(pattern) = n 
	
	int i = 0;
	int j = 0;
	while (i < m) {
    
    
		if (j==n-1 && text[i] == pattern[j]) {
    
    
			printf("Found pattern at %d\n", i-j);
			j = prefix[j];
		}
		if (text[i]==pattern[j]) {
    
    
			i++; j++;
		}
		else {
    
    
			j = prefix[j];  // 关键过程可见上图
			if (j == -1) {
    
    
				i++; j++;
			}
		}
	}
}
/**/
int main() {
    
    
	/**/
	char pattern[] = "ABABCABAA";
	char text[]    = "ABABABCABAABABABAB";
	kmp_search(text, pattern);

	/*
	char pattern[] = "ABABCABAA";
	int prefix[9];
	int n = 9;
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	int i;
	for (i=0; i<n; i++) {
		printf("%d\n", prefix[i]);
	}
    */
	
	return 0;
}

Complejidad temporal: O (m + n), lo peor puede degenerar en O (mn), como {aaaaaaaab, ab}
Complejidad espacial: O (1)

  • Comprensión complementaria: cómo construir rápidamente la siguiente matriz

Punto clave: la cadena del patrón coincide.
La definición de siguiente [i]: P [0] ~ P [i] La longitud de la cadena común máxima de prefijo y sufijo en esta sección de cadena de caracteres, es decir, el k máximo del prefijo k igual al sufijo k.

Los pasos clave para entender: len = prefijo [len-1], ¿por qué? (El análisis proviene de la Referencia 7)

Objetivo: ahora índice = 12, encontrar el valor k del último dígito de la siguiente matriz, es decir, siguiente [último] =? Cómo encontrar el siguiente [i + 1]

  • Información conocida:
  1. Ahora la cadena A es la misma que la cadena B, siguiente [último-1] = String_A.length = String_B.length = 5.
  2. En la primera mitad del párrafo, next [ahora-1] = {a, b} .length = 2, 2 es la longitud de la cadena común más grande del prefijo y sufijo de la subcadena A.
  • análisis:
  1. Si P [ahora] == P [x], agregue directamente +1 sobre la base del dígito anterior, y se convierte en 5 + 1 = 6.
  2. Pero P [ahora]! = P [x], luego siguiente [último] = siguiente [ahora] = 0? Obviamente no.
  • Find next [last] =? Necesito saber el prefijo común y el sufijo de P [0] ~ P [x-1].

Cadena A = cadena B, P [ahora]! = P [x], es decir, ya sabemos el carácter común más grande del prefijo y la cadena de sufijo A o B no está satisfecho. Necesitamos acortar la cadena común, y el prefijo y sufijo deben acortarse aún más.

Continuando con el análisis, el prefijo de la siguiente iteración (abreviado) debe estar en la cadena A y el sufijo debe estar en la cadena B. Cadena A == Cadena B, por lo que debe encontrar la longitud común máxima del prefijo y sufijo de la cadena A, es decir, siguiente [ahora-1].

Ahora salte, el valor específico: ahora = siguiente [ahora-1], luego compare p [ahora] == p [x]? Igual, luego siguiente [último] = ahora + 1; de lo contrario, ahora = siguiente [ahora-1], continúa el ciclo.
resultado

Dos, referencia

1. Algoritmo de concordancia de cadenas KMP 1
2. Algoritmo de concordancia de cadenas KMP 2
3. Algoritmo KMP
para concordancia de cadenas 4, KMP para concordancia de cadenas, BoyerMoore, algoritmo Sunday
5, código de fuerza bruta de concordancia de cadenas ejemplo
6, Rabin-Karp Ejemplo de código
7. ¿Cómo comprender y dominar mejor el algoritmo KMP?

Supongo que te gusta

Origin blog.csdn.net/HeavenDan/article/details/109165811
Recomendado
Clasificación