Está en la página 1de 8

Estructura de Datos y Algoritmos II (EDA II) curso 2019-2020

Conferencia # 07: “Búsqueda de patrones en un texto (String Matching)”


 Definiciones
 TDA String. Clases de Java
 Algoritmo de Fuerza bruta
 Algoritmo de Boyer-Moore

Bibliografía.
Thomas H. Cormen, Charles E. Leiserson, and Ronald L. Rivest, Introduction to algorithms, en
\\10.12.1.64\docs\MFC\Docencia\Materiales Docentes\Pregrado\Computacion\07- Programacion e
Ingenieria de Software\Estructuras de Datos y Algoritmos II\03- MaterialesEstudio\Introduction to
Algorithms 3rd Edition.pdf, capítulo 32 página 985.

Introducción.
El procesamiento de documentos es hoy día una de las funciones dominantes de las computadoras.
Éstas se usan para editar documentos, buscarlos, transportarlos por Internet y mostrarlos en impresoras
y pantallas de computadora. La navegación y la búsqueda por la Web se han convertido en
aplicaciones cada vez más importantes y muchos de los cálculos clave de todo el procesamiento de
documentos implican strings de caracteres y la coincidencia de patrones de string.
En este tema se presentan varios de los algoritmos fundamentales de procesamiento de texto, para
efectuar con rapidez operaciones string importantes.
 Definiciones
- String Matching:
Dado un patrón P de longitud m y un texto T de longitud n (n ≥ m), se trata de encontrar las posiciones
del texto que contienen el patrón de búsqueda.
Vamos a asumir que:
 el texto T es un arreglo T[0 . . n-1] de longitud n
 el patrón P es un arreglo P[0 . . m-1] de longitud m
 P y T contienen elementos de un alfabeto finito
Vamos a definir algunos conceptos necesarios en este problema para trabajar con textos o cadenas. A
menudo se les denomina a P y T como cadena.
- Un alfabeto ( ) es un conjunto finito no vacío de posibles caracteres.
Ejemplo:
 ASCII
 Unicode
 {0,1}
 {A,C,G,T}
 {A,B,C,…, Z}
- Una cadena es una secuencia de caracteres de un alfabeto finito.
Ejemplo:
 Programa en Java
 Documento HTML
 Secuencia de ADN
 es el conjunto de cadenas de longitud finita formada por caracteres del alfabeto .
*
- La longitud o tamaño de una cadena x es la cantidad de caracteres que contiene, y es representada
por |x|. La cadena vacía tiene longitud 0 y también  *.
Sea P una cadena de tamaño m:
- Una subcadena P[i .. j] de P es una subsecuencia de P compuesta de los caracteres en el rango i y j.
- Se llama prefijo de P a una subcadena Pi del tipo P[0..i] con i<=n-1. Una cadena x es prefijo de una
cadena P si xw= P, para w*, y se denota por x  P.
- Se llama sufijo a una subcadena del tipo P[i ..m - 1] con i>=0. Una cadena x es sufijo de una cadena
P si wx=P, para w*, y se denota por x  P.
Podemos entonces decir que dadas las cadenas T y P, el problema de String Matching
consiste de encontrar subcadenas de T iguales a P.
- Se dice que un patrón P se encuentra con un desplazamiento s en el texto T (el patrón comienza en
la posición s+1 en el texto T) si
0 s n-m y T[s + 1 . . s + m] = P[1 . . m], o sea si T[s + j] = P[j], para 1 j m. - Si P ocurre
con desplazamiento s en T, entonces se llama a s desplazamiento válido, en otro caso, se llama a s un
desplazamiento inválido.
El problema de String Matching se puede definir como la búsqueda de todos los
desplazamientos válidos con los cuales el patrón P se encuentra en un texto dado T.

 TDA String. Clases de Java


En el corazón de los algoritmos de procesamiento de texto se encuentran los métodos para manejar
strings de caracteres. Estos objetos pueden originarse en gran variedad de fuentes, que abarcan
aplicaciones científicas, lingüísticas y de Internet.
A continuación, se dan algunos ejemplos de strings:
P = " CGTAFACTGCTTTAATCAAACGC "
S = “http://www.wiley.com.college/cs2java/”

El primer string es acerca del DNA; el último es la dirección de Internet (URL) del sitio que acompaña
al libro.
Las operaciones de string tienen dos acciones: los que modifican el string sobre el que actúan, y los
que só1o regresan información sobre él, sin modificarlo. Java hace que la distinción sea precisa, al
definir que la clase String represente strings inmutables, que no se pueden modificar, y la clase
StringBuffer para representar los strings mutables, que sí se pueden modificar.
 Clase String de Java
Las operaciones principales de la clase String de Java son las siguientes:
length(): Regresa la longitud n de S.
Entrada: ninguna; Salida: int
charAt(i): Regresa el carácter del índice i en S.
Entrada: int; Salida: char.
startsWith(Q): Determina si Q es un prefijo de S.
Entrada: String; Salida: boolean.
endsWith(Q): Determina si Q es un sufijo de S.
Entrada: Sring; Salida: boolean.
substring(i,j): Regresa el substring S[i,j].
Entrada: int (i) e int (j); Salida: String
concat(Q): Regresa la concatenación de S y Q, es decir, S + Q.
Entrada: String; Salida: String.
equals(Q): Determina si Q es igual a S.
Entrada: String; Salida: boolean.
indexOf(Q): Si Q es un substring de S, regresa el índice del inicio de la primera ocurrencia
de Q en S; en caso contrario regresa -1.
Entrada: String; Salida: int.
Esta colección forma las operaciones características de strings inmutables.
Ejemplo 1: Se tiene el siguiente conjunto de operaciones, que se ejecutan con la cadena S =
"abcdefghijklmnop":

 Clase StringBuffer de Java


Los métodos principales de la clase StringBuffer de Java se muestran a continuación:
append(Q): Regresa a S + Q, reemplazando a S con S + Q.
Entrada: String; Salida: StringBuffer.
insert(i,Q): Regresa y actualiza a S para que sea el string obtenido, insertando a Q dentro
de S y comenzando en el índice i.
Entrada: String; Salida: StringBuffer.
reverse(): Invierte y regresa la cadena S.
Entrada: ninguna; Salida: StringBuffer.
setCharAt(i,ch): Establece el carácter en el índice i en S para que sea ch.
Entrada: int (i) y char(ch); Salida: ninguna.
charAt(i): Regresa el carácter en el índice i en S.
Entrada: int: Salida: char.
Se producen condiciones de error cuando el índice i sale de los límites de los índices del string. Por
fortuna, la clase StringBuffer de Java proporciona un método toString() que regresa una versión
String de S, que se puede usar para tener acceso a los métodos String.
Ejemplo 2: Se tiene la siguiente secuencia de operaciones, que se ejecutan sobre el string mutable que
inicialmente es S = "abcdefghijklmnop":

No se han mostrado todos los métodos soportados por los objetos String y StringBuffer en Java, pero
los que se dan arriba deben bastar para la mayor parte de las aplicaciones, como por ejemplo el uso en
variables de instancia. Por lo anterior, se consideran los métodos de la clase String como que definen
un TDA string inmutable, y los métodos de StringBuffer para definir un TDA string mutable.
 Algoritmos de coincidencia de patrón
En el problema clásico de coincidencia de patrón con strings, se da un string T de texto, de longitud n,
y un string P patrón de longitud m, y se desea saber si P es un substring de T.
La noción de una "coincidencia" es que hay un substring de T que comienza en algún índice i, que
coincide con P, carácter par carácter, de modo que:
T[i] = P[0], T[i + 1] = P[1], . . . , T[i + m – 1] = P[m – 1].
Esto es, P = T[i .. i+m–1]. Entonces, el resultado de un algoritmo de coincidencia de patrón podría ser
una indicación de que no existe el patrón P en T, o bien un entero indicando el índice inicial en T, de
un substring que coincide con P. Esta es exactamente la computación que hace el método indexOf de la
interfaz String de Java. También, se puede tratar de encontrar todos los índices donde comienza un
substring de T que coincide con P.
Como la mayor parte de los algoritmos de procesamiento de documentos se usan en aplicaciones en las
que el conjunto básico de caracteres es finito, se supone, en general, que el tamaño del alfabeto ∑,
representado por │∑│, es una constante fija y finita.
Veremos tres algoritmos de coincidencia de patrón, con niveles crecientes de dificultad.
 Algoritmo de Fuerza Bruta
El patrón de diseño de un algoritmo de fuerza bruta es una técnica poderosa cuando se tiene algo que
se desea buscar, o cuando se desea optimizar una función. Para aplicar esta técnica en un caso general,
normalmente se enumeran todas las configuraciones posibles de las entradas que intervienen y se
escoge la mejor de esas configuraciones enumeradas.
 Comparación de un patrón por fuerza bruta
Al aplicar esta técnica para diseñar el algoritmo de comparación de patrón por fuerza bruta, se deduce
lo que quizás sea el primer algoritmo en que se podría pensar para resolver el problema de coincidencia
de patrón: tan sólo se prueban todas las colocaciones posibles de P en relación con T. Este algoritmo,
que se presenta en el Fragmento de programa 1, es bastante simple.
Algoritmo BruteForceMatch(T, P):
Entrada: Strings T (texto) con n caracteres y P (patrón) con m caracteres
Salida: índice de partida del primer substring de T que coincide con P, ó -1.
for( int i=0 ; i<=(n–m) ; i++)//para cada índice candidato en T
{ j = 0; //puntero del patrón
while (j < m && T[i + j] = P[j])
j++;
if (j == m)
return i;
}
return -1;
Fragmento de programa 1: Comparación de patrón por fuerza bruta.

El algoritmo de comparación de patrón por fuerza bruta está formado por dos ciclos anidados donde el
exterior recorre todos los índices iniciales posibles del patrón en el texto, y el ciclo interior recorre
cada carácter del patrón, comparándolo con su carácter potencialmente correspondiente del texto.
El tiempo de ejecución de la comparación de patrón por fuerza bruta no es bueno, en el peor de los
casos, porque para cada índice candidato en T se pueden ejecutar hasta m comparaciones de carácter,
para descubrir que P no coincide con T en el índice actual. De acuerdo con el Fragmento de programa
1, se muestra que se ejecuta el ciclo for externo cuando mucho n - m + 1 veces, y el ciclo interior se
ejecuta cuando mucho m veces. Así, el tiempo de ejecución del método de fuerza bruta es O((n - m +
1)m), que se simplifica a la forma O(n m). Por consiguiente, en el peor de los casos, cuando n y m son
aproximadamente iguales, este algoritmo tiene un tiempo de ejecución cuadrático.
Ejemplo 3: Se tiene el string de texto T = “abacaabaccabacabaabb” y la cadena patrón
P= "abacab".
En la Figura 1 se ilustra la ejecución del algoritmo de coincidencia de patrón por fuerza bruta, con T y
P.

Figura 1: Corrida de ejemplo del algoritmo de coincidencia de patrón por fuerza bruta. El
algoritmo hace 27 comparaciones de caracteres, indicadas arriba con etiquetas numéricas.
 Algoritmo de Boyer-Moore (BM)
Al principio parecería que siempre es necesario examinar cada carácter en T para localizar un patrón P
como substring. Sin embargo, ese no siempre es el caso porque el algoritmo de comparación de patrón
BM a veces puede evitar las comparaciones entre P y una fracción apreciable de los caracteres de T. El
único inconveniente es que, mientras que el algoritmo de fuerza bruta puede funcionar aun con un
alfabeto potencialmente no acotado, el algoritmo BM supone que el alfabeto es de tamaño fijo y finito.
Funciona con la máxima rapidez cuando el alfabeto tiene un tamaño moderado y el patrón es
relativamente largo. Así, el algoritmo BM es ideal para buscar palabras en documentos.
La base del algoritmo BM es mejorar el tiempo de ejecución del algoritmo de fuerza bruta agregándole
dos métodos heurísticos (es decir, por tanteos) con la intención de ahorrar tiempo. Esos métodos,
enunciados brevemente, son los siguientes:
Heurística de la lupa: Cuando se prueba una colocación posible de P en T, comenzar las
comparaciones desde el final de P y avanzar hacia atrás, hacia el frente de P.
Heurística de salto de caracteres: Durante la prueba de una colocación posible de P contra T, se
maneja como sigue una falta de coincidencia del carácter de texto T[i] = c, respecto al correspondiente
carácter P[i] del patrón:
Si c no está contenido en lugar alguno de P, recorrer a P por completo hasta pasar a T[i],
porque no puede coincidir carácter alguno en P. En caso contrario, desplazar P hasta que una
aparición del carácter c en P quede alineada con T[i].
A nivel intuitivo, estas heurísticas funcionan como un equipo integrado. La heurística de la lupa
prepara la otra heurística para permitir evitar comparaciones entre P y grupos completos de caracteres
en T. Cuando menos en este caso se puede llegar con más rapidez al destino yendo hacia atrás, porque
si se encuentra una desigualdad durante la consideración de P en cierto lugar de T, es posible evitar
grandes cantidades de comparaciones innecesarias al desplazar P en forma apreciable en relación con
T, usando la heurística de salto de caracteres.
En consecuencia, veamos la definición de cómo se puede integrar la heurística de salto de caracteres en
un algoritmo de coincidencia de patrón de string. Para implementarla se define la función last(c), que
toma un carácter c del alfabeto y caracteriza hasta dónde se puede desplazar el patrón P si se encuentra
en el texto un carácter igual a c, que no es igual al patrón. En particular, se define last(c) como sigue:
Si c está en P, last(c) es el índice de la última ocurrencia (la de la extrema derecha) de c
en P. En caso contrario, se define last(c) = -1.
La función last se puede implementar en el tiempo O(m + │∑│) dado P, ya que esta depende de la
cantidad de caracteres que tiene el Patrón (m) porque debe buscar la última ocurrencia de los caracteres
en él y esto lo debe realizar por cada carácter del alfabeto ∑, por tanto también depende del tamaño del
mismo. Esta función last proporciona toda la información necesaria para ejecutar la heurística de salto
de caracteres.
En el Fragmento de programa 2 se muestra el algoritmo BM de comparación de patrón.
Algoritmo BMMatch(T,P):
Entrada: Strings T (texto) con n caracteres y P (patrón) con m caracteres
Salida: índice inicial del primer substring de T que coincide con P, ó -1
calcular la función last;
i = m – l ;
j = m – 1 ;
do
if (P[j] = T[i])
if (j = 0 )
return i; //una coincidencia)
else
{i --;
j --;}
else
{i = i + m – min(j, 1+last(T[i])); //paso de salto
j = m – l;}
while (i <= n – 1);
return -1;
Fragmento de programa 2: El algoritmo Boyer-Moore de coincidencia de patrón.
El paso de salto se ilustra en la Figura 2.
Figura 2: Ilustración del paso de salto en el algoritmo BM (véase el Fragmento de programa
2), donde se usa la notación l = last(T[i]). Se presentan dos casos:
(a) l + l < j, cuando se desplaza el patrón j - l unidades;
(b) j < l + l, donde el patrón se desplaza una unidad.
La correctitud del algoritmo BM de coincidencia de patrón es consecuencia de que cada vez que el
método hace un desplazamiento, se garantiza que no "se salta" coincidencias posibles, porque last(c) es
el lugar de la última aparición de c en P.
El tiempo de ejecución del algoritmo BM en el peor de los casos es O(n (m + │∑│)). Es decir, el
cálculo de la función last requiere el tiempo O(m + │∑│), y el ciclo del algoritmo es un O(n), en el
peor de los casos, igual que el algoritmo de fuerza bruta.
Pero es válido aclarar que dicha complejidad temporal está dada por el cálculo de la función last para
cada carácter dentro del ciclo, es decir, en cada iteración del ciclo se hace necesario recalcular la
función last para el carácter que se está analizando en ese momento. De esta manera nuestro algoritmo
es cuadrático.
Sin embargo, si asumiéramos el cálculo de la función last como un método privado y se obtiene su
solución para todos los caracteres del alfabeto antes de entrar al ciclo, el algoritmo tendría complejidad
lineal, pero ganaría en complejidad espacial debido a que se necesita una estructura de datos para
guardar el resultado de dicha función y poder acceder luego a él.
Un ejemplo de un par texto-patrón que alcanza el peor de los casos es

En la Figura 3 se representa la ejecución del algoritmo Boyer-Moore de coincidencia de patrón en un


string de entrada parecida a la del ejemplo 3.
La función last (c)
. c│ a b c d
last (c)│ 4 5 3 -1

Figura 3: Ilustración del algoritmo BM de coincidencia de patrón. El algoritmo


hace 13 comparaciones de caracteres, que se indican con etiquetas numéricas.

En realidad, el algoritmo BM puede con cierta frecuencia, saltarse grandes partes del texto (véase la
Figura 4). Hay pruebas experimentales de que, con texto en inglés, la cantidad promedio de
comparaciones hechas por carácter de texto es, aproximadamente, 0.24 para un string de patrón de
cinco caracteres.

Figura 4: Ejecución del algoritmo Boyer-Moore en un texto y con patrón ambos en inglés,
donde se logra una aceleración importante. Nótese que no se examinan todos los caracteres
del texto.

En realidad, se acaba de presentar una versión simplificada del algoritmo de Boyer-Moore (BM). El
algoritmo original logra un tiempo de ejecución O(n + m +│∑│) usando una heurística alternativa de
desplazamiento del string de texto con comparación parcial, siempre que desplaza el patrón más que la
heurística de salto de caracteres.

Conclusiones.
En un texto T de longitud n y un patrón P de longitud m:
 El algoritmo de Fuerza Bruta es un O((n - m + 1)m),
 El de Boyer-Moore (BM) es un O(n(m + │∑│)).

También podría gustarte