Programación · Algoritmos
Búsqueda de cadenas KMP explicada: cómo funciona, código y complejidad
Respuesta corta
El algoritmo de Knuth-Morris-Pratt (KMP) encuentra todas las apariciones de un patrón de longitud m en un texto de longitud n en tiempo O(n + m). Primero construye una función prefijo (también llamada tabla de fallos) del patrón: para cada posición, la longitud del prefijo propio más largo que también es sufijo. Cuando ocurre una discrepancia, KMP usa esta tabla para desplazar el patrón sin retroceder nunca en el texto.
Abrir Probador de regexQué problema resuelve KMP
La búsqueda de cadenas pregunta en qué lugares de un texto más largo aparece un patrón corto. El método ingenuo prueba cada posición de inicio y compara los caracteres uno a uno. En texto normal es rápido, pero con entradas repetitivas es lento: buscar aaaab en aaaaaaaaaa compara casi todo el patrón en cada posición, lo que da un tiempo O(n × m).
KMP nunca vuelve a leer un carácter del texto tras una discrepancia. Donald Knuth, James H. Morris y Vaughan Pratt lo publicaron en 1977 en "Fast Pattern Matching in Strings".
La idea en palabras sencillas
Supón que buscas el patrón ABABC, has encontrado ABAB y el siguiente carácter del texto no es C. La parte que acabas de encajar, ABAB, termina en AB, que es también como empieza el patrón. Así que ya sabes que el siguiente intento empieza con 2 caracteres coincidentes y puedes seguir comparando desde la posición 2 del patrón en lugar de empezar de cero.
La función prefijo guarda estos números. pi[i] es la longitud del prefijo propio más largo de pattern[0..i] que también es sufijo suyo (propio significa que no es la cadena entera).
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| pattern[i] | A | B | A | B | A | C | A |
| pi[i] | 0 | 0 | 1 | 2 | 3 | 0 | 1 |
Por ejemplo, pi[4] = 3 porque ABABA empieza y termina con ABA. pi[5] = 0 porque ningún prefijo de ABABAC termina en C.
Ejemplo resuelto, paso a paso
Busca el patrón ABABC (función prefijo 0, 0, 1, 2, 0) en el texto ABABABC. La variable k cuenta cuántos caracteres del patrón coinciden en este momento.
| Posición en el texto | Carácter del texto | k antes | Acción | k después |
|---|---|---|---|---|
| 0–3 | A B A B | 0 | Cuatro coincidencias seguidas | 4 |
| 4 | A | 4 | Discrepancia con pattern[4] = C: se asigna k = pi[3] = 2 y luego A coincide con pattern[2] | 3 |
| 5 | B | 3 | Coincide con pattern[3] | 4 |
| 6 | C | 4 | Coincide con pattern[4]: coincidencia completa que termina en 6, así que empieza en 6 − 5 + 1 = 2 | 5 → pi[4] = 0 |
El puntero del texto avanzó 7 veces y nunca retrocedió. El método ingenuo reiniciaría en la posición 1 y volvería a comparar algunos caracteres.
Implementación en Python
def prefix_function(p):
pi = [0] * len(p)
k = 0
for i in range(1, len(p)):
while k > 0 and p[i] != p[k]:
k = pi[k - 1] # fall back to a shorter border
if p[i] == p[k]:
k += 1
pi[i] = k
return pi
def kmp_search(text, pattern):
"""Return the start index of every occurrence (overlaps included)."""
if not pattern:
return []
pi, k, hits = prefix_function(pattern), 0, []
for i, ch in enumerate(text):
while k > 0 and ch != pattern[k]:
k = pi[k - 1]
if ch == pattern[k]:
k += 1
if k == len(pattern):
hits.append(i - k + 1)
k = pi[k - 1] # keep going to find overlapping matches
return hits
print(prefix_function("ABABACA")) # [0, 0, 1, 2, 3, 0, 1]
print(kmp_search("ABABABC", "ABABC")) # [2]
print(kmp_search("AAAAA", "AA")) # [0, 1, 2, 3]Complejidad temporal y espacial
| Fase | Tiempo | Espacio | Motivo |
|---|---|---|---|
| Construir la función prefijo | O(m) | O(m) | k aumenta como máximo una vez por paso y cada retroceso lo disminuye, así que el total de retrocesos es como máximo m. |
| Búsqueda | O(n) | O(1) adicional | El mismo argumento sobre el texto: como máximo n aumentos, por tanto como máximo n retrocesos. |
| Total | O(n + m) | O(m) | El peor caso es igual al mejor caso salvo una constante; no hay entradas malas. |
El algoritmo ingenuo es O(n × m) en el peor caso, pero a menudo se acerca a O(n) en texto en lenguaje natural, por lo que la ventaja de KMP se nota sobre todo con datos repetitivos.
Variantes y errores comunes
- Error de uno en el retroceso: tras una discrepancia, asigna
k = pi[k - 1], nopi[k].pise indexa por la última posición coincidente. - Usar
ifen lugar dewhilepara el retroceso: puede hacer falta retroceder varias veces seguidas antes de que un carácter coincida. - Olvidar reiniciar k tras una coincidencia completa: asigna
k = pi[k - 1]para encontrar coincidencias solapadas, ok = 0para las no solapadas. - Truco de la concatenación: calcula la función prefijo de
pattern + separator + text, con un separador que no aparezca en ninguno de los dos; cada posición donde el valor es igual a m es el final de una coincidencia. - El algoritmo Z calcula un arreglo estrechamente relacionado y también busca en O(n + m).
Cuándo usarlo y usos en el mundo real
| Algoritmo | Mejor uso |
|---|---|
| KMP | Tiempo lineal garantizado para un patrón; texto en flujo continuo (streaming), porque nunca mira hacia atrás. |
| Boyer-Moore y variantes | Patrones largos sobre alfabetos grandes; a menudo salta muchos caracteres y se usa en herramientas como grep. |
| Rabin-Karp | Muchos patrones de la misma longitud, o detección de plagio con hashes rodantes. |
| Aho-Corasick | Muchos patrones a la vez; generaliza los enlaces de fallo de KMP a un trie. |
| Arreglo de sufijos o autómata de sufijos | Muchas consultas distintas sobre el mismo texto fijo. |
- Problemas de entrevista: implementar
strStr(), palíndromo más corto (función prefijo des + "#" + reversed(s)), patrón de subcadena repetida, período más pequeño de una cadena (m - pi[m - 1]). - En el mundo real: búsqueda en secuencias de ADN y en flujos de paquetes de red, donde importa el tiempo lineal en el peor caso.
Preguntas frecuentes
¿Qué es la función prefijo o tabla de fallos en KMP?
Para cada posición i del patrón, guarda la longitud del prefijo propio más largo de pattern[0..i] que también es sufijo suyo. Ante una discrepancia, KMP la usa para saber cuántos caracteres siguen coincidiendo.
¿Cuál es la complejidad temporal de KMP?
O(n + m): O(m) para construir la función prefijo y O(n) para recorrer el texto. Es lineal incluso en el peor caso.
¿Es KMP más rápido que str.find de Python?
No. str.find está implementado en C con un algoritmo de búsqueda optimizado y es mucho más rápido en la práctica. KMP vale por su tiempo lineal garantizado, para aprender y para secuencias que no son cadenas.
¿Encuentra KMP coincidencias solapadas?
Sí, si tras una coincidencia completa asignas k = pi[k - 1] en lugar de 0. Por ejemplo, encuentra AA en las posiciones 0, 1, 2 y 3 de AAAAA.