MPFC PDF
MPFC PDF
MPFC PDF
Análisis de algoritmos
de búsqueda de un solo patrón
Sergio Talens-Oliag
Noviembre 1997
Capítulo 1. Introducción .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 1
1.1. Origen del proyecto .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 1
1.2. Planteamiento general .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 2
1.3. Uso del C++ .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 3
1.4. Modelo de programación .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 4
1.5. El entorno de trabajo .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 5
iii
Referencias .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 64
iv
Capítulo 1. Introducción
El desarrollo de un programa informático consta de varias fases o pasos; comienza con la especificación
del problema a resolver, continúa con el diseño, implementación y prueba de una o varias soluciones y
termina con la evaluación de la aplicación obtenida, sin olvidar la documentación de todo el proceso.
En la etapa de diseño es fundamental identificar correctamente los datos y operaciones que vamos
a manejar, pero, aunque los encontremos, esto no es suficiente para que el resultado sea satisfactorio; si
deseamos un resultado óptimo deberemos evaluar cuales son los tipos de datos y algoritmos más adecuados
para la implementación.
Para elegir esas estructuras y algoritmos debemos conocer las distintas alternativas y disponer
de alguna herramienta que nos permita comprenderlas y compararlas. Este proyecto es un intento de
proporcionar esa herramienta para el estudio de los algoritmos y estructuras implicados en el tratamiento
de cadenas de símbolos alfabéticos, mediante la implementación de una biblioteca de clases C++ que
incorpora mecanismos para estudiar los algoritmos desde distintos puntos de vista.
La implementación presentada aquí incluye fundamentalmente los tipos de datos y algoritmos rela-
cionados con la búsqueda de una subcadena en otra, dejando para posibles ampliaciones la implementación
de la búsqueda simultánea de varias subcadenas, la búsqueda aproximada o con errores y la búsqueda con
expresiones regulares.
1
1.1. Origen del proyecto 2
Este nuevo planteamiento contemplaba dos tipos de texto en función de la frecuencia con la que
cambia: dinámico y estático [Bae92a]. La diferencia fundamental entre ambos es que las estructuras y al-
goritmos más eficientes para almacenar el texto o realizar operaciones de consulta necesitan preprocesarlo,
algo que normalmente sólo vale la pena hacer cuando el texto cambia con poca frecuencia o el número de
operaciones de consulta es muy elevado entre actualizaciones (como sucede hoy día con los buscadores
de Internet).
Esta biblioteca se estructuraba en torno a las operaciones de edición (inserción, borrado, sustitución),
búsqueda (de subcadenas y expresiones regulares), ordenación (teniendo en cuenta que el orden binario
no es siempre el más adecuado), filtrado (recodificación, compresión y en general cualquier tipo de
preproceso) y gestión de entrada/salida (mecanismos de buffering y almacenamiento de estructuras) de
las cadenas de texto, definiendo los tipos de datos necesarios para cada una de ellas [Aho83]. En estos
apartados distinguíamos entre texto dinámico y estático cuando era apropiado.
Como se ve, la idea era excesivamente ambiciosa, por lo que al final se decidió centrar el tema en
el texto dinámico, comenzando por los algoritmos de búsqueda. Dada la gran cantidad de algoritmos
existentes y lo difícil que era comprender el funcionamiento de algunos de ellos, sólo se implementaron
los algoritmos de búsqueda de un solo patrón, diseñando herramientas para su análisis.
Este proyecto es el resultado de ese proceso.
Símbolo
Elemento de un tipo de datos que tiene definida una relación de orden.
Alfabeto
Conjunto finito y no vacío de símbolos.
Cadena de símbolos
Secuencia finita de símbolos.
Con las cadenas realizaremos operaciones de edición (inserción, borrado y sustitución de símbolos o
subcadenas), comparación (de igualdad o de orden) y búsqueda de unas en otras.
La idea es que los algoritmos y estructuras que se emplean en el tratamiento de cadenas no están
restringidos a caracteres sino que, a efectos prácticos, son aplicables a cualquier tipo de datos que
queramos tratar como secuencias.
Según la aplicación podríamos usar como símbolos las figuras geométricas elementales, las
moléculas, las palabras reservadas de un lenguaje de programación o cualquier otro tipo de datos.
Las operaciones de edición (inserción, borrado y sustitución) son independientes del tipo de datos,
ya que emplean únicamente las posiciones de los símbolos en la secuencia. En el caso de la búsqueda la
única operación necesaria es la comparación, por lo que sólo necesitamos definir una relación de igualdad
entre símbolos, de manera que dos cadenas serán iguales si todos sus símbolos lo son. Por último, para
ordenar las cadenas necesitamos una relación de orden que nos diga si un símbolo es menor que otro, para
poder compararlas símbolo a símbolo y determinar cual de es la menor cadena.
El alfabeto es el conjunto de símbolos que pueden aparecer en una cadena y en muchos casos no
será necesario definirlo, ya que el propio tipo de datos define cuales son los valores aceptables. De todos
modos, algunos de los algoritmos presentados dependen de él para construir tablas indexadas por símbolo
1.2. Planteamiento general 3
1
Si empleamos vectores para las tablas es interesante que los símbolos puedan convertirse en enteros con coste de orden uno, ya que
un método que tenga un orden mayor puede anular la eficacia del algoritmo que las emplee.
2
En fase de revisión, con un borrador que tiene más de 700 páginas [C++96].
1.3. Uso del C++ 4
La solución adoptada es no emplear las nuevas características del lenguaje que no se han podido
probar y utilizar las librerías disponibles que proporcionan características que estarán incluidas en el
estándar (cómo la Standard Template Library, que se ha incorporado con pequeñas modificaciones)
aun a coste de perder en eficiencia; cuando el estándar esté aceptado es muy probable que aparezcan
implementaciones gratuitas y comerciales más eficientes y, se mejore la calidad de los compiladores.
1
Es suficiente con que incluyan las clases de la Standard Template Library ([Ste95]) y soporten el tipo string definido en el borrador
del C++ estándar de abril de 1995.
1.5. El entorno de trabajo 6
ligeramente los ficheros de configuración se pueden obtener buenos resultados, con la ventaja de poder
generar la documentación en varios formatos sin esfuerzo.
De cualquier modo, eran necesarias demasiadas modificaciones para obtener los resultados deseados,
por lo que se realizó una conversión al formato del sistema Lout ( [Kin96a], [Kin96b], [lout] ), un sistema
de composición de documentos similar al LATEX [Lam86], pero más sencillo de utilizar. La memoria se
completo en este sistema.
Capítulo 2. Algoritmos de búsqueda simple
Como la función principal de la biblioteca es estudiar los algoritmos de búsqueda de un sólo patrón,
describiremos en este capítulo el problema de la búsqueda y la implementación de los algoritmos, dejando
para el siguiente los detalles sobre las clases que integran la biblioteca.
Dada una subcadena x, con |x| = m, y una cadena y, con |y| = n, donde m > 0, n > 0 y m ≥ n, si
x aparece como subcadena de y entonces determinar la posición en y de la primera ocurrencia
de x, es decir, calcular el mínimo valor de i tal que yi…i + m − 1 = x1…m.
• Fuerza bruta
• Karp-Rabin [KR87]
• Knuth-Morris-Pratt [KMP77]
• Shift Or [Bae92b]
• Boyer-Moore [BM77]
• Boyer-Moore-Horspool [Hor80]
Dividiremos el estudio de cada algoritmo en tres apartados: descripción del algoritmo, análisis de costes
e implementación.
En la descripción de los algoritmos, denominaremos patrón a la subcadena buscada y cadena o texto
a la cadena objeto de la búsqueda y emplearemos las letras m y n para referirnos a la longitud del patrón
y la cadena respectivamente, asumiendo siempre que n > m.
En los ejemplos utilizaremos la salida del programa de prueba, donde el patrón aparece sobre el
texto mostrando su posición relativa y se indica qué símbolos han sido comparados con éxito y cual es
el símbolo actual colocando los caracteres «*» (comparados) y «-» (posición actual) bajo el patrón y el
texto. Por ejemplo, si buscamos el patrón «de» en la cadena «Buscando en un texto de prueba»
la salida:
de
Buscando en un texto de prueba
*-
indica que hemos encontrado la letra d y vamos a comparar la o del texto con la e del patrón.
7
2.1. Definición del problema 8
Cada vez que se produce un acierto parcial (-- PARTIAL MATCH --) o un acierto total
(-- FULL MATCH --) se indica mostrando un mensaje. Cuando el algoritmo utiliza estructuras de pre-
proceso para saltar también se muestran sus valores intercalados entre los distintos pasos de la búsqueda,
aunque únicamente cuando son utilizados. El significado de esos parámetros se comenta al describir los
algoritmos.
El análisis de costes se divide en temporal y espacial. Para cada algoritmo comentaremos los costes
temporales para los casos mejor, peor y promedio de la búsqueda de todas las ocurrencias del patrón
en el texto, aunque sin dar una demostración formal, ya que esta se puede encontrar en la bibliografía
relacionada con cada algoritmo. Del coste espacial nos limitaremos a indicar lo que ocupan las estructuras
de preproceso de cada algoritmo.
En el apartado de implementación utilizamos una versión simplificada de los algoritmos escrita en
C. Lo que se pretende con esto es mostrar claramente el funcionamiento del algoritmo sin incluir detalles
que pueden dificultar la comprensión. De entrada, las versiones en C acceden al texto y al patrón a través
de índices, mientras que la implementación real usa punteros. En la versión real los algoritmos retornan
valores (tamaño del texto o posición del primer acierto) y cuando se encuentra un acierto se realizan
verificaciones para saber si vamos a continuar o no. De cualquier modo, la versión completa en C++ se
puede ver directamente en el código de la biblioteca.
La versión simplificada asume que tenemos definidas las variables txt y pat para acceder al texto
y al patrón y los valores txt_size y pat_size para acceder a los tamaños de ambas estructuras (como
realmente sucede en la implementación en C++). Además, se supone que el tamaño del texto es mayor o
igual al del patrón y que ambos son distintos de 0 (la implementación no ejecuta el algoritmo si esto no
se cumple).
Por último señalar que la implementación de los algoritmos no se corresponde con el planteamiento
original de los autores, ya que los algoritmos que emplean tablas de preproceso se han modificado siguiendo
un modelo similar al de la fuerza bruta, en el que el bucle principal compara el texto con el primer elemen-
to del patrón (el último en el caso de los algoritmos de Boyer-Moore, Boyer-Moore-Horspool y Sunday
QuickSearch) y actualiza el iterador del texto si no hay coincidencia (dependiendo del tipo de algoritmo
se utilizan las tablas de preproceso, pero los valores que dependen del patrón son fijos y por tanto se alma-
cenan en variables locales para evitar accesos a vector). Cuando encontramos un acierto parcial se entra
en otro bucle que se desplaza en el patrón y usa la información del preproceso.
Con esta implementación, los valores que no se utilizan no son actualizados, evitando operaciones
innecesarias sin introducir otros retardos. Esto se puede hacer porque todos los algoritmos tienen un
comportamiento especial a partir del momento en el que es encontrado un acierto parcial, pero antes de
haberlo hecho no necesitan utilizar ni actualizar muchas de las variables locales.
2.2.1. Descripción
Este algoritmo es la forma más simple de aproximarse al problema de búsqueda de subcadenas. La
idea es ir deslizando el patrón sobre el texto de izquierda a derecha, comparándolo con las subcadenas del
mismo tamaño que empiezan en cada carácter del texto.
El funcionamiento es como sigue: vamos comparando el primer carácter del patrón con cada uno de
los caracteres de la cadena, cuando se encuentra un acierto se compara el segundo carácter del patrón con
el carácter del texto alineado con él (el que sigue al que causó el acierto), si coinciden seguimos con el
tercero, cuarto, etc. hasta que se encuentra un fallo o se termina el patrón. Si alcanzamos el final del patrón
hemos encontrado la subcadena, si nos detenemos antes volvemos a comparar el patrón con la subcadena
que comienza en el carácter siguiente al primer acierto, es decir, deslizamos el patrón una posición a
la derecha.
2.2. Fuerza bruta 9
SEARCHING ...
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
*-
texto
Este es un texto de prueba.
**-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
2.2. Fuerza bruta 10
texto
Este es un texto de prueba.
-
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
*-
texto
Este es un texto de prueba.
**-
texto
Este es un texto de prueba.
***-
texto
Este es un texto de prueba.
****-
texto
Este es un texto de prueba.
*****
-- FULL MATCH --
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
*-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
2.2. Fuerza bruta 11
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
Number of matches: 1
Match positions: 11
Este algoritmo necesita poder avanzar y retroceder en el texto, de manera que para textos almacenados
en disco y no en memoria es necesario el empleo de buffers.
Por último indicar que, dadas las características de este algoritmo, el reinicio de la búsqueda consiste
en continuar desde el carácter siguiente al que inició el acierto, como se puede observar en el ejemplo.
2.2.2. Costes
En el mejor caso, el primer elemento del patrón no está en el texto, de manera que ningún carácter
coincide con él. Tenemos un coste temporal de orden Ω (n), el algoritmo es lineal.
En el peor caso el coste temporal de este algoritmo es de Ω (mn), que seria aquel en el que
encontramos el patrón en todas las subcadenas del texto.
En promedio el coste temporal es menor que Ω (mn), ya que no precisamos comparar cada vez los
m caracteres, sólo comparamos hasta que se detecta un fallo y las probabilidades de falsos comienzos son
muy inferiores a 1 en general. Si buscamos en textos normales será de orden Ω (m+n) en la mayoría de
los casos [Aho90].
El coste espacial es nulo, salvo que consideremos parte del algoritmo los buffers empleados para
almacenar el patrón y la subcadena del texto con la que este está alineado, en cuyo caso será de Ω (m).
2.2.3. Implementación
Podemos expresar el algoritmo en C de la siguiente manera:
brute_force() {
int ti, pi, tj; /* variables auxiliares */
ti = 0; /* índice en el texto */
while (ti < txt_size - pat_size){
if (txt[ti] = pat[0]) { /* Acierto parcial */
pi = 0; /* índice acierto parcial en patrón */
tj = ti; /* índice acierto parcial en texto */
do {
tj++; pi++;
if (pi == pat_size){
acierto_en (ti);
break; /* continuamos en ti + 1 */
} /* (ti se incrementa al salir del bucle) */
} while (txt[tj] == pat[pi]);
} /* Fin Acierto Parcial */
ti++;
} /* Fin bucle while */
2.2. Fuerza bruta 12
Nótese que la condición de salida del bucle principal detiene la búsqueda si el texto que queda es menor
que el tamaño del patrón, es decir, cuando ya no es posible encontrar un acierto, y que el empleo de una
variable auxiliar nos evita el incremento y decremento del índice en el texto cuando se produce un acierto
parcial; ti siempre nos dará la posición relativa del patrón respecto al texto.
Para mejorar el rendimiento se pueden definir variables para eliminar operaciones aritméticas
(txt_size - pat_size es un valor constante durante la ejecución del algoritmo, pero el compilador
no tiene porque detectarlo, si usamos una variable evitamos una resta en cada iteración) y accesos a vector
(el valor del primer carácter del patrón es consultado en cada iteración del bucle principal, guardándolo
en un registro eliminamos accesos a vector innecesarios).
La versión real aplica las modificaciones mencionadas y utiliza punteros para los tres índices
definidos, ya que todos ellos son de acceso secuencial al texto y al patrón.
2.3. Karp-Rabin
2.3.1. Descripción
El algoritmo fue enunciado por Karp y Rabin en [KR87]. Se trata de un algoritmo probabilístico
que adapta técnicas de dispersión (hashing) a la búsqueda de patrones. Se basa en tratar cada uno de los
grupos de m caracteres del texto como un índice en una tabla de dispersión, de manera que si la función
de dispersión de los m caracteres del texto coincide con la del patrón probablemente hemos encontrado
un acierto (hay que comparar el texto con el patrón, ya que la función de dispersión elegida puede
presentar colisiones).
La función de dispersión tiene la forma d(k) = kmodq (d(k) es igual al resto de la división k/q), con
q un número primo grande que será el tamaño de la tabla de dispersión.
Para transformar cada subcadena de m caracteres en un entero lo que hacemos es representar los
caracteres en una base B que en el planteamiento original coincide con el tamaño del alfabeto.Por ejemplo,
el entero xi correspondiente a la subcadena txti…i + m − 1 sería:
xi + 1 = xi ∗ B − txti ∗ Bm + txti + m
Es decir, si la cadena es un número en base B, el nuevo valor será el resultado de multiplicar por la base
el valor anterior eliminando el dígito de mayor peso (que ya no está en la cadena) y añadiendo como
componente de menor peso el valor del nuevo símbolo.
Por ejemplo, si B = 10 y m = 4 (queremos obtener valores de 4 dígitos), dado el texto txt = 256789
calcularíamos el primer valor haciendo:
Y obtendríamos x1 haciendo:
2.3. Karp-Rabin 13
El problema de este planteamiento es que, dependiendo de la longitud del patrón, el número entero
equivalente a la cadena supera el rango de enteros representable por computador. Si la base (es decir, el
tamaño del alfabeto) es pequeña, siempre que Bm sea un número dentro del rango de enteros representables,
la representación de la cadena como un número en base B será una función de dispersión perfecta. Para
enteros de 32 bits y alfabetos de tamaño 256 la función de dispersión es perfecta para patrones de tamaño
menor o igual a cuatro.
Como la representación en base B causa problemas de rango lo que se hace es utilizar la función
módulo (resto de la división). La ventaja fundamental es que la operación módulo es asociativa, por lo
que podemos aplicarla después de cada operación de actualización.
Si empleamos como divisor un número primo tenemos la garantía de que el resto de la división
es siempre el dividendo para todos los números menores que el divisor. Cómo todas las operaciones de
actualización emplean sólo un símbolo del alfabeto, escogiendo el mayor número primo que multiplicado
por el tamaño del alfabeto (en realidad el mayor índice de los símbolos más uno) nos de un valor
representable en el computador tenemos un divisor que no causará nunca desbordamiento, es decir, no se
saldrá del rango.
Así, la formula anterior para actualizar el valor de la cadena en base B se transforma en la función
de dispersión siguiente:
Donde el factor B ∗ Q del cálculo de d1(txti + 1) se emplea para evitar un valor negativo.
Al final, el algoritmo funciona de la siguiente manera:
En primer lugar se realiza el computo de la función de dispersión del patrón y los primeros m
caracteres del texto. Mientras d (pat) sea distinto de d (txt) se calcula la clave siguiente haciendo uso de
la clave actual, empleando la fórmula anterior. En caso de que las funciones de dispersión coincidan se
realiza la comparación carácter a carácter para verificar que no ha sido una colisión.
Como se ve, este algoritmo reduce la búsqueda de caracteres a comparaciones de enteros, pero,
aunque las operaciones matemáticas sean poco costosas, superan en mucho el coste de las comparaciones
de símbolos del resto de algoritmos.
Para reducir algo el coste de las operaciones podemos utilizar una base que sea potencia de dos (la
menor que sea mayor o igual que el tamaño del alfabeto), de manera que los productos de enteros pueden
p
ser reemplazados por desplazamientos en registro, ya que x ∗ 2 ≡ x << p, donde << es un desplazamiento
a la izquierda de tantos bits como indique el operando derecho.
La ejecución del ejemplo del apartado anterior para este algoritmo sería:
PREPROCESSING ...
2.3. Karp-Rabin 14
texto
-
Bm = 1
hpat = 116
texto
-
Bm = 256
hpat = 29797
texto
-
Bm = 65536
hpat = 7628152
texto
-
Bm = 30
hpat = 6653452
texto
-
Bm = 7680
hpat = 399444
SEARCHING ...
texto
Este es un texto de prueba.
htxt = 8161434
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7514109
hpat = 399444
texto
Este es un texto de prueba.
htxt = 3017033
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7425248
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7793739
hpat = 399444
texto
Este es un texto de prueba.
htxt = 2906344
hpat = 399444
texto
Este es un texto de prueba.
2.3. Karp-Rabin 15
htxt = 191471
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7466538
hpat = 399444
texto
Este es un texto de prueba.
htxt = 3028809
hpat = 399444
texto
Este es un texto de prueba.
htxt = 85319
hpat = 399444
texto
Este es un texto de prueba.
htxt = 6899212
hpat = 399444
texto
Este es un texto de prueba.
htxt = 399444
hpat = 399444
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
*-
texto
Este es un texto de prueba.
**-
texto
Este es un texto de prueba.
***-
texto
Este es un texto de prueba.
****-
texto
Este es un texto de prueba.
*****
-- FULL MATCH --
texto
Este es un texto de prueba.
htxt = 21311
hpat = 399444
2.3. Karp-Rabin 16
texto
Este es un texto de prueba.
htxt = 8207868
hpat = 399444
texto
Este es un texto de prueba.
htxt = 3017063
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7432928
hpat = 399444
texto
Este es un texto de prueba.
htxt = 6876200
hpat = 399444
texto
Este es un texto de prueba.
htxt = 2896968
hpat = 399444
texto
Este es un texto de prueba.
htxt = 8145973
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7750277
hpat = 399444
texto
Este es un texto de prueba.
htxt = 168653
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7523351
hpat = 399444
texto
Este es un texto de prueba.
htxt = 7348996
hpat = 399444
Number of matches: 1
Match positions: 11
Si lo comparamos con el ejemplo anterior vemos que el único acierto parcial es el correspondiente al
2.3. Karp-Rabin 17
acierto completo. El valor Bm mostrado en el preproceso es el de la base (tamaño del alfabeto) elevada al
tamaño del patrón. Este valor se calcula en el preproceso, ya que hasta no disponer de patrón no tenemos
su tamaño. El cálculo se hace a la vez que el de la función de dispersión del patrón usando desplazamientos
en registro, como veremos en la implementación.
Después de un acierto continuamos calculando la siguiente función de dispersión del texto, de modo
similar al caso de la fuerza bruta.
La ventaja más comentada de este algoritmo es que es extensible a búsqueda en dos dimensiones.
2.3.2. Costes
El preproceso tiene coste temporal de Ω (m), aunque se suele ignorar ya que se puede hacer junto
con el inicio de la búsqueda.
El coste temporal de la búsqueda es de Ω (m+n) en promedio. La m es por la comparación del patrón
y el texto en un acierto, si no hay colisiones la n es el recorrido de todos los caracteres (el algoritmo es
lineal). En el peor caso el coste es Ω (mn), si todas las secuencias de m caracteres tienen la misma función
de dispersión realizaríamos una búsqueda por fuerza bruta (en la práctica esto supondría buscar un patrón
con todos sus símbolos iguales en un texto que sólo contenga ese símbolo), aunque dadas las constantes
empleadas este caso sólo se presentará cuando realmente todas las subcadenas del texto coincidan con el
patrón ([Dav86]).
El coste espacial es nulo ya que no almacenamos la tabla de dispersión, salvo que consideremos el
almacenamiento del patrón como parte del algoritmo, en cuyo caso será Ω (m).
2.3.3. Implementación
Presentaremos a continuación las versiones en C del preproceso y la función de búsqueda. En ambos
casos se supone definida una base B que es potencia de dos, de modo que B ≡ 2BS y un número primo Q
que cumple las condiciones mencionadas en la descripción del algoritmo.
El preproceso del algoritmo calcula los valores Bm y hpat (función de dispersión para el patrón):
preproceso_karp_rabin() {
int pi; /* variables auxiliares */
Bm = 1; hpat = pat[0]; /* inicialización de variables */
for (pi = 1; pi < pat_size; pi++) {
Bm = (Bm << BS) % Q;
hpat = ( (hpat << BS) + pat[pi]) ) % Q;
}
};
El cálculo del primer valor de hpat se pone fuera del bucle para inicializar a la vez el valor de Bm.
La versión incluida en la biblioteca calcula los valores de B y BS en función del tamaño del alfabeto
empleado y selecciona el valor del número primo Q de una tabla usando como índice el valor de BS
obtenido antes.
Con el patrón preprocesado (es decir, suponiendo inicializados hpat y Bm) podemos ejecutar la
función de búsqueda:
karp_rabin() {
int ti, pi, tj; /* variables auxiliares */
ti = 0; /* índice en el texto */
htxt = 0; /* calculo de htext */
2.3. Karp-Rabin 18
Para este algoritmo se pueden hacer los mismos comentarios que para el de fuerza bruta: el algoritmo se
detiene cuando ya no se puede encontrar un acierto, ti siempre apunta a la posición relativa del patrón
respecto al texto y txt_size - pat_size se puede almacenar en una variable local. La versión real
aplica las modificaciones mencionadas y utiliza punteros para los índices.
2.4. Knuth-Morris-Pratt
2.4.1. Descripción
Este algoritmo es similar al de la fuerza bruta, pero en el caso de un acierto parcial utiliza el
conocimiento de los caracteres previamente analizados para no retroceder en el texto. Además, como
siempre avanza hacia adelante, no necesita mecanismos de buffering al realizar búsquedas en fichero. El
algoritmo se describe en [KMP77].
Para evitar el retroceso, el algoritmo preprocesa el patrón para construir una tabla, denominada tabla
de siguientes, que indica que posición del patrón debemos alinear con el texto en caso de fallo durante un
acierto parcial.
A continuación veremos un ejemplo de la ejecución del algoritmo, que emplearemos más adelante
como referencia para explicar como se realiza el preproceso y el uso de los valores calculados.
PREPROCESSING ...
text
-
shift [0] = -1
resume = -1
2.4. Knuth-Morris-Pratt 19
text
-
shift [1] = 0
resume = 0
text
-
shift [1] = 0
resume = -1
text
-
shift [2] = 0
resume = 0
text
-
shift [2] = 0
resume = -1
text
-
shift [3] = -1
resume = 0
text
shift [4] = 1
resume = 1
PREPROCESSOR STRUCTURES
0 1 2 3 : i
t e x t : pat[i]
-1 0 0 -1 : shift [i]
resume = 1
SEARCHING ...
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
-- PARTIAL MATCH --
text
Este es un texto de prueba.
*-
text
Este es un texto de prueba.
**-
shift [2] = 0
2.4. Knuth-Morris-Pratt 20
text
Este es un texto de prueba.
-
shift [0] = -1
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
-- PARTIAL MATCH --
text
Este es un texto de prueba.
*-
text
Este es un texto de prueba.
**-
text
Este es un texto de prueba.
***-
text
Este es un texto de prueba.
****
-- FULL MATCH --
resume = 1
-- PARTIAL MATCH --
text
Este es un texto de prueba.
*-
shift [1] = 0
text
Este es un texto de prueba.
-
2.4. Knuth-Morris-Pratt 21
shift [0] = -1
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
text
Este es un texto de prueba.
-
Number of matches: 1
Match positions: 11
Como se ve en el ejemplo, cuando se produce un fallo en una posición del patrón cuyo siguiente es i (con
i ≥ 0) deberemos alinear el carácter que ocupa la posición i en el patrón con el carácter actual en el texto
(desplazando el patrón, pero sin movernos en el texto), y continuar comparando a partir de esta posición.
Cuando el siguiente es -1 deberemos desplazarnos a la siguiente posición del texto, alineando el primer
caracter del patrón con ella y reiniciar la búsqueda.
En realidad, la tabla de siguientes se emplea para determinar el máximo prefijo del patrón que
podemos encontrar inmediatamente antes de cada símbolo. Cuando se produce un fallo al comparar el
contenido de una posición del patrón con el texto, la tabla de siguientes «recuerda» los símbolos que hemos
visto anteriormente, de manera que si el fallo se produce despues de haber visto un prefijo, alineamos el
patrón con él y continuamos comparando, sin retroceder en el texto.
El valor en la tabla de siguientes para el elemento que ocupa la posición j del patrón será el máximo
índice i < j que verifique pati ≠ patj para i ≥ 0 y pat0…i − 1 ≡ patj − i…j − 1 para i > 0. En caso de no existir un
valor de i que verifique la condición le asignamos un valor que no sea un índice del vector (para simplificar
usaremos i = − 1). La tabla de siguientes se calcula para los valores de j entre 0 y m.
Para recuperarnos después de un acierto utilizamos el valor de la tabla de siguientes que correspon-
dería a un símbolo que se encuentra en la posición que sigue al último elemento del patrón (el que estaría
en la posición m), suponiendo que el símbolo adicional no coincide con ninguno de los visitados anterior-
mente. Este valor es el resume de los ejemplos.
Para el patrón mimo, la tabla de siguientes y el valor de recuperación serán:
2.4. Knuth-Morris-Pratt 22
i : 0 1 2 3
pat[i] : m i m o
shift[i] : -1 0 -1 1
resume = 0
El valor de shift0 será siempre -1, ya que no existe un i que verifique 0 ≤ i y i < 0, shift1 será 0, ya que pat0
≠ pat1, shift2 valdrá -1, ya que para i ≡ 0 no se cumple la primera condición (pat0 ≡ pat2), shift3 será 1,
ya que pat0…1− 1 ≡ pat3− 1…3− 1 y pat1 ≠ pat3. Si existiera un símbolo adicional distinto de los anteriores su
valor sería 0, ya que pat0 ≠ pat4.
El funcionamiento de este algoritmo es similar al de Aho-Corasik [Aho75] para búsqueda de
múltiples patrones, ya que ambos se basan en la misma idea. La diferencia está en que para el caso de un
solo patrón este algoritmo mejora los costes espaciales y de tiempo de preproceso.
2.4.2. Costes
El coste temporal de este algoritmo es independiente del tamaño del alfabeto y es de orden Ω (n) en
el mejor caso y de Ω (m+n) en el peor [KMP77].
El coste espacial será de Ω (m), ya que para almacenar la tabla de siguientes usamos un vector del
mismo tamaño que el patrón.
2.4.3. Implementación
En el código en C del preproceso supondremos declarado un vector de enteros shift del mismo
tamaño del patrón y una variable resume en la que almacenaremos el índice empleado para recuperarnos
después de un acierto.
preproceso_knuth_morris_pratt () {
int pi; /* variables auxiliares */
pi=0; resume = -1; /* inicialización de variables */
shift[pi] = resume;
while (pi < pat_size) {
if (resume < 0 || pat[pi] == pat[resume]) {
pi++; resume++;
if ( (pat[pi] < pat_size) && (pat[i] == pat[resume]) ) {
shift[pi] = shift[resume];
} else {
shift[pi] = resume;
}
} else {
resume = shift[resume];
}
}
}
El preproceso comienza asignando a la primera posición el valor -1, y va avanzando en el patrón cada
vez que resume == -1 o el símbolo actual del patrón es igual al que ocupa la posición resume (cuando
hemos alcanzado la posición actual o hemos encontrado un prefijo). Si resume <= 0 actualizamos su
valor asignándole el valor del siguiente de la posición a la que apunta.
Al avanzar en el patrón asignamos el valor de la tabla de siguientes en función del símbolo que
encontremos, el siguiente será el mismo que el del simbolo que ocupa la posición resume si ambos
símbolos son iguales (el símbolo actual forma parte de un prefijo, y por lo tanto ya hemos calculado su
siguiente), o, en caso contrario, será el valor resume, que contendrá la posición alcanzada en el patrón.
2.4. Knuth-Morris-Pratt 23
En realidad, el preproceso es una ejecución del algoritmo (en su forma original), buscando el patrón
sobre si mismo.
El algoritmo de búsqueda será:
knuth_morris_pratt (){
int ti, pi; /* variables auxiliares */
ti = 0; /* índice en el texto */
while (ti < txt_size - pat_size){
if (txt[ti] = pat[0]) { /* Acierto parcial */
pi = 0; /* índice acierto parcial en patrón */
do {
ti++; pi++;
if (pi == pat_size){
acierto_en (ti - pat_size);
/* Recuperación tras un acierto */
if (resume < 0) {
ti++; /* Continuamos en ti + 1 */
break;
} else if (pi == 0) {
break; /* Reinicio al principio del patrón */
} else {
pi = resume; /* Seguimos en el acierto parcial */
}
/* Fin Recuperación tras un acierto */
}
/* Paso de un acierto parcial */
if (pat[pi] != txt[ti]) {
if (shift[pi] < 0) {
ti++; /* Avanzamos en el texto */
break;
} else {
if (shift[pi] == 0)
break; /* Reinicio al principio del patrón */
else
pi = shift[pi];
}
}
/* Fin Paso de un acierto parcial */
} while (txt[ti] == pat[pi]);
} /* Fin Acierto Parcial */
ti++;
} /* Fin bucle while */
}
Al igual que en otros algoritmos, la búsqueda se detiene cuando ya no se puede encontrar un acierto y la
ejecución de la condición de salida se puede optimizar almacenando el valor de txt_size - pat_size
en una variable local.
La recuperación tras un acierto es identica al paso de un acierto parcial cuando los símbolos actuales
del patrón y el texto son distintos, pero resulta más sencillo y rápido repetir el código en lugar de usar
condicionales (salvo que tengamos muchos aciertos, la recuperación se ejecuta pocas veces, pero usando
condicionales haremos comprobaciones siempre que haya un acierto parcial).
La versión real aplica las modificaciones mencionadas y utiliza punteros para los dos índices..
2.5. Shift Or 24
2.5. Shift Or
2.5.1. Descripción
Este algoritmo se debe a Baeza-Yates y Gonnet ([Bae92b], [Bae91] ). Se basa en la teoría de
autómatas y en el uso de alfabetos finitos. Funciona recorriendo el texto de izquierda a derecha, carácter
a carácter, sin retroceder jamás. El algoritmo representa el estado de la búsqueda con un número y usa
desplazamientos en registro y una operación O lógica cada vez que se desplaza en el texto. Para simplificar
la actualización del estado y eliminar las comparaciones entre símbolos se emplea una tabla del tamaño
del alfabeto que asocia una máscara a cada símbolo.
El algoritmo emplea un método que en su planteamiento original es muy ineficiente, la idea es
representar el estado de la búsqueda con un vector de valores binarios que nos indican, para cada posición
i del patrón, si los últimos i caracteres del texto visitados son iguales a los primeros i caracteres del patrón.
Es decir, si estamos mirando el carácter j del texto, el valor de vi será verdadero si pat0…i ≡ txtj − i…j o falso
en caso contrario. Si el valor de la última posición es verdadero habremos encontrado un acierto (todos
los símbolos del patrón serán iguales a los del texto).
Para actualizar los valores de este vector tras cada avance en el texto podemos ir desplazando la
información de unas posiciones a otras, reduciendo las actualizaciones a la comparación del carácter actual
en el texto. Si hacemos vi + 1 = vi para i < m − 1, habremos comprobado si pat0…i − 1 ≡ txtj − (i − 1)…j, y sólo nos
faltará comprobar si pati ≡ txtj + 1 para cada i < m − 1. En realidad la última comprobación sólo se debe
hacer para v0 y los vi que tienen valor verdadero tras el desplazamiento.
El algoritmo tal y como lo acabamos de plantear es muy costoso, ya que aunque sólo miremos
un carácter del texto debemos compararlo con cada uno de los símbolos del patrón. Para evitar las
comparaciones en cada paso aprovechamos el hecho de que el alfabeto es finito, construyendo una tabla
de vectores binarios del tamaño del patrón para cada símbolo, que denominaremos tabla de máscaras.
Para cada símbolo del alfabeto el vector máscara nos indicará en que posiciones del patrón lo podemos
encontrar, asignando el valor verdadero al elemento o elementos correspondientes de la máscara.
Utilizando la tabla de máscaras eliminamos las comparaciones en la actualización después de cada
paso. En primer lugar desplazamos los valores del vector (vi + 1 = vi para i < m − 1) y luego añadimos
el componente aportado por el nuevo símbolo, haciendo (vi = vi )∧ (masktxt ,i ) (∧ es un Y lógico), es
j+1
decir, si los primeros i − 1 elementos del patrón coincidían con los caracteres del texto correspondientes
(txtj − (i − 1)…j) y txtj + 1 está en la posición i del patrón, el valor de vi será verdadero.
La ventaja de este planteamiento es que podemos emplear un número binario para representar los
vectores de booleanos siempre que el patrón sea de menor tamaño que el número de bits en una palabra
del computador, reduciendo las operaciones de actualización a un desplazamiento (shift) en registro y un
Y lógico (∧ ) a nivel de bit.
Siguiendo el razonamiento anterior, el algoritmo debería denominarse Shift-And ([Bae92b]), pero
como se ha implementado en C y los desplazamientos en registro de este lenguaje añaden ceros cuando
nos movemos hacia la izquierda, se ha modificado el método de actualización anterior para que funcione
utilizando operaciones O lógicas, representando con un 0 la presencia del símbolo y con un 1 la ausencia
(cuando hacemos el O de dos valores sólo obtendremos un cero si ambos son 0, que es lo mismo que
hacíamos antes con la operación Y).
Veamos un ejemplo:
PREPROCESSING ...
mask [ * ] = 11111111111111111111111111111111
texto
-
mask [’t’] = 01111111111111111111111111111111
texto
-
mask [’e’] = 10111111111111111111111111111111
texto
-
mask [’x’] = 11011111111111111111111111111111
texto
-
mask [’t’] = 01101111111111111111111111111111
texto
-
mask [’o’] = 11110111111111111111111111111111
PREPROCESSOR STRUCTURE
SEARCHING ...
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
*-
state = 01111111111111111111111111111111
mask [’e’] = 10111111111111111111111111111111
state << 1 = 00111111111111111111111111111111
nextstate = 10111111111111111111111111111111
texto
Este es un texto de prueba.
**-
state = 10111111111111111111111111111111
mask [’ ’] = 11111111111111111111111111111111
state << 1 = 01011111111111111111111111111111
nextstate = 11111111111111111111111111111111
2.5. Shift Or 26
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
-- PARTIAL MATCH --
texto
Este es un texto de prueba.
*-
state = 01111111111111111111111111111111
mask [’e’] = 10111111111111111111111111111111
state << 1 = 00111111111111111111111111111111
nextstate = 10111111111111111111111111111111
texto
Este es un texto de prueba.
**-
state = 10111111111111111111111111111111
mask [’x’] = 11011111111111111111111111111111
state << 1 = 01011111111111111111111111111111
nextstate = 11011111111111111111111111111111
texto
Este es un texto de prueba.
***-
state = 11011111111111111111111111111111
mask [’t’] = 01101111111111111111111111111111
state << 1 = 01101111111111111111111111111111
nextstate = 01101111111111111111111111111111
texto
Este es un texto de prueba.
****-
state = 01101111111111111111111111111111
mask [’o’] = 11110111111111111111111111111111
state << 1 = 00110111111111111111111111111111
nextstate = 11110111111111111111111111111111
2.5. Shift Or 27
texto
Este es un texto de prueba.
*****
-- FULL MATCH --
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
texto
Este es un texto de prueba.
-
Number of matches: 1
Match positions: 11
El preproceso asigna a cada símbolo del alfabeto una máscara con todos los valores a uno y despues va
recorriendo el patrón símbolo a símbolo, asignando un cero al bit que ocupa la posición visitada en la
máscara del símbolo correspondiente, sin modificar el valor del resto de bits.
La búsqueda se ejecuta mirando si la máscara de cada símbolo del texto tiene un cero en el primer bit.
Si lo tiene, se produce un acierto parcial, en el primer momento el estado tendrá un cero en el primer bit y
el resto serán unos, cuando pasamos al siguiente símbolo del texto desplazamos el cero (introduciendo un
nuevo 0) y hacemos un O lógico con la máscara del símbolo. Si el valor del nuevo estado contiene algún
cero continuamos desplazándonos y actualizando la máscara, si es todo unos salimos del acierto parcial.
El acierto se produce cuando encontramos un cero en la posición correspondiente al último caracter del
patón. Para recuperarnos del acierto eliminamos el ultimo cero del estado, si el valor es todo unos salimos
del acierto parcial y continuamos y si no seguimos en el búcle, actualizando el nuevo estado.
La ventaja fundamental de este algoritmo es su fácil extensión a búsquedas con clases de caracteres,
ya que basta con modificar el cálculo de la máscara para que ponga a 0 el bit correspondiente a la posición
de la clase en el patrón para las máscaras de todos los símbolos de la clase. Con este cambio no hay que
modificar el algoritmo de búsqueda para que se encuentre el acierto. De igual modo se soporta el uso de
símbolos don’t care, es decir, el uso de patrones para los que nos da igual que símbolo ocupe alguna de sus
posiciones. La modificación es sencilla, podemos considerar que la clase de caracteres es todo el alfabeto
(todas las máscaras contendrán un cero en la posición del símbolo que no nos importa).
En [Wu92] se presenta una versión de este algoritmo que permite la búsqueda aproximada o con
2.5. Shift Or 28
errores. El algoritmo se utiliza en el programa agrep, una herramienta de búsqueda aproximada para
Unix, que se ha incluido en el sistema de indexación y recuperación denominado glimpse ([Man93],
[glimpse]).
2.5.2. Costes
El coste temporal del preproceso es de Ω (alpha_size + m), alpha_size por la inicialización del vector
de máscaras a 1, y m para calcular la máscara de cada símbolo del patrón.
El coste temporal de la búsqueda es de Ω (n) y es independiente del tamaño del patrón y el alfabeto
[Bae92b].
El coste espacial es de Ω (alpha_size) que es el espacio empleado para almacenar la tabla de más-
caras.
2.5.3. Implementación
A continuación veremos el código en C del preproceso, supondremos definido un vector de enteros
del tamaño del alfabeto (mask) y una máscara que nos indicará cual es el último estado de la búsqueda
(last_state) y cual es el máximo número de estados (max_states).
preproceso_shift_or() {
int i, st; /* variables auxiliares */
for (i=0; i < alpha_size; i++)
mask[i] = ~0;
max_states = min(pat_size, sizeof(int) * 8);
st = 1;
for (i=0; i < max_states; i++) {
mask[ pat[i] ] &= ~st;
st <<= 1;
}
last_state = 1 << (max_states - 1);
}
La actualización de las máscaras se hace empleando una variable auxiliar que contiene un uno en la
posición correspondiente al símbolo visitado, como inicializamos las máscaras poniendo todos los bits a
uno, haciendo un Y lógico de la máscara con el valor de la variable auxiliar negada pondremos un cero
en la posición correspondiente, manteniendo cualquier otro cero que hubiera en la máscara.
El valor de max_states será el tamaño del patrón o el número de bits de cada máscara si el patrón
lo supera en tamaño.
El código en C de la búsqueda será:
shift_or() {
int ti, pi, tj; /* variables auxiliares */
ti = 0; /* índice en el texto */
while (ti < txt_size - pat_size){
if ( (mask[ txt[ti] ] & 1) == 0) { /* Acierto parcial */
state = ~0;
state = (state << 1) | (mask[txt[ti]]);
while (state != ~0) {
ti++;
if ((state & last_state) == 0) {
pi = max_states; /* índice acierto parcial en patrón */
tj = ti; /* índice acierto parcial en texto */
/* búsqueda lineal si el patrón es mayor que la máscara */
2.5. Shift Or 29
Como no sabemos cual es el tamaño del patrón antes de comenzar a buscar en la detección de los aciertos
comprobamos que es menor que max_states, en caso de no serlo terminamos la búsqueda por fuerza
bruta. Para los patrones de tamaño mayor al número de bits de la palabra del computador la recuperación
retrocede en el texto, continuando a partir del último símbolo analizado por el método normal (usando
el estado). El problema se podría evitar, pero la solución complicaría el algoritmo y en la práctica es
raro encontrar patrones más largos de 32 caracteres, que es el tamaño de los enteros en la mayoría de
computadores modernos.
En la versión real la condición de entrada en el acierto parcial compara el símbolo actual del texto
con el primer caracter del patrón, ya que no precisa acceder al vector de máscaras (es más rápido). Esto
implica que si queremos extender el algoritmo para que soporte clases de caracteres debemos modificar
la implementación, ya que si ponemos la clase en la primera posición del patrón no se puede comparar
un solo símbolo.
Como en otros algoritmos, la búsqueda se detiene cuando ya no se puede encontrar un acierto y
accedemos al texto a través de punteros, aunque para la tabla de máscaras necesitamos usar el vector, ya
que el acceso no es secuencial.
2.6. Boyer-Moore
2.6.1. Descripción
Este es el algoritmo más rápido para la búsqueda de un solo patrón en la teoría y la práctica. Se
presentó por primera vez en [BM77].
Funciona recorriendo el texto de izquierda a derecha, pero comparando el patrón con el texto de
derecha a izquierda. El aumento de velocidad se consigue saltando caracteres del texto que no pueden dar
origen a un acierto. Para saltar empleamos dos tablas auxiliares que denominaremos tabla de salto y tabla
de desplazamiento.
Supongamos que nos encontramos al principio de la búsqueda, con el patrón alineado con el inicio
del texto. Según lo comentado antes, lo primero que haremos será comparar patm − 1 con txtm − 1. Si sus
valores no coinciden y el caracter txtm − 1 no aparece en el patrón, podremos desplazarnos sobre el texto m
posiciones (alineando pat0 con txtm), ya que es seguro que no encontraremos ningun acierto que comience
antes de txtm (cualquier acierto que comenzara en una posición anterior a la m fallaría al comparar un
2.6. Boyer-Moore 30
caracter del patrón con txtm − 1). En este caso habremos dejado de comparar m − 1 símbolos del patrón y
la siguiente comparación será entre patm − 1 y txt2m − 1.
Siguiendo con la técnica anterior llegamos al caso en el que el patrón está alineado con la posición j
− (m − 1) del texto, y al comparar patm − 1 con txtj los valores no coinciden pero el símbolo del texto aparece
en otra posición del patrón. En este caso no podremos saltar m posiciones, pero si sabremos que, en caso
de haber un acierto, el símbolo actual del texto deberá estar alineado con la posición del patrón en la que
aparece. Si esa posición es m − 1 − k alinearemos patm − 1− k con txtj y continuaremos comparando patm − 1
con txtj + k, saltando k posiciones. Hay que señalar que si el símbolo aparece más de una vez en el patrón
deberemos elegir el índice de la ocurrencia más a la derecha, ya que si no lo hacemos así podemos perder
aciertos. Por ejemplo, si buscámos la palabra texto en la frase «Un pretexto absurdo» llegamos a
la situación:
texto
Un pretexto absurdo
-
(desplazamiento para k=1)
texto
Un pretexto absurdo
-
(desplazamiento para k=4)
texto
Un pretexto absurdo
-
En este caso debemos desplazar el patrón una posición a la derecha, (txtj ≡ pat4 − 1 ≡ ′t′, luego k = 1), si
escogemos k = 4 perderemos el acierto.
En el caso de que el símbolo actual en el texto coincidiera con el último carácter del patrón (txtj ≡
patm − 1), seguimos comparando los símbolos precedentes del texto y el patrón hasta encontrar un acierto
completo o hasta que se produce un fallo. Si el fallo se produce en la posición i − 1 del patrón sabremos
que pati…m − 1 ≡ txtj − (m − 1)+ i…j, es decir, que los últimos (m − 1) − i caractes del patrón y el texto coinciden.
En esta situación, si la ocurrencia más a la derecha de txtj − m + i en el patrón es patg con g < i − 1 (antes del
fallo) podemos desplazar el patrón g posiciones a la derecha para alinear patg con txtj − m + i, continuando
con la comparación de patm − 1 con txtj − (m − 1)+ i + g. Si g > i − 1 (la ocurrencia está despues del fallo), no
ganamos nada alineando, ya que eso implicaría retroceder en el texto a posiciones ya estudiadas, por lo que
desplazamos el patrón sobre el texto una posición (lo alinearemos con j + 1) y continuaremos comparando
patm − 1 con txtj + 1, ya que sabemos que en la posición actual no hay acierto, pero no podemos decir nada
de la siguiente.
Hasta aquí hemos visto la denominada heurística de salto, para implementarla preprocesaremos el
patrón asignando a cada símbolo del alfabeto el máximo salto que podemos dar cuando al encontrarlo se
produce un fallo. Para todos los símbolos del alfabeto que no se encuentren en el patrón el salto será m, y
para los símbolos que sí aparezcan en él el salto máximo será la distancia desde su última aparición en el
patrón hasta el final del mismo.
La heurística de desplazamiento surge al estudiar el patrón de un modo similar al del algoritmo de
Knuth, Morris y Pratt, al darnos cuenta de que cuando se produce un fallo trás un acierto parcial es posible
avanzar más posiciones que con la tabla de saltos, teniendo en cuenta que despues del desplazamiento el
patrón debe coincidir con los símbolos previamente comparados y que el símbolo alineado con la posición
del texto que causó el fallo debe ser distinto al símbolo con el que se comparó antes.
Cuando pati…m − 1 ≡ txtj − (m − 1)+ i…j y pati − 1 ≠ txtj − m + i si pati…m − 1 aparece como la subcadena
pati − g…(m − 1)− g en el patrón y pati − g ≠ pati con la g < i de mayor valor (es decir, si hay más de una aparición
de la subcadena, tomamos la que se encuentra más a la derecha), podemos avanzar más en el texto que
2.6. Boyer-Moore 31
con la heurística de saltos alineando pati − g…(m − 1)− g con txtj − (m − 1)+ i…j y continuar comparando patm − 1 con
txtj + g.
Para utilizar está heurística calculamos una tabla del tamaño del patrón que contiene para cada
posición del mismo el desplazamiento g descrito antes más un desplazamiento adicional para apuntar
a la última posición del patrón una vez desplazado. La definición formal de las entradas de la tabla
shift será:
shifti = min(g + m − 1 − i : g ≥ 1′′∧′′(g ≥ i′∨′pati − g ≠ pati )′′∧′′((g ≥ k′∨′patk − g ≡ patk )′′para′′j < k <
m))
En la ejecución del algoritmo usaremos ambas heurísticas, seleccionando en cada fallo el valor que nos
permita saltar más en el texto. Así, en cada paso compararemos el último elemento del patrón con el sím-
bolo del texto correspondiente, si no son iguales incrementaremos nuestra posición en el texto sumándole
el valor max(skiptxt , shiftm − 1 ) y si son iguales continuaremos mirando los anteriores hasta encontrar el
j
patrón completo o detenernos en un fallo, en cuyo caso haremos lo mismo que antes (sumaremos el valor
max(skiptxt , shifti ) a la posición actual en el texto, siendo i la posición actual en el patrón).
j
Hay que señalar que la implementación propuesta asume que siempre usamos el mismo iterador para
acceder a los elementos del texto y siempre apunta al símbolo a comparar, esta es la razón de que en el
cálculo de shift se sume un desplazamiento adicional para alcanzar el final del patrón.
Veamos un ejemplo:
[ ... ]
PREPROCESSOR STRUCTURES
0 1 2 3 4 5 : i
c o c o l o : pat[i]
11 10 9 8 3 1 : shift [i]
skip [’c’] : 3
skip [’l’] : 1
skip [’o’] : 0
skip [ * ] : 6
SEARCHING ...
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
2.6. Boyer-Moore 32
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
-- PARTIAL MATCH --
cocolo
Vamos a beber un cocoloco bajo el baobab.
-*
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
-- PARTIAL MATCH --
cocolo
Vamos a beber un cocoloco bajo el baobab.
-*
cocolo
Vamos a beber un cocoloco bajo el baobab.
-**
cocolo
Vamos a beber un cocoloco bajo el baobab.
-***
cocolo
Vamos a beber un cocoloco bajo el baobab.
-****
cocolo
Vamos a beber un cocoloco bajo el baobab.
-*****
cocolo
Vamos a beber un cocoloco bajo el baobab.
******
-- FULL MATCH --
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
Number of matches: 1
Match positions: 17
En este ejemplo el algoritmo ejecuta 16 comparaciones para buscar un patrón de longitud 6 en un texto de
42 caracteres. Para este mismo caso el algoritmo de la fuerza bruta hubiera necesitado 48 comparaciones
y el de Knuth-Morris-Pratt 38. Como se ve, este algoritmo ejecuta muchas menos comparaciones que
los otros.
La recuperación tras un acierto es sencilla, basta con incrementar el valor del iterador del texto como
si el primer carácter del patrón no hubiera sido acertado, por la definición de las tablas sabemos que el
acierto no volverá a ser encontrado (al menos nos desplazará al final del patrón más uno).
Este algoritmo ha sido muy estudiado y existen multitud de variantes: unas utilizan una versión
distinta de la tabla de desplazamiento otras la eliminan por completo e incluso alteran el orden de
comparación de los símbolos del patrón con el texto. En los proximos apartados veremos dos variantes de
este algoritmo, para ver la descripción de algunos más se puede consultar [Ste92].
Utilizando técnicas similares a las de este algoritmo se ha desarrollado una versión para búsqueda
de múltiples patrones ([Com79]).
En [Wat92] y [Wat95] se presenta una taxonomía de algoritmos de búsqueda de uno o multiples
patrones ordenada por los detalles que introduce cada versión y que por tanto incluye las variantes del
Boyer-Moore y Commenz-Walter.
2.6.2. Costes
El coste temporal del preproceso es de Ω (alpha_size + m) para la tabla de saltos (alpha_size para
inicializar a m y m para el cálculo del salto para cada símbolo del patrón) y de Ω (m) para el cálculo de
la tabla de desplazamientos. El coste total del preproceso será de Ω (alpha_size + m).
El coste temporal de la búsqueda en el peor caso es de Ω (m+n) para la búsqueda de una sola
ocurrencia (Ω (m+rn) si buscamos todas las ocurrencias y el patrón aparece r veces en el texto) y en el
mejor caso (si ningún símbolo del patrón aparece en el texto), el coste será de Ω (n/m).
El coste espacial es de Ω (alpha_size+m) que es el tamaño de las tablas de salto y siguientes respec-
tivamente.
2.6.3. Implementación
Presentamos a continuación el código en C del preproceso, supondremos definidas las tablas skip
y shift del tamaño del alfabeto y el patrón respectivamente.
Para el cálculo de shift emplearemos una tabla auxiliar f, que se define como f m − 1 = m + 1 y f j − 1
= min(i : j < i < m∧pati…m − 1 = patj…j + m − 1− i )) para 0 < j < m.
preproceso_boyer_moore () {
int i, j, k; /* variables auxiliares */
int f[pat_size];
/* cálculo de skip */
for (i=0; i < alpha_size; i++)
skip[i] = pat_size;
for (i=1; i < pat_size; i++)
skip [ pat[i-1] ] = pat_size - i;
/* cálculo de shift */
for (i=1; i <= pat_size; i++) /* valores máximos de shift */
shift[i-1] += pat_size - i;
/* */
2.6. Boyer-Moore 34
k = pat_size + 1;
for (j=pat_size; j > 0; j--) {
f[j-1] = k;
while (k <= pat_size && pat[j-1] != pat[k-1]) {
shift[k-1] = min ( shift[k-1], pat_size - j);
k = f[k-1];
}
k--;
}
/* */
for (i=1; i<=k; i++) {
shift[i-1] = min( shift[i-1], pat_size + k - i );
}
/* Corrección de Rytter planteada por Mehlhorn */
j = f[k-1];
while(k < pat_size){
while (k <= j) {
shift[k-1] = min( shift[k -1], pat_size + j - k);
k++;
}
j = f[j-1];
}
}
boyer_moore() {
int ti, pi; /* variables auxiliares */
ti = pat_size - 1; /* índice en el texto */
while (ti < txt_size){
if (txt[ti] = pat[pat_size-1]) { /* Acierto parcial */
pi = pat_size - 1; /* índice acierto parcial en patrón */
do {
if (pi == 0){
acierto_en (ti);
break; /* continuamos en ti + max(...) */
/* (ti se incrementa al salir del bucle) */
}
ti--; pi--;
} while (txt[ti] == pat[pi]);
ti += max(skip[txt[ti]], shift[pi]);
} else { /* Fin Acierto Parcial */
ti += max(skip[txt[ti]], shift[pat_size-1]);
}
} /* Fin bucle while */
}
Al igual que para otros algoritmos presentados antes, la versión real emplea optimizaciones triviales
como el almacenamiento del valor del último símbolo del patrón y su valor asociado en la tabla shift
en registros, además de usar punteros en lugar de índices para acceder al texto y al patrón.
2.7.1. Descripción
Este algoritmo es una versión simplificada del de Boyer y Moore debida a Horspool [Hor80] que
2.7. Boyer Moore Horspool 35
elimina el uso de la tabla de desplazamiento, ya que esta tabla sólo mejora la velocidad cuando buscamos
patrones muy repetitivos, que en la práctica no suelen aparecer.
Esta versión reduce el coste espacial (sólo necesitamos una tabla del tamaño del alfabeto), el
problema es que ahora el peor caso tiene un coste temporal de Ω (mn), aunque en situaciones normales
tendrá un comportamiento similar al del algoritmo original.
Además, se modifica el cálculo de la tabla de salto no asignando valor al símbolo patm − 1 (que en el
caso del algoritmo de Boyer-Moore era siempre 0, ya que cuando encontrabamos un fallo en ese símbolo
seguíamos saltando gracias a la tabla de desplazamiento), de manera que skippat valdrá m si patm − 1 es
m−1
la única ocurrencia del símbolo en el patrón o menos si hay alguna ocurrencia en una posición anterior.
Por último, si se produce un acierto parcial y no se encuentra el patrón, usando el desplazamiento
asignado al símbolo del texto que provoca el fallo podemos encontrarnos con que el nuevo alineamiento
deje alguno de los símbolos ya comparados alineado con su ocurrencia más a la derecha en el patrón, lo
que implicaría un retroceso en el patrón que debe ser detectado, ya que puede causar recursión y detener la
búsqueda. Para evitarlo siempre saltamos a partir del símbolo del texto que inició el acierto parcial, usando
su valor en la tabla de salto. Por ejemplo, si buscamos el patrón cero en la cadena «es un letrero
enorme» nos encontraremos con la siguiente secuencia:
PREPROCESSING ...
skip [ * ] = 4
cero
-
skip [’c’] = 3
cero
-
skip [’e’] = 2
cero
-
skip [’r’] = 1
PREPROCESSOR STRUCTURES
skip [’c’] : 3
skip [’e’] : 2
skip [’r’] : 1
skip [ * ] : 4
SEARCHING ...
cero
Es un letrero enorme.
-
skip [’u’] = 4
cero
Es un letrero enorme.
-
2.7. Boyer Moore Horspool 36
skip [’e’] = 2
cero
Es un letrero enorme.
-
skip [’r’] = 1
cero
Es un letrero enorme.
-
skip [’e’] = 2
cero
Es un letrero enorme.
-
-- PARTIAL MATCH --
cero
Es un letrero enorme.
-*
cero
Es un letrero enorme.
-**
cero
Es un letrero enorme.
-***
Llegados a este punto, se produce un fallo en la primera r de letrero, si usamos el desplazamiento dado
por skip[’r’] llegamos a un búcle infinito:
skip [’r’] = 1
cero
Es un letrero enorme.
-
skip [’e’] = 2
cero
Es un letrero enorme.
-
-- PARTIAL MATCH --
cero
Es un letrero enorme.
-*
cero
Es un letrero enorme.
-**
cero
Es un letrero enorme.
-***
skip [’r’] = 1
2.7. Boyer Moore Horspool 37
skip [’o’] = 4
cero
Es un letrero enorme.
-
-- PARTIAL MATCH --
cero
Es un letrero enorme.
-*
skip [’o’] = 4
cero
Es un letrero enorme.
-
skip [’.’] = 4
Number of matches: 0
[ ... ]
PREPROCESSOR STRUCTURES
skip [’c’] : 3
skip [’l’] : 1
skip [’o’] : 2
skip [ * ] : 6
SEARCHING ...
[ ... ]
cocolo
Vamos a beber un cocoloco bajo el baobab.
******
-- FULL MATCH --
skip [’o’] = 2
2.7. Boyer Moore Horspool 38
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
-- PARTIAL MATCH --
cocolo
Vamos a beber un cocoloco bajo el baobab.
-*
skip [’o’] = 2
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
[ ... ]
Number of matches: 1
Match positions: 17
Al saltar a partir del valor de skip[’o’] sólo saltamos 2 posiciones a la derecha, mientras que usando
la tabla de desplazamiento conseguíamos un salto de 6 posiciones hacia la derecha (11 desde el primer
símbolo del patrón).
2.7.2. Costes
El coste temporal del preproceso es de Ω (alpha_size + m) para la tabla de saltos (alpha_size para
inicializar a m y m para el cálculo del salto para cada símbolo del patrón).
El coste temporal de la búsqueda en el peor caso es de Ω (mn) y en el mejor caso (si ningún símbolo
del patrón aparece en el texto) de Ω (n/m). El coste en promedio es similar al del algoritmo de Boyer-Moore
para textos normales, aunque ahora el peor caso sea de orden Ω (mn).
El coste espacial es de Ω (alpha_size) que es el tamaño de la tabla de salto.
2.7.3. Implementación
El código en C del preproceso es sencillo, sólo asume que tenemos declarado el vector skip del
tamaño del alfabeto:
preproceso_boyer_moore_hoorspool () {
int i; /* variables auxiliares */
for (i=0; i < alpha_size; i++)
skip[i] = pat_size;
for (i=1; i < pat_size; i++)
skip[ pat[i-1] ] = pat_size - i;
}
El código en C de la búsqueda es muy similar al del algoritmo de Boyer-Moore, salvo que aquí no
empleamos la función máximo y utilizamos un índice auxiliar en los aciertos parciales.
boyer_moore_hoorspool() {
int ti, pi, tj; /* variables auxiliares */
ti = pat_size - 1; /* índice en el texto */
while (ti < txt_size){
2.7. Boyer Moore Horspool 39
La versión real del algoritmo utiliza punteros para acceder al patrón y al texto y almacena en registro el
valor del último símbolo del patrón.
2.8.1. Descripción
Este algoritmo es otra versión simplificada del de Boyer y Moore que elimina el uso de la tabla de
desplazamiento (igual que en el Boyer-Moore-Horspool) y emplea una técnica diferente para saltar en el
texto. Esta versión se debe a Sunday [Sun90].
La idea básica es que cuando se produce un fallo y patm − 1 está alineado con el símbolo txti, en lugar
de saltar a partir del valor de esté último podemos usar el del siguiente símbolo del texto (txti + 1), ya que,
para el mínimo desplazamiento (una posición a la derecha), txti + 1 forma parte de la nueva subcadena a
examinar. De este modo, los desplazamientos de la tabla skip son iguales a los de la tabla del algoritmo
original más uno en promedio.
Además, el algoritmo hace uso del hecho de que las comparaciones entre el patrón y la subcadena del
texto alineada con él pueden hacerse en cualquier orden, lo que nos permite elegir a partir de qué símbolo
queremos comenzar las comparaciones. Esta versión compara comenzando a partir del primer símbolo
del patrón, continuando hacia la derecha. Las modificaciones del algoritmo son sencillas, en principio
inicializamos los valores de la tabla de salto a partir de m + 1 para incorporar el uso del símbolo siguiente
para indexar los saltos y además si estamos en la posición i del texto utilizamos el símbolo i + m actualizar
el índice.
Sunday propone dos versiones más, una que ordena de mayor a menor los símbolos en función de
la longitud del salto que proporcionan (Maximal Shift) y otra que utiliza una estadística de la frecuencia
de aparición de los símbolos en el texto para comparar primero los de menor frecuencia (Optimal
Mismatch).
Veamos un ejemplo:
PREPROCESSING ...
2.8. Sunday Quick Search 40
skip [ * ] = 7
cocolo
-
skip [’c’] = 6
cocolo
-
skip [’o’] = 5
cocolo
-
skip [’c’] = 4
cocolo
-
skip [’o’] = 3
cocolo
-
skip [’l’] = 2
cocolo
-
skip [’o’] = 1
PREPROCESSOR STRUCTURES
skip [’c’] : 4
skip [’l’] : 2
skip [’o’] : 1
skip [ * ] : 7
SEARCHING ...
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’a’] = 7
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’ ’] = 7
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’o’] = 1
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’l’] = 2
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
-- PARTIAL MATCH --
2.8. Sunday Quick Search 41
cocolo
Vamos a beber un cocoloco bajo el baobab.
*-
cocolo
Vamos a beber un cocoloco bajo el baobab.
**-
cocolo
Vamos a beber un cocoloco bajo el baobab.
***-
cocolo
Vamos a beber un cocoloco bajo el baobab.
****-
cocolo
Vamos a beber un cocoloco bajo el baobab.
*****-
cocolo
Vamos a beber un cocoloco bajo el baobab.
******
-- FULL MATCH --
skip [’c’] = 4
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’a’] = 7
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’b’] = 7
cocolo
Vamos a beber un cocoloco bajo el baobab.
-
skip [’
’] = 7
Number of matches: 1
Match positions: 17
2.8.2. Costes
El coste temporal del preproceso es de Ω (alpha_size + m) para la tabla de saltos (alpha_size para
inicializar a m y m para el cálculo del salto para cada símbolo del patrón).
El coste temporal de la búsqueda en el peor caso es de Ω (mn) y en el mejor caso (si ningún sím-
bolo del patrón aparece en el texto) de Ω (n/m). En promedio es similar al Boyer-Moore y al Boyer-
Moore-Horspool, aunque su rendimiento es algo mejor con patrones pequeños.
2.8. Sunday Quick Search 42
2.8.3. Implementación
El código en C del preproceso es sencillo, sólo asume que tenemos declarado el vector skip del
tamaño del alfabeto:
preproceso_sunday_quick_search () {
int i; /* variables auxiliares */
for (i=0; i < alpha_size; i++)
skip[i] = pat_size + 1;
for (i=0; i < pat_size; i++)
skip[ pat[i] ] = pat_size - i;
}
El código en C de la búsqueda será muy parecido al del algoritmo Boyer-Moore-Horspool, salvo que en
este caso incrementamos los iteradores en un acierto parcial y el incremento de ti usa el caracter que está
justo despues del patrón en el texto:
sunday_quick_search() {
int ti, pi, tj; /* variables auxiliares */
ti = 0;
while (ti < txt_size){
if (txt[ti] = pat[0]) { /* Acierto parcial */
pi = 0; /* índice acierto parcial en patrón */
tj = ti; /* índice acierto parcial en texto */
do {
tj++; pi++;
if (pi == pat_size){
acierto_en (ti);
break; /* continuamos en ti + skip[txt[ti + pat_size]] */
/* (ti se incrementa al salir del bucle) */
}
La versión real del algoritmo utiliza punteros para acceder al patrón y al texto y almacena en registro el
valor del primer símbolo del patrón.
Capítulo 3. Descripción del código
En este capítulo comentaremos como se implementan y que función tienen las clases que integran la
biblioteca. Comenzaremos con una visión rápida que nos dará una idea de cual es la estructura de la
biblioteca y las relaciones entre las clases, para pasar despues a una descripción más detallada de las clases
básicas de la biblioteca, las clases auxiliares y de ayuda al análisis y implementación de los algoritmos de
búsqueda de un solo patrón.
El último apartado del capítulo lo dedicaremos a comentar el programa de análisis de los algoritmos,
centrandonos más en su funcionalidad que en la codificación.
Algoritmo
Clase para dar una base común a distintos tipos de algoritmos.
Unidad (token)
Clase que se emplea para definir una unidad con algún significado dentro de una cadena: palabra,
frase, etc. Se incorporan dos versiones, una que define los elementos por los símbolos válidos dentro
de la unidad y otra por los que pueden aparecer antes y después de ella.
Cronómetro (stopwatch)
Clase empleada para medir lapsos de tiempo.
43
3.1. Estructura de la biblioteca 44
3.2.1. Símbolo
Como ya hemos mencionado en la introducción, no existe un tipo símbolo definido por la biblioteca,
ya que la idea es que pueda ser usada con distintos tipos de datos; un símbolo sería un valor de un tipo
cualquiera. Además, no necesitamos darle ningún atributo o propiedad, eso se hace al definir un alfabeto
y emplearlo junto a los símbolos, por lo que no vale la pena encapsular el tipo paramétrico en una clase.
3.2.2. Alfabeto
El alfabeto es un conjunto ordenado de símbolos. La clase se define como template, proporcionan-
do una especialización para el tipo char. Se definen métodos para comprobar si un símbolo pertenece o
no al alfabeto, conocer su tamaño del alfabeto o si está vacío y obtener el índice de un símbolo dentro del
conjunto o el símbolo correspondiente a un índice.
El alfabeto es estático, una vez definido no se puede modificar. Se construye pasándole un vector de
símbolos del que se extraen los posibles valores eliminando duplicados.
Existe un constructor por defecto que se emplea para inicializar el alfabeto. La versión genérica
genera un alfabeto vacío que no sirve más que para poder declarar variables sin inicializar (como por
ejemplo dentro de una clase). La versión especializada para caracteres inicializa las tablas como alfabeto
identidad en lugar de como alfabeto vacío.
Los métodos definidos son:
• index(symbol). Retorna el índice del símbolo, si no pertenece al alfabeto retorna el tamaño
del mismo.
• value(index). Retorna el valor asociado al índice. El valor de un índice mayor que el tamaño del
alfabeto no está definido.
• size(). Retorna el tamaño del alfabeto.
Para representar el conjunto se emplean dos estructuras:un vector de símbolos y otro de enteros (indexado
por símbolos e implementado usando el tipo map de la STL). El primer vector se utiliza para acceder a los
símbolos correspondientes a cada índice en el alfabeto (con coste de orden 1) y el segundo para obtener
el índice de cada símbolo (con coste de orden logarítmico respecto al tamaño del alfabeto).
La especialización para caracteres usa un vector en lugar de un mapa para asociar símbolos e índices
(permitiendo acceder a estos últimos con un coste de orden 1). Dependiendo del compilador el acceso a
los índices causa problemas: si el tipo usado para representar caracteres es con signo no podemos usarlos
para acceder al vector (los caracteres por encima del 127 se convierten en enteros negativos, al menos en
el gcc). La versión implementada lo soluciona convirtiendo los símbolos a caracteres sin signo antes de
usarlos como índices del vector.
3.2.4. Texto
Se define como una cadena de símbolos. Si la biblioteca fuese adecuada para implementar programas
de edición de texto se podría considerar el texto como un vector de cadenas. De todos modos, tal y como
está ahora, se podrían emplear los mismos algoritmos llamándolos para cada cadena del vector.
La biblioteca asume que el texto está en memoria, si se desarrolla algún programa que necesite
buscar en el texto desde un fichero y no se puede mantener completo en memoria basta con hacer una
composición similar al caso del vector de cadenas, dividiendo el texto en bloques. Después de acceder al
primer bloque cargaríamos los últimos caracteres del primero (tantos como el tamaño del patrón menos
uno) seguidos del segundo bloque, de este modo no perderíamos un acierto que estuviera en la frontera
entre dos bloques. De hecho, con este método no repetimos operaciones, ya que todos los algoritmos
terminan cuando ya no pueden encontrar un acierto; cuando quedan menos caracteres que el tamaño del
patrón dejamos de buscar.
3.2.5. Algoritmos
Clase de la que se derivan los distintos tipos de algoritmos. Aunque podría incluir más funciones, sólo
obliga a que las clases derivadas definan métodos para darle un identificador a cada algoritmo e indicar su
nombre y su tipo. El identificador es útil para la selección de los algoritmos (se puede usar para comparar
con la entrada del usuario en un menú), mientras que el tipo y el nombre son útiles para la salida de los
algoritmos o para saber que algoritmo estamos usando cuando accedemos a través de un puntero a una
clase base (podemos identificar el tipo de datos en tiempo de ejecución)
La clase podría incluir algunos de los métodos y atributos que ahora se incluyen en la clase que
representa los algoritmos de búsqueda de un solo patrón, como el soporte para la contabilidad de pasos o
el análisis gráfico (aunque este último debería ser modificado).
• Si el patrón es eliminado continuamos desde la posición en la que se encontró el acierto. Por ejemplo,
si buscamos el patrón bab en el texto bababoom encontramos [*bab*]aboom y después de la
eliminación continuaríamos con [a]boom. En este caso no habríamos eliminado el segundo bab, ya
que la primera eliminación hace desaparecer la b. Este comportamiento puede ser el correcto o no,
dependiendo del tipo de aplicación.
• Si se añade un nuevo patrón como prefijo continuaremos desde la antigua posición de acierto sumán-
dole el tamaño del nuevo patrón. Ejemplo: siguiendo con el patrón, texto y nuevo patrón anteriores,
encontramos [*bab*]aboom y después de la inserción continuamos con exb[a]baboom y si se-
guimos llegamos a exba[*bab*]oom, que despues de insertar nos deja con exbaexb[a]boom. Este
es el único caso que no causa problemas.
• Si se añade un nuevo patrón como sufijo continuamos a partir de la antigua posición de acierto más el
tamaño del patrón buscado mas el tamaño del nuevo patrón. Ejemplo: encontramos [*bab*]aboom
y después de la inserción continuamos con babex[a]boom. Como se puede observar, esta inserción
también pierde aciertos (de nuevo se pierde el segundo bab) y la única solución es insertar los sufijos
despues de haber encontrado todos los aciertos.
Por último indicaremos que junto a las funciones de acierto incluidas en la biblioteca se definen funciones
de adaptación que toman como parámetros el algoritmo de búsqueda, el patrón y el texto y retornan la
función de acierto. Su utilidad está en que indican claramente que hace la función de acierto, pero no son
realmente necesarias.
3.3.2. Unidad
Clase que sirve para definir una unidad con algún significado dentro de una cadena:palabra, frase, etc.
Se definen tres clases, una clase base abstracta (atoken) y dos clases derivadas (actoken y adtoken).
La clase base define operaciones para encontrar el principio o el final de una unidad a partir de
un punto (o ambos) y para verificar si un rango del texto es una unidad. Las operaciones se definen en
función de métodos que indican si un símbolo es un delimitador, separando entre delimitadores anteriores
y posteriores. La detección de delimitadores se implementa en las clases derivadas.
La clase actoken define las unidades por su contenido, es decir, un rango es una unidad si todos sus
símbolos pertenecen al conjunto de símbolos válidos. Ese conjunto se define al declarar variables (en el
constructor), y no puede ser modificado.
3.3. Clases auxiliares y de soporte para el análisis 48
La clase adtoken utiliza delimitadores para detectar las unidades, empleando dos conjuntos de
símbolos, los que podemos encontrar antes y despues del elemento. Los conjuntos se definen al construir
un objeto de la clase y no se pueden modificar.
La determinación de si un rango es o no una unidad se hace comprobando únicamente que los
símbolos contenidos en él no son delimitadores, si deseamos verificar que está bien delimitado deberemos
invocar el método is_predelim() para el símbolo que precede al rango y el método is_postdelim()
para el que está inmediatamente despues de él.
El empleo de dos métodos de definición de unidades permite mayor flexibilidad a la hora de
utilizarlos, ya que ambos métodos no son simétricos, aunque las diferencias son sutiles. Por ejemplo,
la versión que emplea delimitadores anteriores y posteriores podría obtener, a partir de un símbolo, una
unidad que contuviera símbolos delimitadores finales que no pertenecen al conjunto de delimitadores
iniciales y viceversa, ya que al buscar el principio de una unidad sólo miramos que los símbolos no sean
delimitadores iniciales y al buscar el final que no sean delimitadores finales. Si los conjuntos de inicio y
final son iguales ambas clases son simétricas (siempre que el conjunto de símbolos válidos sea el de todos
los símbolos que no son delimitadores).
1
También se definió una versión especial para Windows, pero como no se ha probado ha sido eliminada.
3.3. Clases auxiliares y de soporte para el análisis 49
anterior, cada vez que nos desplacemos en el texto habremos ejecutado un paso del algoritmo. El ejemplo
anterior sugería que lo único que se modifica en cada paso es la posición en el texto (y quizás alguna
variable relacionada con él), si además modificamos nuestra posición en el patrón o alguna condición
nos indica que el estado actual tiene una propiedad especial (como por ejemplo que hemos encontrado un
acierto) también habremos ejecutado un paso, pero de un tipo distinto.
Esta definición de paso es útil para la representación gráfica de los algoritmos y para relacionar su
ejecución real con el coste temporal teórico. Para utilizar los pasos definimos puntos de ruptura dentro
del código, que no son más que llamadas a una función que toma como parámetro el tipo de paso que
hemos completado. La idea de los puntos de ruptura es similar a la empleada en los depuradores de
programas (debuggers) disponibles en la mayoría de entornos de programación. La diferencia está en que
los depuradores nos permiten definir los puntos mientras se ejecuta nuestro programa y nuestro modelo
los define dentro del código, en tiempo de compilación.
En cuanto a la relación entre la ejecución real y el coste temporal teórico tenemos que señalar que,
si contamos el número de veces que se atraviesa un punto de ruptura durante una ejecución del algoritmo,
estamos calculando el número de operaciones de modo similar a lo que se hace para obtener el coste
asintótico en función del tamaño de los valores de entrada. De hecho, los resultados nos pueden ayudar
a verificar si los cálculos asintóticos se corresponden con los resultados reales, aunque siempre pensando
que trabajamos con una entrada real y por tanto no consideramos el cálculo en el caso promedio, sólo el
de la entrada actual. Para ver el comportamiento en el caso promedio deberemos diseñar un conjunto de
casos de prueba y obtener estadísticas a partir de los resultados obtenidos empíricamente. Para el peor y
el mejor caso bastará ejecutar los algoritmos con la entrada adecuada.
Además, los distintos puntos de ruptura suelen corresponder con los distintos bloques estudiados para
obtener los costes teóricos. Por ejemplo, en los algoritmos de búsqueda suele haber un coste directamente
relacionado con el recorrido del texto y otro con las operaciones realizadas en caso de un acierto parcial.
En general el cálculo asintótico del primer coste se corresponde con el número de veces que pasamos por
el punto de ruptura que nos indica una actualización de nuestra posición en el texto y el segundo con el
número de pasos que ejecutamos comparando distintas posiciones del patrón con el texto manteniendo la
posición relativa del primero con el segundo, aunque esto no siempre sea así.
Por otro lado, el calculo del número de pasos nos sirve para valorar la relación entre coste asintótico
y coste real, ya que no todos los pasos tienen el mismo coste en ciclos de procesador (en realidad al
hablar de coste de orden n estamos obviando una constante que nos indique el coste de las operaciones
en cada paso). Por ejemplo, en el caso de los algoritmos de búsqueda, los costes en número de pasos del
algoritmo de fuerza bruta suelen ser superiores a los del algoritmo de Karp-Rabin (aunque son del mismo
orden), pero en la práctica el primero es mucho más rápido que el segundo, ya que sus pasos sólo incluyen
comparaciones de caracteres e incrementos de variables, mientras que el segundo realiza en cada paso
varias operaciones aritméticas que necesitan ciclos extra de CPU.
Para contabilizar los pasos que se ejecutan en cada algoritmo se define una clase (stepacct) que
almacena un vector de enteros que representan el número de pasos a través de cada punto de ruptura. El
número y nombre de los puntos de ruptura se pasan como parámetros en el constructor.
Se definen métodos para acceder, borrar e incrementar los valores de cada punto de ruptura individual
y para poner a cero todos los contadores e imprimirlos (indicando sus nombres).
Se emplea desde skm_algo para llevar la cuenta de pasos. En realidad la función que cumple esta
clase podría haberse integrado en skm_algo, pero implementándola de manera independiente podemos
reutilizarla si definimos otro tipo de algoritmos.
Es responsabilidad de la implementación de cada algoritmo definir que llamadas al dispositivo de
análisis se deben realizar al detenerse en un punto de ruptura concreto, asi como la inserción de esos
mismos puntos en las posiciones adecuadas dentro del código del algoritmo.
3.3. Clases auxiliares y de soporte para el análisis 50
análisis, se encarga de llamar a la función que recibe entrada del usuario despues de ejecutar la función
del algoritmo que le indica al dispositivo los datos que debe mostrar. Con el retorno de esta función se
actualiza el valor de la variable interna stop que los algoritmos utilizan para saber si deben continuar
buscando despues de ejecutar cada paso del algoritmo.
• preprocess(), preproceso del patrón, accede al patrón a través de las variables internas y calcula
los valores de las estructuras de preproceso. No utiliza el texto, si hay operaciones de preproceso
que utilizan el texto se hacen dentro del método de búsqueda. Si el algoritmo no tiene preproceso se
define vacía.
• run_preprocessor(), es la misma función de preproceso con puntos de ruptura.
• match(), función de acierto, busca el patrón en el texto. Asume que todos los valores que dependen
de la entrada y el preproceso están inicializados. Cuando encuentra un acierto llama al método
report_match y en función de su retorno reinicia la búsqueda o termina.
algoritmo se reinicia totalmente o se hace uso de los resultados anteriores. Los sistemas de recuperación
se comentan en la descripción de cada algoritmo.
Como ya se ha dicho, los métodos de búsqueda emplean punteros para el acceso al texto y al patrón,
evitando el uso de índices siempre que es posible, lo que nos ahorra una suma para cada acceso a los
elementos de un vector.
En todas las implementaciones se aplican optimizaciones triviales, evitando depender de la
inteligencia del compilador. Fundamentalmente consisten en el uso de variables locales (a ser posible
almacenadas en registro) para el acceso a elementos de vectores que son utilizados varias veces, como por
ejemplo el primer o último carácter del patrón.
Hay que señalar que, para los métodos de preproceso y búsqueda que no incorporan análisis
(preprocessor() y matcher()), se redeclaran algunos de los atributos de la clase como variables
locales en registro (como por ejemplo el iterador en el texto, que se usa en todos los algoritmos), ya que no
se va acceder a ellas durante la ejecución. Esta optimización anula en parte el funcionamiento como clase,
pero se hace así porque no se puede declarar un atributo para que se almacene en registro y el compilador
no puede optimizar como nosotros (no sabe que el valor del atributo no va a ser usado desde otro método
y por tanto no lo guarda en registro). Para algoritmos como el de la fuerza bruta el aumento de velocidad
es considerable.
Respecto a la compilación hay que indicar que muchas funciones y métodos de se declaran como
inline, por lo que es imprescindible utilizar las opciones adecuadas para que el compilador no generen
llamadas a función, lo que empeora sensiblemente el rendimiento (para el gcc esto implica utilizar la
opción -O2 o -O3).
-a, --algorithm=[algoritmo]
Permite seleccionar el algoritmo a utilizar, si no se da el nombre del algoritmo obtenemos una lista
de valores posibles. Para utilizar más de un algoritmo se pueden incluir varias opciones -a en la línea
de comandos. Si no se incluye esta opción se ejecutan todos los algoritmos.
-A, --alphabet=[cadena]
Hace que los algoritmos utilicen como alfabeto el conjunto de caracteres incluidos en la cadena pasada
como parámetro. Si se pasa este parámetro el patrón debe contener sólo símbolos alfabéticos.
-b, --breakpoints=[cadena]
Indica en que puntos de ruptura debe detenerse el programa para preguntarle al usuaro si debemos
continuar. Esta opción sólo es efectiva cuando el algoritmo se invoca con las opciones -i y -s acti-
vadas.
-c, --compact
Muestra una salida más compacta para la medida de tiempos y el recuento de pasos. Los datos de
cada algoritmo estan en una línea que contiene el identificador del algoritmo, el tiempo de ejecución
y las cuentas de pasos separadas por comas. Los campos se separan usando ’:’
3.5. El programa de análisis 55
-d, --delays=[cadena]
Indica en que puntos de ruptura debe esperar un programa un determinado lapso de tiempo (por
defecto 1 segundo) cuando se ejecuta el programa con las opciones -i y -s activadas.
-D, --delay-time=[ms]
Indica el tiempo (en milisegundos) que debe durar la pausa en los puntos de ruptura marcados con
-d. La opción sólo es efectiva si se ejecuta el programa con las oprciones -i y -s activadas.
-f, --find-first
Le indica al programa que sólo búsque la primera ocurrencia del patrón en el texto (por defecto las
busca todas).
-h, --help
Imprime un mensaje con las opciones
-i, --interactive
Aplica retardos a todos los pasos de los algoritmos y se detiene para recibir entrada del usuario cuando
nos encontramos en un acierto parcial. Sólo es útil si se emplea junto con -s
-n, --no-prep
Preprocesa el patrón antes de la búsqueda. Esta opción es útil para medir tiempos, ya que si buscamos
más de una vez el patrón (opción -T) el preproceso sólo se ejecuta una vez y no se calcula su tiempo
de ejecución.
-p, --patterns[=ARCHIVO]
Lee los patrones de un archivo, cada uno es una línea. Si se usa esta opción no se debe pasar un patrón
en la línea de comandos, sólo el texto.
-r, --report
Imprime los pasos ejecutados en cada algoritmo
-s, --show
Muestra la ejecución de los algoritmos.
-t, --time
Calcula el tiempo que tarda en ejecutarse cada algoritmo
-T, --text-size[=TAM]
Ejecuta cada algoritmo varias veces para simular que el texto tiene un tamaño de TAM bytes. Esta
opción sólo se emplea junto a -t
-v, --version
Imprime la versión del programa de prueba
Respecto al código del programa hay poco que decir, está escrito siguiendo un modelo imperativo
de programación, definiendo unas pocas funciones auxiliares y el bloque principal del programa. Lo
primero que hace es crear un vector con los algoritmos de búsqueda implementados e inicializar las
variables auxiliares con los valores por defecto. Despues lee los parámetros de entrada usando la función
getopt_long de GNU. Según las opciones leidas se asignan valores a variables o se generan mensajes de
error. Cuando se han leido los parámetros y no se han producido errores se lee el patrón (excepto si se ha
leido la opción -p, en cuyo caso tendremos los patrones almacenados en una cola) y el texto (almacenando
todo el contenido del fichero como una sola cadena). Despues ajusta los parámetros que dependen del
texto o el patrón y entra en el bloque principal, que ejecuta los algoritmos seleccionados para cada patrón.
El código del programa se puede ver en los apendices.
Capítulo 4. Análisis y resultados experimentales
En este capítulo describiremos el análisis de los algoritmos implementados, comentando el diseño de los
casos de prueba (elección de los textos y patrones de entrada, condiciones del análisis, etc.), los resultados
obtenidos y las conclusiones que se derivan de ellos.
1. Ulysses de James Joyce (en inglés)1. Texto literario. Para las pruebas se empleó el texto resultante de
concatenar por orden todos los capítulos (1.585.338 bytes).
2. Constitución española de 1978 (en castellano)2. Texto técnico. Para las pruebas se usó el texto tal cual
se obtuvo del navegador (113.343 bytes).
3. Secuencia de ADN HC21-000020 del Cromosoma 213. Cadena de ADN. Para las pruebas se
transformó el texto para simplificar las búsquedas. Primero se agruparon los bloques de cada línea
(cada una se divide en 6 bloques de 10 bases) eliminando los espacios en blanco y posteriormente se
eliminaron los saltos de linea, con lo que se obtubo un texto de una sola línea con toda la secuencia
de bases (1.296.826 bytes).
La elección de los textos se debe a su tamaño (dos relativamente grandes y otro más pequeño), el tipo de
texto (literario, legal y cadena de ADN), al uso de alfabetos distintos (ASCII de 7 bits, ISO-8859-1y bases
’A’, ’C’, ’G’ y ’T’ de ADN) y a su disponibilidad (todos se pueden obtener a traves de Internet).
Algunos de los estudios de la eficiencia de los algoritmos de búsqueda emplean textos sintéticos
generados aleatoriamente o usando las propiedades estadísticas del tipo de texto ([Dav86], [Bae89]),
pero nosotros hemos preferido hacerlo con textos reales que son los que se utilizan en la mayoría
de aplicaciones.
1
Obtenido de ftp://blaze.trentu.ca/pub/jjoyce/ulysses/ascii_texts/ulys*.txt.
2
Obtenida en el URL http://ccdis.dis.ulpgc.es/~secrdis/constitucion.txt
3
URL http://www-eri.uchsc.edu/chr21/dna/HC21-000020.html
56
4.1. Diseño de los casos de prueba 57
propio texto y otro seleccionándolas del diccionario del sistema (/usr/dict/english para Ulysses y
/usr/dict/spanish para la Constitución). En el caso de la secuencia de DNA se eligieron fragmentos
de distintos tamaños de la misma secuencia.
Para los dos primeros textos las subcadenas se eligen seleccionando palabras agrupadas por tamaño
1
. Para la ejecución se seleccionan aleatoriamente 20 subcadenas distintas de cada grupo. En los casos en
los que no hay 20 subcadenas diferentes del mismo tamaño se usan todas las disponibles.
Para la secuencia de ADN se eligieron aleatoriamente cinco subcadenas de 10, 25, 50, 75, 100, 250,
500 y 750 elementos, tomadas directamente del texto a buscar.
1
Las palabras se definen como subcadenas que no contienen espacios (entendiendo como tales los que retornan verdadero al ejecutar
la función isspace() de C++), lo que ocasiona que encontremos subcadenas que contienen símbolos de puntuación y números al
seleccionar palabras de los textos (en los diccionarios también podría pasar, pero no hay ninguna puntuación).
4.2. Resultados de la ejecución 58
400
300
kr
Pasos preproceso
kmp
so
200
bm
bmh
sqs
100
0
0 5 10 15 20 25
Ulysses
400
300
kr
Pasos de búsqueda
kmp
so
200
bm
bmh
sqs
100
0
0 10 20 30 40
HC21-0000020
3000
2000
kr
Pasos de preproceso
kmp
so
bm
bmh
sqs
1000
0
0 200 400 600 800
150000
100000 bf
Pasos de la búsqueda
kr
kmp
so
bm
bmh
50000 sqs
0
0 5 10 15 20 25
Ulysses (1585338)
2000000
1500000
bf
Pasos preproceso
kr
kmp
1000000 so
bm
bmh
sqs
500000
0
0 10 20 30 40
HC21-0000020 (1296826)
2000000
1500000
bf
Pasos preproceso
kr
kmp
1000000 so
bm
bmh
sqs
500000
0
0 200 400 600 800
60
40 bf
kr
kmp
Mb/s so
bm
bmh
20 sqs
0
0 5 10 15 20 25
Ulysses
80
60
bf
kr
kmp
Mb/s 40 so
bm
bmh
sqs
20
0
0 10 20 30 40
HC21-0000020
50
40
bf
30 kr
kmp
Mb/s so
bm
20 bmh
sqs
10
0
0 200 400 600 800
4.3. Conclusiones
Comentaremos a continuación los resultados de los análisis presentados antes y las posiblidades
gráficas de la biblioteca implementada.
fuerza bruta, lo que no quiere decir que en textos repetitivos cualquiera de los otros dos no pueda ser
muy superior.
Los algoritmos de la familia Boyer-Moore tambien se comportan como era de esperar, incluso si
miramos la búsqueda sobre la cadena de ADN veremos que la versión completa (Boyer-Moore) es más
rápida que las otras dos, ya que hace uso del alfabeto para mejorar la heurística de salto mientras que los
otros dos son modificaciones del algoritmio que se basan en eliminar esa tabla, que sólo es útil para tabajar
con textos altamente repetitivos.
[Aho83]
Aho, A. V.; Hopcroft, J. E. y Ullman, J. D.. Data Structures and Algorithms. Addison-Wesley,
Reading, Massachusetts, 1983.
[Aho90]
Alfred V. Aho. Algorithms for Finding Patterns in Strings. En J. van Leeuwen, editor, Handbook of
Theoretical Computer Science, páginas 255–300. Elsevier Science Publishers, New York, 1990.
[BM77]
Boyer, R. S. y Moore, J. S.. A Fast String Searching Algorithm. Comunications of the ACM 20 (10),
62–72 (Octubre 1977).
[Bae89]
Ricardo A. Baeza-Yates. Efficient Text Searching. Research Report CS-89-17 (1989), Departament
of Computer Science, University of Waterloo, Ontario.
[Bae91]
Baeza-Yates, R. A. y Gonnet, G. H.. Handbook of Algorithms and Data Structures: in Pascal and
C. Addison-Wesley, Reading, Massachusetts. Segunda Edición, 1991.
[Bae92a]
Ricardo A. Baeza-Yates. Text Retrieval: Theory and Practice, Twelfth IFIP World Computer
Congress, Madrid, España, Septiembre 1992.
[Bae92b]
Baeza-Yates, R. A. y Gonnet, G. H.. A New Aproach to Text Searching. Comunications of the ACM
35 (10), 74–82 (Octubre 1992).
[C++96]
Working Paper for Draft Proposed International Standard for Information Systems - Programming
Language C++, Diciembre 1996. URL http://www.maths.warwick.ac.uk/c++/pub/.
[Com79]
Commentz-Walter, B.. A string matching algorithm fast on the average. En H. A. Maurer, editor,
Proc. 6th International Coll. on Automata, Languajes and Programing, páginas 118–132, Springer,
Berlin, 1979.
[Dav86]
Davies, G. y Bowsher, S.. Algorithms for Pattern Matching. Software – Practice and Experience 16
(6), 575–601 (Junio 1986).
[Debian]
Debian/GNU Linux. URL http://www.debian.org/. Páginas Web relacionadas con la distribución
Debian del sistema operativo Linux
64
65
[GNU]
GNU WWW. URL http://www.gnu.org/. Páginas Web relacionadas con el proyecto GNU (GNU’s
Not Unix) de la FSF (Free Software Foundation)
[Hor80]
Horspool, R. N.. Practical Fast Searching in String. Software – Practice and Experience 10 (6),
501–506 (1980).
[Hum91]
Hume, A. y Sunday, D. M.. Fast String Searching. Software – Practice and Experience 21 (11),
1221–1248 (Noviembre 1991).
[KMP77]
Knuth, D. E.; Morris, J. H. y Pratt, V. R.. Fast Pattern Matching in Strings. SIAM Journal on
Computing 6 (2), 323–350 (Junio 1977).
[KR87]
Karp, R. M. y Rabin, M. O.. Efficient Randomized Pattern Matching Algorithms. IBM J. Res.
Develop. 31 (2), 249–260 (1987).
[Kin96a]
Jeffrey H. Kingston. An Expert’s Guide to the Lout Document Formatting System (Version 3.10).
Basser Departament of Computer Science, University of Sydney, Octubre 1996.
[Kin96b]
Jeffrey H. Kingston. A User’s Guide to the Lout Document Formatting System (Version 3.10). Basser
Departament of Computer Science, University of Sydney, Noviembre 1996.
[Lam86]
Leslie Lamport. LATEX User’s Guide and Reference Manual. Addison-Wesley, Reading, Massachu-
setts, 1986.
[Man93]
Manber, U. y Wu, S.. GLIMPSE: A Tool to Search Through Entire File Systems. TR 93-34
(Octubre 1993), Department of Computer Science, University of Arizona, Tucson, Arizona. URL
ftp://ftp.cs.arizona.edu/glimpse/glimpse.ps.Z.
[SGML]
SGML Web Page. URL http://www.sil.org/sgml/. Páginas Web relacionadas con el SGML
(Standard Generalized Markup Languaje).Incluye punteros a información y programas relacionados
con el SGML
[Ste92]
Graham A. Stephen. String Search. ?? TR-92-gas-01 (Octubre 1992), School of Electronic
Engineering Science, University College of North Wales.
[Ste95]
Stepanov, A. A. y Lee, M.. The Standard Template Library, Hewlett-Packard Laboratories, Palo Alto,
California, Febrero 1995. URL http://www.cs.rpi.edu/~musser/.
[Str91]
Bjarne Stroustroup. The C++ Programing Languaje. Addison-Wesley, Reading, Massachusetts.
Segunda Edición, 1991.
66
[Sun90]
Daniel M. Sunday. A Very Fast Substring Search Algorithm. Comunications of the ACM 33 (8),
132–142 (Agosto 1990).
[Wat92]
Watson, B. W. y Zwaan, G.. A taxonomy of keyword pattern matching algorithms. Computing
Science Notes 92/27 (Diciembre 1992), Eindhoven University of Technology, Eindhoven, Holanda.
[Wat94]
Bruce W. Watson. The performance of single-keyword and multiple-keyword pattern matching
algorithms. Computing Science Notes 94/19 (Mayo 1994), Eindhoven University of Technology,
Eindhoven, Holanda.
[Wat95]
Watson, B. W. y Zwaan, G.. A taxonomy of sublinear multiple keyword pattern matching algorithms.
Computing Science Notes 95/ (Abril 1995), Eindhoven University of Technology, Eindhoven,
Holanda.
[Wu92]
Wu, S. y Manber, U.. Fast Text Searching Allowing Errors. Comunications of the ACM 35 (10),
83–91 (Octubre 1992).
[glimpse]
GLIMPSE: GLobal IMPlicit SEarch. URL http://glimpse.cs.arizona.edu/. Sistema de indexación y
recuperación que permite realizar búsquedas muy eficientes en multiples archivos
[lout]
The Basser Lout Document Formating System. URL ftp://ftp.cs.su.oz.au/jeff/lout/. Sistema de
composición de documentos similar al LATEX
[sgmltools]
SGML-Tools. URL http://web.inter.NL.net/users/C.deGroot/sgmltools/. Sistema basado en el
SGML que permite generar versiones en distintos formatos a partir de un solo documento SGML
escrito empleando el linuxdoc-dtd (usado para escribir los HOWTO de Linux )