OK, así que no sueno como un idiota, voy a exponer el problema / requisitos más explícitamente:
- La aguja (patrón) y el pajar (texto para buscar) son cadenas terminadas en nulo de estilo C. No se proporciona información de longitud; si es necesario, debe calcularse.
- La función debe devolver un puntero a la primera coincidencia, o
NULL
si no se encuentra ninguna coincidencia. - Los casos de falla no están permitidos. Esto significa que cualquier algoritmo con requisitos de almacenamiento no constantes (o grandes constantes) necesitará tener un caso de respaldo para la falla de asignación (y el rendimiento en la atención de respaldo por lo tanto contribuye al rendimiento en el peor de los casos).
- La implementación debe realizarse en C, aunque una buena descripción del algoritmo (o enlace a dicho) sin código también está bien.
... así como lo que quiero decir con "más rápido":
- Determinista
O(n)
donden
= longitud del pajar. (Pero puede ser posible usar ideas de algoritmos que normalmente sonO(nm)
(por ejemplo, hash rodante) si se combinan con un algoritmo más robusto para darO(n)
resultados deterministas ). - Nunca funciona (medible; un par de relojes para
if (!needle[1])
etc. están bien) peor que el ingenuo algoritmo de fuerza bruta, especialmente en agujas muy cortas, que probablemente sean el caso más común. (La sobrecarga de preprocesamiento pesado incondicional es mala, al igual que tratar de mejorar el coeficiente lineal para agujas patológicas a expensas de las agujas probables). - Dada una aguja arbitraria y un pajar, un rendimiento comparable o mejor (no peor que un 50% más de tiempo de búsqueda) en comparación con cualquier otro algoritmo ampliamente implementado.
- Aparte de estas condiciones, estoy dejando la definición de "más rápido" abierta. Una buena respuesta debería explicar por qué considera que el enfoque que sugiere es "más rápido".
Mi implementación actual se ejecuta aproximadamente entre un 10% más lento y 8 veces más rápido (dependiendo de la entrada) que la implementación de Two-Way de glibc.
Actualización: mi algoritmo óptimo actual es el siguiente:
- Para agujas de longitud 1, use
strchr
. - Para agujas de longitud 2-4, use palabras de máquina para comparar 2-4 bytes a la vez de la siguiente manera: precargue la aguja en un entero de 16 o 32 bits con desplazamientos de bits y ciclo de bytes viejos / bytes nuevos desde el pajar en cada iteración . Cada byte del pajar se lee exactamente una vez e incurre en una comprobación contra 0 (final de la cadena) y una comparación de 16 o 32 bits.
- Para agujas de longitud> 4, use el algoritmo de dos vías con una tabla de desplazamiento incorrecta (como Boyer-Moore) que se aplica solo al último byte de la ventana. Para evitar la sobrecarga de inicializar una tabla de 1kb, lo que sería una pérdida neta para muchas agujas de longitud moderada, mantengo una matriz de bits (32 bytes) que marca qué entradas en la tabla de desplazamiento se inicializan. Los bits que no están establecidos corresponden a valores de bytes que nunca aparecen en la aguja, para los cuales es posible un desplazamiento de longitud de aguja completa.
Las grandes preguntas que me quedan en la mente son:
- ¿Hay alguna manera de hacer un mejor uso de la tabla de turnos malos? Boyer-Moore lo aprovecha al escanear hacia atrás (de derecha a izquierda), pero Two-Way requiere un escaneo de izquierda a derecha.
- Los únicos dos algoritmos candidatos viables que he encontrado para el caso general (sin condiciones de falta de memoria o de rendimiento cuadrático) son la coincidencia bidireccional y de cadenas en alfabetos ordenados . ¿Pero hay casos fácilmente detectables en los que diferentes algoritmos serían óptimos? Ciertamente, muchos de los algoritmos
O(m)
(dondem
está la longitud de la aguja) en el espacio podrían usarse para másm<100
o menos. También sería posible usar algoritmos que son el peor de los casos, si hay una prueba fácil para las agujas que probablemente solo requieren tiempo lineal.
Puntos de bonificación por:
- ¿Puedes mejorar el rendimiento asumiendo que la aguja y el pajar son UTF-8 bien formados? (Con caracteres de diferentes longitudes de byte, la buena forma- imposición impone algunos requisitos de alineación de la cuerda entre la aguja y el pajar y permite cambios automáticos de 2-4 bytes cuando se encuentra un byte de cabecera que no coincide. Pero estas restricciones le compran mucho / algo más allá de lo que cálculos de sufijos máximos, buenos cambios de sufijos, etc. ¿ya te dan varios algoritmos?)
Nota: conozco la mayoría de los algoritmos que existen, pero no sé qué tan bien funcionan en la práctica. Aquí hay una buena referencia para que las personas no me sigan dando referencias sobre algoritmos como comentarios / respuestas: http://www-igm.univ-mlv.fr/~lecroq/string/index.html
strstr
como algo para más adelante, por lo que no he podido leer correctamente el documento que vinculaste, pero suena muy prometedor. Gracias y perdón por no volver a usted.Respuestas:
Construya una biblioteca de prueba de posibles agujas y pajares. Perfile las pruebas en varios algoritmos de búsqueda, incluida la fuerza bruta. Elija el que mejor funcione con sus datos.
Boyer-Moore usa una tabla de caracteres malos con una buena tabla de sufijos.
Boyer-Moore-Horspool usa una tabla de personajes malos.
Knuth-Morris-Pratt usa una tabla de coincidencias parciales.
Rabin-Karp usa hashes en ejecución.
Todos intercambian gastos generales por comparaciones reducidas en un grado diferente, por lo que el rendimiento del mundo real dependerá de las longitudes promedio de la aguja y el pajar. Cuanto más sobrecarga inicial, mejor con entradas más largas. Con agujas muy cortas, la fuerza bruta puede ganar.
Editar:
Un algoritmo diferente podría ser mejor para encontrar pares de bases, frases en inglés o palabras sueltas. Si hubiera un mejor algoritmo para todas las entradas, se habría publicado.
Piensa en la siguiente mesita. Cada signo de interrogación puede tener un mejor algoritmo de búsqueda diferente.
Esto realmente debería ser un gráfico, con un rango de entradas más cortas a más largas en cada eje. Si trazó cada algoritmo en dicho gráfico, cada uno tendría una firma diferente. Algunos algoritmos sufren con mucha repetición en el patrón, lo que puede afectar usos como la búsqueda de genes. Algunos otros factores que afectan el rendimiento general son la búsqueda del mismo patrón más de una vez y la búsqueda de diferentes patrones al mismo tiempo.
Si necesito un conjunto de muestra, creo que eliminaría un sitio como google o wikipedia, luego eliminaría el html de todas las páginas de resultados. Para un sitio de búsqueda, escriba una palabra y luego use una de las frases de búsqueda sugeridas. Elija algunos idiomas diferentes, si corresponde. Usando páginas web, todos los textos serían de cortos a medianos, así que combine páginas suficientes para obtener textos más largos. También puede encontrar libros de dominio público, registros legales y otros grandes cuerpos de texto. O simplemente genera contenido aleatorio eligiendo palabras de un diccionario. Pero el punto de creación de perfiles es probar el tipo de contenido que buscará, por lo tanto, use muestras del mundo real si es posible.
Me fui corto y largo vago. Para la aguja, pienso en corto como menos de 8 caracteres, medio como menos de 64 caracteres y largo como menos de 1k. Para el pajar, pienso en corto como debajo de 2 ^ 10, medio como debajo de 2 ^ 20 y largo como hasta 2 ^ 30 caracteres.
fuente
Publicado en 2011, creo que podría ser el algoritmo "Coincidencia simple de cadenas de espacio constante en tiempo real" de Dany Breslauer, Roberto Grossi y Filippo Mignosi.
Actualizar:
En 2014, los autores publicaron esta mejora: Hacia una coincidencia de cadena empaquetada óptima .
fuente
El http://www-igm.univ-mlv.fr/~lecroq/string/index.html enlace que apunta es una excelente fuente y resumen de algunos de los algoritmos de coincidencia de cadenas más conocidos e investigados.
Las soluciones a la mayoría de los problemas de búsqueda incluyen compensaciones con respecto a los requisitos generales de procesamiento previo, tiempo y espacio. Ningún algoritmo único será óptimo o práctico en todos los casos.
Si su objetivo es diseñar un algoritmo específico para la búsqueda de cadenas, ignore el resto de lo que tengo que decir: Si desea desarrollar una rutina de servicio de búsqueda de cadenas generalizada, intente lo siguiente:
Dedique un tiempo a revisar las fortalezas y debilidades específicas de los algoritmos que ya ha mencionado. Realice la revisión con el objetivo de encontrar un conjunto de algoritmos que cubran el rango y el alcance de las búsquedas de cadenas que le interesen. Luego, cree un selector de búsqueda frontal basado en una función de clasificador para apuntar al mejor algoritmo para las entradas dadas. De esta manera, puede emplear el algoritmo más eficiente para hacer el trabajo. Esto es particularmente efectivo cuando un algoritmo es muy bueno para ciertas búsquedas pero se degrada mal. Por ejemplo, la fuerza bruta es probablemente la mejor para agujas de longitud 1, pero se degrada rápidamente a medida que aumenta la longitud de la aguja, con lo cual el algoritmo sustik-moorepuede volverse más eficiente (sobre alfabetos pequeños), luego, para agujas más largas y alfabetos más grandes, los algoritmos KMP o Boyer-Moore pueden ser mejores. Estos son solo ejemplos para ilustrar una posible estrategia.
El enfoque de algoritmo múltiple no es una idea nueva. Creo que ha sido utilizado por algunos paquetes comerciales de clasificación / búsqueda (por ejemplo, SYNCSORT, comúnmente utilizado en mainframes, implementa varios algoritmos de clasificación y utiliza la heurística para elegir el "mejor" para las entradas dadas)
Cada algoritmo de búsqueda viene en varias variaciones que pueden marcar diferencias significativas en su rendimiento, como, por ejemplo, ilustra este artículo .
Compare su servicio para clasificar las áreas donde se necesitan estrategias de búsqueda adicionales o para ajustar de manera más efectiva su función de selección. Este enfoque no es rápido ni fácil, pero si se hace bien puede producir muy buenos resultados.
fuente
Me sorprendió ver nuestro informe técnico citado en esta discusión; Soy uno de los autores del algoritmo que se llamó anteriormente Sustik-Moore. (No utilizamos ese término en nuestro artículo).
Quería enfatizar que para mí la característica más interesante del algoritmo es que es bastante simple demostrar que cada letra se examina como máximo una vez. Para versiones anteriores de Boyer-Moore, probaron que cada letra se examina como máximo 3 y más tarde 2 veces como máximo, y esas pruebas estaban más involucradas (ver citas en papel). Por lo tanto, también veo un valor didáctico al presentar / estudiar esta variante.
En el documento también describimos variaciones adicionales que están orientadas hacia la eficiencia mientras se relajan las garantías teóricas. Es un documento corto y el material debe ser comprensible para un graduado promedio de secundaria en mi opinión.
Nuestro objetivo principal era llevar esta versión a la atención de otros que puedan mejorarla aún más. La búsqueda de cadenas tiene tantas variaciones y nosotros solos no podemos pensar en todos dónde esta idea podría traer beneficios. (Texto fijo y patrón cambiante, texto diferente de patrón fijo, preprocesamiento posible / no posible, ejecución paralela, búsqueda de subconjuntos coincidentes en textos grandes, permitir errores, coincidencias cercanas, etc., etc.)
fuente
El algoritmo de búsqueda de subcadenas más rápido dependerá del contexto:
El documento de 2010 "El problema de coincidencia de cadenas exactas: una evaluación experimental integral" ofrece tablas con tiempos de ejecución para 51 algoritmos (con diferentes tamaños de alfabeto y longitudes de aguja), para que pueda elegir el mejor algoritmo para su contexto.
Todos esos algoritmos tienen implementaciones en C, así como un conjunto de pruebas, aquí:
http://www.dmi.unict.it/~faro/smart/algorithms.php
fuente
Una muy buena pregunta. Solo agrega algunos pedacitos ...
Alguien hablaba de la secuencia de ADN. Pero para la secuencia de ADN, lo que solemos hacer es construir una estructura de datos (por ejemplo, matriz de sufijos, árbol de sufijos o índice FM) para el pajar y unir muchas agujas contra ella. Esta es una pregunta diferente.
Sería genial si alguien quisiera comparar varios algoritmos. Hay muy buenos puntos de referencia sobre la compresión y la construcción de matrices de sufijos, pero no he visto un punto de referencia sobre la coincidencia de cadenas. Los posibles candidatos de pajar podrían ser del punto de referencia SACA .
Hace unos días estaba probando la implementación de Boyer-Moore desde la página que me recomendó (EDITAR: necesito una llamada de función como memmem (), pero no es una función estándar, así que decidí implementarla). Mi programa de evaluación comparativa utiliza un pajar aleatorio. Parece que la implementación de Boyer-Moore en esa página es más rápida que la memmem () de glibc y strnstr () de Mac. En caso de que esté interesado, la implementación está aquí y el código de evaluación comparativa está aquí . Definitivamente, este no es un punto de referencia realista, pero es un comienzo.
fuente
Sé que es una pregunta antigua, pero la mayoría de las tablas de cambio malas son de un solo carácter. Si tiene sentido para su conjunto de datos (por ejemplo, especialmente si se trata de palabras escritas), y si tiene el espacio disponible, puede obtener una aceleración dramática utilizando una tabla de cambio incorrecta hecha de n-gramos en lugar de caracteres individuales.
fuente
Use stdlib
strstr
:Fue muy rápido, solo tardé unos 5 segundos en escribir.
fuente
Aquí está la implementación de búsqueda de Python , utilizada desde todo el núcleo. Los comentarios indican que utiliza una tabla comprimida boyer-moore delta 1 .
Yo mismo he realizado una experimentación bastante extensa con la búsqueda de cadenas, pero fue para múltiples cadenas de búsqueda. Las implementaciones de ensamblaje de Horspool y Bitap a menudo pueden defenderse contra algoritmos como Aho-Corasick para bajos recuentos de patrones.
fuente
Un
strchr
algoritmo más rápido de "Búsqueda de un solo carácter coincidente" (ala ).Notas importantes:
Estas funciones usan un
gcc
compilador "número / recuento de ceros (iniciales | finales)" intrínseco__builtin_ctz
. Es probable que estas funciones solo sean rápidas en máquinas que tienen una o más instrucciones que realizan esta operación (es decir, x86, ppc, arm).Estas funciones suponen que la arquitectura de destino puede realizar cargas no alineadas de 32 y 64 bits. Si su arquitectura de destino no es compatible con esto, deberá agregar algo de lógica de inicio para alinear correctamente las lecturas.
Estas funciones son neutrales al procesador. Si la CPU de destino tiene instrucciones vectoriales, es posible que pueda hacerlo (mucho) mejor. Por ejemplo, la
strlen
siguiente función usa SSE3 y puede modificarse trivialmente a XOR los bytes escaneados para buscar un byte que no sea0
. Los puntos de referencia realizados en una computadora portátil Core 2 de 2.66 GHz con Mac OS X 10.6 (x86_64):strchr
findFirstByte64
strlen
... una versión de 32 bits:
... y una versión de 64 bits:
Editar 2011/06/04 El OP señala en los comentarios que esta solución tiene un "error insuperable":
Esto es técnicamente cierto, pero se aplica a prácticamente cualquier algoritmo que opera en fragmentos que son más grandes que un solo byte, incluido el método sugerido por el OP en los comentarios:
Tampoco tiene nada que ver con la alineación. per se. Es cierto que esto podría causar el comportamiento discutido en la mayoría de las arquitecturas comunes en uso, pero esto tiene más que ver con los detalles de implementación de microarquitectura: si la lectura no alineada se extiende a un límite de 4K (nuevamente, típico), entonces esa lectura causará un programa falla de terminación si el siguiente límite de página 4K no está mapeado.
Pero esto no es un "error" en el algoritmo dado en la respuesta; ese comportamiento se debe a que funciona como
strchr
ystrlen
no acepta unlength
argumento para limitar el tamaño de la búsqueda. La búsquedachar bytes[1] = {0x55};
, que para los fines de nuestra discusión se coloca al final de un límite de página de VM de 4K y la siguiente página no está asignada, constrchr(bytes, 0xAA)
(dondestrchr
hay una implementación de byte a la vez) se bloqueará exactamente el mismo camino. Lo mismo parastrchr
primo relacionadostrlen
.Sin un
length
argumento, no hay forma de saber cuándo debe cambiar el algoritmo de alta velocidad y volver a un algoritmo byte por byte. Un "error" mucho más probable sería leer "más allá del tamaño de la asignación", lo que técnicamente resulta deundefined behavior
acuerdo con los diversos estándares del lenguaje C, y sería marcado como un error por algo comovalgrind
.En resumen, todo lo que opera en fragmentos de bytes más grandes para ir más rápido, como lo hace este código de respuestas y el código señalado por el OP, pero que debe tener una semántica de lectura con precisión de byte, es probable que sea "defectuoso" si no hay
length
argumento para controle los casos de esquina de "la última lectura".El código en esta respuesta es un núcleo para poder encontrar el primer byte en un fragmento de tamaño de palabra de CPU natural rápidamente si la CPU de destino tiene una
ctz
instrucción rápida similar. Es trivial agregar cosas como asegurarse de que solo funcione en límites naturales correctamente alineados, o alguna forma delength
límite, lo que le permitiría salir del núcleo de alta velocidad y pasar a una verificación byte por byte más lenta.El OP también dice en los comentarios:
Que esta afirmación sea cierta o no depende en gran medida de la microarquitectura en cuestión. Usando el modelo canónico de tubería RISC de 4 etapas, entonces es casi seguro que es cierto. Pero es extremadamente difícil saber si es cierto para una CPU súper escalar contemporánea fuera de servicio, donde la velocidad del núcleo puede eclipsar por completo la velocidad de transmisión de memoria. En este caso, no solo es plausible, sino bastante común, que haya una gran brecha en "el número de instrucciones que se pueden retirar" en relación con "el número de bytes que se pueden transmitir" para que tenga "el número de instrucciones que se pueden retirar para cada byte que se puede transmitir ". Si es lo suficientemente grande, la
ctz
instrucción + shift se puede hacer "gratis".fuente
strchr
." - Usted solicitó los algoritmos de búsqueda de subcadenas más rápidos. Encontrar una subcadena de longitud 1 es solo un caso especial, uno que también se puede optimizar. Si cambia su código de caso especial actual por subcadenas de longitud 1 (strchr
) con algo como lo anterior, las cosas (posiblemente, dependiendo de cómostrchr
se implementen) irán más rápido. El algoritmo anterior es casi 3 veces más rápido que unastrchr
implementación ingenua típica .char bytes[1] = {0x55};
es irrelevante. Muy relevante es su comentario acerca de que esto sea cierto para cualquier algoritmo de lectura de palabras que no conozca la longitud de antemano.malloc
asignación estaba "suficientemente acolchada" a cada lado y el sistema VM aplicaba protección granular de byte para esa asignación ... si el puntero está alineado suponiendo queint
la alineación natural trivial de 32 bits ) sea discutible, todavía es posible que esa lectura alineada lea más allá del tamaño de la asignación. CUALQUIER lectura más allá del tamaño de la asignación esundefined behavior
.mmap
, entonces la alineación es suficiente.Simplemente busque "strstr más rápido", y si ve algo de interés, pregúnteme.
En mi opinión, se imponen demasiadas restricciones (sí, todos queremos lineal sub-lineal en el buscador máximo), sin embargo, se necesita un programador real para intervenir, hasta entonces creo que el enfoque hash es simplemente una solución ingeniosa (limbo) bien reforzado por BNDM para 2..16 patrones más cortos).
Solo un ejemplo rápido:
Haciendo Buscar un patrón (por 32bytes) en Cuerda (206908949bytes) como uno de línea ... Skip-Rendimiento (más grande-la-mejor): 3041%, 6801754 salta / iteraciones Railgun_Quadruplet_7Hasherezade_hits / Railgun_Quadruplet_7Hasherezade_clocks: 0/58 Railgun_Quadruplet_7Hasherezade rendimiento: 3483KB / reloj
Haciendo la búsqueda del patrón (32bytes) en la cadena (206908949bytes) como una línea ... Saltar-rendimiento (más grande-mejor): 1554%, 13307181 saltos / iteraciones Boyer_Moore_Flensburg_hits / Boyer_Moore_Flensburg_clocks: 0/83 Boyer_Moore_Flensburg performance: 0/83 / reloj
Haciendo una búsqueda de patrón (32bytes) en cadena (206908949bytes) como una línea ... Saltar rendimiento (mayor-mejor): 129%, 160239051 saltos / iteraciones Two-Way_hits / Two-Way_clocks: 0/816 Two -Rendimiento del camino : 247 KB / reloj
Sanmayce,
Saludos
fuente
El Algoritmo de dos vías que mencionas en tu pregunta (¡que por cierto es increíble!) Se ha mejorado recientemente para que funcione de manera eficiente en palabras de varios bytes a la vez: Coincidencia óptima de cadenas empaquetadas .
No he leído todo el documento, pero parece que confían en un par de nuevas instrucciones especiales de CPU (incluidas, por ejemplo, en SSE 4.2) que son O (1) para su reclamo de complejidad temporal, aunque si no están disponibles pueden simúlelos en tiempo O (log log w) para palabras w-bit que no suenan tan mal.
fuente
Podría implementar, por ejemplo, 4 algoritmos diferentes. Cada M minutos (para determinar empíricamente) ejecuta los 4 en datos reales actuales. Acumula estadísticas sobre N carreras (también TBD). Luego use solo el ganador para los próximos M minutos.
Registre las estadísticas de las ganancias para que pueda reemplazar los algoritmos que nunca ganan con otros nuevos. Concentre los esfuerzos de optimización en la rutina más ganadora. Preste especial atención a las estadísticas después de cualquier cambio en el hardware, la base de datos o la fuente de datos. Incluya esa información en el registro de estadísticas si es posible, para que no tenga que averiguarlo a partir de la fecha / hora del registro.
fuente
Recientemente descubrí una buena herramienta para medir el rendimiento de los diversos algos disponibles: http://www.dmi.unict.it/~faro/smart/index.php
Tu podrias encontrar esto útil. Además, si tengo que responder rápidamente al algoritmo de búsqueda de subcadenas, iría con Knuth-Morris-Pratt.
fuente
También es posible que desee tener diversos puntos de referencia con varios tipos de cadenas, ya que esto puede tener un gran impacto en el rendimiento. Los algos realizarán diferencias basadas en la búsqueda del lenguaje natural (e incluso aquí aún puede haber distinciones de grano fino debido a las diferentes morfologías), cadenas de ADN o cadenas aleatorias, etc.
El tamaño del alfabeto jugará un papel en muchos algos, al igual que el tamaño de la aguja. Por ejemplo, Horspool es bueno en el texto en inglés pero malo en el ADN debido al diferente tamaño del alfabeto, lo que dificulta la regla de mal carácter. La introducción del buen sufijo lo alivia enormemente.
fuente
No sé si es lo mejor, pero he tenido una buena experiencia con Boyer-Moore .
fuente
Esto no responde directamente a la pregunta, pero si el texto es muy grande, ¿qué hay de dividirlo en secciones superpuestas (superposición por la longitud de un patrón), luego busca simultáneamente las secciones usando hilos. Con respecto al algoritmo más rápido, Boyer-Moore-Horspool creo que es uno de los más rápidos, si no el más rápido, entre las variantes de Boyer-Moore. Publiqué un par de variantes de Boyer-Moore (no sé su nombre) en este tema Algoritmo más rápido que Búsqueda de BMH (Boyer – Moore – Horspool) .
fuente
El más rápido es actualmente EPSM, de S. Faro y OM Kulekci. Ver http://www.dmi.unict.it/~faro/smart/algorithms.php?algorithm=EPSM&code=epsm
"Coincidencia exacta de cadenas empaquetadas" optimizado para SIMD SSE4.2 (x86_64 y aarch64). Se realiza estable y mejor en todos los tamaños.
El sitio al que lo vinculé compara 199 algoritmos de búsqueda rápida de cadenas, y los habituales (BM, KMP, BMH) son bastante lentos. EPSM supera a todos los demás mencionados aquí en estas plataformas. También es lo último.
fuente