CC30A Algoritmos y Estructuras de Datos
CC30A Algoritmos y Estructuras de Datos
CC30A Algoritmos y Estructuras de Datos
Datos
Benjamin Bustos ([email protected])
Patricio Poblete ([email protected])
Datos
Instrucciones Elementales
Instrucciones Compuestas
Ejemplos de programas iterativos
Diagramas de Estados
Recursividad
"Dividir para reinar"
Recursividad y Tabulacin (Programacin Dinmica)
Conceptos de Programacin Orientada al Objeto (OOP)
En esta seccin se revisarn los elementos bsicos que se van a utilizar para escribir
programas. Esto supone que los alumnos ya saben programar, y es slo un resumen y
una ordenacin de conceptos. La notacin utilizada es la del lenguaje Java, pero los
conceptos son ms generales y se aplican a muchos otros lenguajes similares.
Datos
Los programas representan la informacin que manejan mediante valores llamados
"constantes", y dichos valores se almacenan en "variables".
Variables
int k; // entero
float x; // real
double prom; // real de doble precisin
boolean condicion; // verdadero o falso
char c; // un carcter
String nombre; // secuencia de caracteres
Constantes
3 // int
4.50 // float
1e-6 // float
'a' // char
"hola" // String
Instrucciones Elementales
Asignacin
Esta es la instruccin ms simple, que permite modificar el valor de una variable:
a = E; // asigna a la variable 'a' el valor de la expresin 'E'
Ejemplos:
k = 0;
k = k+1;
k += 1;
++k;
k++;
Las tres ltimas son abreviaturas. La notacin "+=" permite evitar repetir el lado
izquierdo de la asignacin. Las dos ltimas incrementan el valor de la variable en 1,
pero difieren respecto del valor retornado. En el primer caso (preincremento) se
incrementa primero y luego se retorna el valor resultante. El el segundo caso
(postincremento) se incrementa despus, y se retorna el valor previo de la variable.
Salida
System.out.println("Hola!");
Instrucciones Compuestas
Estas instrucciones se forman agrupando a otras instrucciones, ya sean elementales o
compuestas, usando las reglas de secuencia, alternacin (if) e iteracin (while).
Secuencia de instrucciones
Un grupo de instrucciones escritas una a continuacin de la otra se ejecutan en ese
mismo orden:
instruccin1;
instruccin2;
. . .
Tambin es posible agrupar las instrucciones entre llaves para que sean equivalentes a
una sola instruccin:
{
instruccin1;
instruccin2;
. . .
}
Ejemplo:
// Intercambiar dos valores a y b
{
int aux = a;
a = b;
b = aux;
}
Instrucciones condicionales
Forma 1:
if( condicin )
instruccin1;
else
instruccin2;
Forma 2:
if( condicin )
instruccin;
Ejemplo:
// a = abs(a)
if( a<0 )
a = -a; // se omite el "else" si es vaco
Ejemplo:
// Ordenar a, b (borrador)
if( a>b )
intercambiar a, b;
La lnea destacada no es una instruccin real del lenguaje, es slo una forma de dejar
pendiente esa parte del programa. Ms adelante se podr "refinar" esa seudo-instruccin
definiendo:
intercambiar a, b =>
{
int aux = a;
a = b;
b = aux;
}
Si se efecta la sustitucin del texto refinado en lugar del que se haba escrito
originalmente, resulta un texto de programa refinado que cumple con las reglas del
lenguaje. Para ayudar a la auto-documentacin del programa, se puede conservar la
seudo-instruccin como comentario:
// Ordenar a, b
if( a>b )
{ // intercambiar a, b
int aux = a;
a = b;
b = aux;
}
Nota: En este caso, las llaves no son realmente necesarias, pero pueden utiizarse si
ayudan a la claridad del programa.
Este enfoque de solucin tiene la desventaja que es difcil de generalizar. Por ejemplo,
el programa que encuentra el mximo de cuatro variables tiene aproximadamente el
doble de lneas que ste, y por lo tanto el tamao del programa va creciendo
exponencialmente. Adems no hay forma de escribir un programa para un nmero de
variables que no sea conocido a priori.
// m = max(a,b,c)
// Solucin 2 (borrador)
m = a;
m = max(m,b);
m = max(m,c);
Con cada instruccin que se ejecuta, el estado del proceso cambia. Para entender lo que
est sucediendo en el programa, puede resultar til intercalar comentarios que describan
lo que sabemos que se cumple despus de cada instruccin:
// m = max(a,b,c)
// Solucin 2 (versin refinada y con afirmaciones)
m = a;
// m == max(a)
if( b>m )
m = b;
// m == max(a,b)
if( c>m )
m = c;
// m == max(a,b,c)
Instruccin iterativa
Esta ltima afirmacin se deduce del hecho que al terminar el ciclo se sabe que el
invariante sigue siendo verdadero, pero la condicin del ciclo es falsa. En estricto rigor,
la afirmacin que podramos hacer ah es
// k>=n && k<=n && m == max(a[1],,,a[k])
Esto, que puede parecer ocioso, es muy til, porque a continuacin se relaja la
exigencia de esta condicin, haciendo que se cumpla la primera parte, pero
dejando que la segunda se satisfaga con "k<=n".
2. Escribir la inicializacin, la cual debe asegurar que el invariante se cumpla antes
de empezar a iterar.
3. Encontrar la condicin de trmino. Esto se obtiene de comparar "qu le falta" al
invariante para ser igual al estado final.
4. Escribir el cuerpo del ciclo, el cual debe:
o conseguir que el proceso avance, de modo que termine algn da, y
o preservar el invariante.
Estos dos ltimos objetivos suelen ser contrapuestos. Al efectuar un avance en el
proceso, los valores de las variables cambian, con el resultado que a menudo se deja de
satisfacer el invariante. Por lo tanto, el resto del cuerpo del ciclo se suele dedicar a tratar
de recuperar la validez del invariante.
while( k<n )
{
// Insertar a[k] entre a[0],...,a[k-1]
t = a[k];
for( j=k; j>0 && a[j-1]>t; --j )
a[j] = a[j-1];
a[j] = t;
++k;
}
2
El tiempo que demora este algoritmo en el peor caso es del orden de n , lo que se
denotar O(n2).Se puede demostrar que esto mismo es cierto si se considera el
caso promedio.
En palabras: "Los elementos desde k hasta n-1 ya estn ordenados y son mayores que
los primeros k".
// Ordenar a[0], ..., a[n-1] por seleccin
k = n; // inicialmente los n primeros estn desordenados
while( k>=2 )
{
Llevar el max de a[0], ..., a[k-1] hacia a[k-1];
--k;
}
Donde
Llevar el max de a[0], ..., a[k-1] hacia a[k-1] =>
i = 0; // a[i] es el max hasta el momento
for( j=1; j<=k-1; ++j )
if( a[j]>a[i] )
i = j;
// ahora intercambiamos a[i] con a[k-1]
t = a[i];
a[i] = a[k-1];
a[k-1] = t;
El tiempo que demora este algoritmo es O(n2), y no hay diferencia entre el peor caso y
el caso promedio.
Ordenacin de la Burbuja
Este mtodo se basa en hacer pasadas de izquierda a derecha sobre los datos,
intercambiando pares de elementos adyacentes que estn fuera de orden. Al final de
cada pasada, en forma natural el mximo estar en la posicin de ms a la derecha (que
es su posicin final) y puede por lo tanto ser excluido en pasadas sucesivas.
Esto conduce al siguiente invariante (idntico al de ordenacin por seleccin):
Donde
Hacer una pasada sobre a[0], ..., a[k-1] =>
for( j=0; j<=k-2; ++j )
if( a[j]>a[j+1] )
{ // Intercambiar a[j] con a[j+1]
t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
y
Disminuir k =>
--k;
Esto ltimo puede parecer ocioso, pero pronto se ver que el expresarlo de esta manera
da una flexibilidad que resulta til.
Un problema que presenta este programa es que si el archivo est incialmente ordenado,
el programa igual hace n pasadas, cuando despus de la primera ya podra haberse dado
cuenta que el archivo ya estaba ordenado.
Para aprovechar cualquier posible orden que pueda haber en el archivo, se puede hacer
que el programa anote ("recuerde") el lugar en donde se produjo el ltimo intercambio.
Si la variable i se define de manera que el ltimo intercambio en una pasada dada fue
entre a[i-1] y a[i], entonces todos los elementos desde a[i] en adelante estn ya
ordenados (de lo contrario habra habido intercambios ms hacia la derecha), y por lo
tanto k se puede disminuir haciendo que sea igual a i:
Hacer una pasada sobre a[0], ..., a[k-1] =>
i=0;
for( j=0; j<=k-2; ++j )
if( a[j]>a[j+1] )
{ // Intercambiar a[j] con a[j+1]
t = a[j];
a[j] = a[j+1];
a[j+1] = t;
//Recordar el lugar del ltimo intercambio
i = j+1;
}
Disminuir k =>
k=i;
El tiempo que demora este algoritmo tanto en el peor caso como en promedio es O(n2).
Clculo de xn
Un algoritmo simple consiste en multiplicar n veces:
// Algoritmo simple
y = 1;
for( j=n; j>0; --j )
y = y*x;
Este algoritmo evidentemente toma tiempo O(n), y su invariante se puede escribir como
y * xj == xn
con invariante
y * zj == xn
Esto podra parecer ocioso, pero permite hacer una optimizacin al observar que est
permitido modificar la variable z al inicio del ciclo siempre que se mantenga la validez
Este algoritmo demora tiempo O(log n), lo cual se debe a que j slo se puede dividir
log n veces por 2 antes de llegar a 1. Es cierto que j slo se divide cuando es par, pero
si es impar en una iteracin del for, est garantizado que ser par a la siguiente.
Diagramas de Estados
Un diagrama de estados nos permite visualizar los diferentes estados por los que va
pasando un programa. Las transiciones de un estado a otro se realizan ya sea
incondicionalmente o bajo una condicin. Adems, pueden ir acompaadas de una
accin que se realiza junto con la transicin.
Ejemplo: Contar palabras en una frase.
Para simplificar, supongamos que la frase est almacenada en un string s, y
supongamos que la frase termina con un punto. Por ejemplo,
String s = "Este es un ejemplo.";
Para los fines de este ejemplo, diremos que una "palabra" es cualquier secuencia de
caracteres consecutivos distintos de blanco (y punto).
Para resolver este problema, examinaremos los caracteres del string de izquierda a
derecha, usando charAt(k), y lo que se haga con cada caracter depende si estbamos
dentro o fuera de una palabra. Esto ltimo correspnde al estado del programa.
Problema
Reordenar los elementos de a[0], ..., a[n] dejando a la izquierda los <0 y a la
derecha los >=0.
Solucin 1:
Invariante:
// Version 1
// Version 2
i = 0;
j = n;
while( i<j )
{
if( a[i]<0 )
++i;
else if( a[j]>=0 )
--j;
else
{
a[i] <-> a[j];
++i;
--j;
}
}
i = 0;
j = n;
while( i<j )
{
while( i<j && a[i]<0 )
++i;
while( i<j && a[j]>=0 )
--j;
if( i<j )
{
a[i] <-> a[j];
++i;
--j;
}
}
Solucin 2:
Invariante:
i = 0;
for( j=0; j<=n; ++j )
if( a[j]<0 )
{
a[i] <-> a[j];
++i;
}
Recursividad
Al programar en forma recursiva, buscamos dentro de un problema otro subproblema
que posea su misma estructura.
Ejemplo: Calcular xn.
// Version 1
public static float elevar( float x, int n )
{
if( n==0 )
return 1;
else
return x * elevar(x, n-1);
}
// Version 2
public static float elevar( float x, int n )
{
if( n==0 )
return 1;
else if( n es impar )
return x * elevar( x, n-1 );
else
return elevar( x*x, n/2 );
}
representados por arreglos a[0], ..., a[n-1] y b[0], ..., b[n-1]. Queremos
calcular los coeficientes del polinomio C(x) tal que C(x) = A(x)*B(x).
Un algoritmo simple para calcular esto es:
// Multiplicacin de polinomios
for( k=0; k<=2*n-2; ++k )
c[k] = 0;
for( i=0; i<n; ++i)
for( j=0; j<n; ++j)
c[i+j] += a[i]*b[j];
Supongamos que n es par, y dividamos los polinomios en dos partes. Por ejemplo, si
A(x) = 2 + 3*x - 6*x2 + x3
y en general
A(x) = A'(x) + A"(x) * xn/2
B(x) = B'(x) + B"(x) * xn/2
Entonces
C = (A' + A"*xn/2) * (B' + B"*xn/2)
= A'*B' + (A'*B" + A"*B') * xn/2 + A"*B" * xn
(p>q)
(p<q)
(p=q)
entonces
C = E + (D-E-F)*xn/2 + F*xn
(n>=2)
0
0
1
1
2
1
3
2
4
3
5
5
6 7 8 9 10 11 . . .
8 13 21 34 55 89 . . .
Se puede demostrar que los nmeros de Fibonacci crecen exponencialmente, como una
funcin O(n) donde =1.618....
El problema que se desea resolver es calcular fn para un n dado.
La definicin de la recurrencia conduce inmediatamente a una solucin recursiva:
public static int F( int n )
{
if( n<= 1)
return n;
else
return F(n-1)+F(n-2);
}
0
0
1
0
2
1
3
2
4
4
5 6 7 8 9 10 ...
7 12 20 33 54 88 ...
El origen de esta ineficiencia es que la recursividad calcula una y otra vez los mismos
valores, porque no guarda memoria de haberlos calculado antes.
Una forma de evitarlo es utilizar un arreglo auxiliar fib[], para anotar los valores ya
calculados. Un mtodo general es inicializar los elementos de fib con algn valor
especial "nulo". Al llamar a F(n), primero se consulta el valor de fib[n]. Si ste no es
"nulo", se retorna el valor almacenado en el arreglo. En caso contrario, se hace el
clculo recursivo y luego se anota en fib[n] el resultado, antes de retornarlo. De esta
manera, se asegura que cada valor ser calculado recursivamente slo una vez.
En casos particulares, es posible organizar el clculo de los valores de modo de poder ir
llenando el arreglo en un orden tal que, al llegar a fib[n], ya est garantizado que los
valores que se necesitan (fib[n-1] y fib[n-2]) ya hayan sido llenados previamente.
En este caso, esto es muy sencillo, y se logra simplemente llenando el arreglo en orden
ascendente de subndices:
fib[0] = 0;
fib[1] = 1;
for( j=2; j<=n; ++j )
fib[j] = fib[j-1]+fib[j-2];
Esta es una ecuacin de recurrencia de segundo orden, porque fn depende de los dos
valores inmediatamente anteriores. Definamos una funcin auxiliar
gn = fn-1
Con esto, podemos re-escribir la ecuacin para fn como un sistema de dos ecuaciones
de primer orden:
fn
gn
f1
g1
=
=
=
=
fn-1+gn-1
fn-1
1
0
fn = A*fn-1
donde
fn = [ fn ]
[ gn ]
A = [ 1 1 ]
[ 1 0 ]
Clases
En Java un objeto es una instancia de una clase.
Ejemplo:
// Clase Entero, que permite leer y
// guardar un valor en una variable entera
public class Entero
{
// Datos privados
private int valor;
// Mtodos pblicos
public int leer()
{
return valor;
}
public void guardar( int x )
{
valor = x;
}
}
// Ejemplo de programa principal
public class Prueba
{
public static void main( String[] args )
{
Entero m = new Entero();
m.guardar( 5 );
System.out.println( "m=" + m.leer() );
}
}
Tipos de mtodos
Constructores
Permiten inicializar el objeto. Puede haber varios constructores con distinto nmero y
tipos de parmetros.
Si no hay un constructor definido, los campos se inicializan automticamente con
valores nulos.
El constructor debe tener el mismo nombre que la clase.
Ejemplo: Clase para almacenar fechas:
public class Fecha
{
private int a;
private int m;
private int d;
// Constructor con parmetros
public Fecha( int aa, int mm, int dd )
{
a = aa;
m = mm;
d = dd;
}
// Constructor sin parmetros
public Fecha()
{
a = 2001;
m = 1;
d = 1;
}
}
Ejemplos de uso:
Fecha f1 = new Fecha();
Fecha f2 = new Fecha( 2001, 4, 11 );
"Mutators" y "accessors"
Las variables de una clase tipicamente son privadas. Para mirar su valor, o para
modificarlo, hay que utilizar mtodos ad hoc (como leer y guardar en el ejemplo de la
clase Entero).
Esto es un mayor grado de burocracia, pero aisla a los usuarios de una clase de los
detalles de implementacin de ella, y evita que se vean afectados por eventuales
cambios en dicha implementacin.
toString
Al imprimir un objeto a usando println, automticamente se invoca a
a.toString()
para convertirlo a una forma imprimible. Esto mismo ocurre cada vez que se utiliza a en
un contexto de String.
En el ejemplo, si vamos a imprimir objetos de tipo Fecha, debemos proveer una
implementacin de toString dentro de esa clase:
public String toString()
{
return d + "/" + m + "/" + a;
}
equals
El mtodo equals se utiliza para ver si dos objetos tienen el mismo valor. Se invoca
if( x.equals(y) )
y se declara como
public boolean equals( Object b )
El tipo Object usado aqu es un tipo de objeto "universal" del cual se derivan todos los
otros. El siguiente ejemplo muestra una implementacin de equals para la clase Fecha:
public boolean equals( Object b )
{
if( !(b instanceof Fecha) )
return false; // el otro objeto no era de tipo Fecha
Fecha f = (Fecha) b; // para verlo como una Fecha
return a==f.a && m==f.m && d==f.d;
}
this
La referencia this identifica al objeto actual. Permite desde dentro de la clase accesar
los campos propios diciendo, por ejemplo, this.a. Esto en realidad es redundante,
porque significa lo mismo que decir simplemente a, pero puede ser ms claro en la
lectura.
Tambin permite comparar si este objeto es el mismo que otro (no slo si tienen el
mismo contenido, sino si ambas referencias apuntan al mismo objeto).
El otro uso de this es como constructor, para llamar a otro constructor de la misma
clase. Por ejemplo, el constructor sin parmetros de la clase Fecha podra haber sido
declarado como:
public Fecha()
{
this( 2001, 1, 1 );
}
Campos estticos
Hay dos tipos de campos estticos:
public final static double PI = 3.14159; // constante
private static int precioActual = 1300; // variable compartida
// por todos los objetos de esa clase
Mtodos estticos
Los mtodos estticos estn asociados a una clase, no a objetos particulares dentro de
ella.
Ejemplos:
Math.sin
Integer.parseInt
Packages
Las clases se pueden agrupar en "paquetes". Para esto, cada clase debe precederse de
package P;
class C
{
. . .
}
Cuando a una variable no se le pone public ni private, es visible slo dentro del
mismo package.
La clase C se denomina P.C, pero si antes decimos import P.C; o bien import P.*;,
entonces podemos referirnos a la clase simplemente como C;
Herencia
Principio que permite reutilizar trabajo ya hecho. Se basa en la relacin is-a.
Ejemplo:
Crculo is-a Figura
Auto is-a Vehculo
Las clases forman una jerarqua en base a la relacin de herencia.
Otro tipo de relacin distinta es has-a. Por ejemplo:
Auto has-a Manubrio
Este tipo de relacin se llama agregacin y a menudo es ms importante que la
herencia.
Clase base:
La clase de la cual se derivan otras
Clase derivada:
Hereda todas las propiedades de la clase base. Luego puede agregar campos y
mtodos, o redefinir mtodos.
Los cambios que se hagan en la clase derivada no afectan a la clase base.
Sintaxis:
public class Derivada extends Base
{
. . .
}
Visibilidad
Los campos privados de la clase base no se ven desde las clases derivadas. Para que un
campo de este tipo sea visible debe declararse como protected. Esta posibilidad debe
usarse con mucho cuidado.
Constructores
Cada clase define su propio constructor, el cual lleva el mismo nombre que la clase.
Si no se define un constructor, se genera automticamente un constructor sin parmetros
que:
llama al constructor con cero parmetros de la clase base, para la parte heredada,
y luego
inicializa con valores nulos los campos restantes.
final
Si un mtodo se declara como final, entonces no puede ser redefinido en las clases
derivadas.
Anlogamente, una clase declarada como final no puede ser extendida.
Mtodos abstractos
Un mtodo abstracto declara funcionalidad, pero no la implementa. Las clases derivadas
deben proveer implementaciones para estos mtodos.
Una clase abstracta es una clase que tiene al menos un mtodo abstracto. No se puede
crear objetos pertenecientes a una clase abstracta (no se puede ejecutar new).
Ejemplo:
abstract class Figura
{
private String nombre;
abstract public double area();
public Figura( String n )
{
nombre = n;
}
// Este constructor no puede ser invocado directamente,
// slo lo usan las clases derivadas
Herencia mltiple
En algunos lenguajes, una clase puede heredar de ms de una clase base. En Java esto
no se permite, lo cual evita los conflictos que se podran producir al heredarse
definiciones incompatibles de mtodos y variables.
Interfaz
Una interfaz es un mecanismo que permite lograr algunos de los efectos de la herencia
mltiple, sin sus problemas.
Una interfaz es una clase que slo tiene mtodos pblicos abstractos y campos pblicos
estticos finales.
Se dice que una clase implementa a la interfaz si provee definiciones para todos los
mtodos abstractos de la interfaz.
Una clase puede extender slo a una clase base, pero puede implementar muchas
interfaces.
Ejemplo:
package Definiciones;
public interface Comparable
{
public int Compare( Comparable b );
}
final public class Entero implements Comparable
{
private int valor;
public Entero( int x )
{
valor = x;
}
public String toString()
{
return Integer.toString( valor );
}
public int valorEntero()
{
return valor;
}
public int Compare( Comparable b )
{
return valor - ((Entero) b).valor;
}
}
Comparable t = a[k];
for( j=k; j>0 &&
t.Compare(a[j-1])<0; --j)
a[j] = a[j-1];
a[j] = t;
}
}
Arreglos.
Punteros y variables de referencia.
Listas enlazadas.
rboles.
o rboles binarios.
o rboles generales.
Arreglos
Un arreglo es una secuencia contigua de un nmero fijo de elementos homogneos. En
la siguiente figura se muestra un arreglo de enteros con 10 elementos:
donde tipo corresponde al tipo de los elementos que contendr el arreglo (enteros,
reales, caracteres, etc..), nombre corresponde al nombre con el cual se denominar el
arreglo, y n_elem corresponde al nmero de elementos que tendr el arreglo. Para el
caso del ejemplo presentado, la declaracin del arreglo de enteros es:
int[] arreglo = new int[10];
Para acceder a un elemento del arreglo se utiliza un ndice que identifica a cada
elemento de manera nica. Los ndices en Java son nmeros enteros correlativos y
comienzan desde cero, por lo tanto, si el arreglo contiene n_elem elementos el ndice del
ltimo elemento del arreglo es n_elem-1. El siguiente cdigo muestra como se puede
inicializar el arreglo del ejemplo, luego de ser declarado:
arreglo[0]=80; //el primer indice de los arreglos en Java es 0
arreglo[1]=45;
arreglo[2]=2;
arreglo[3]=21;
arreglo[4]=92;
arreglo[5]=17;
arreglo[6]=5;
arreglo[7]=65;
arreglo[8]=14;
arreglo[9]=34; //el ultimo indice del arreglo es 10-1 = 9
Una ventaja que tienen los arreglos es que el costo de acceso de un elemento del arreglo
es constante, es decir no hay diferencias de costo entre accesar el primer, el ltimo o
cualquier elemento del arreglo, lo cual es muy eficiente. La desventaja es que es
necesario definir a priori el tamao del arreglo, lo cual puede generar mucha prdida de
espacio en memoria si se definen arreglos muy grandes para contener conjuntos
pequeos de elementos (Nota: en Java es posible hacer crecer el tamao de un arreglo
de manera dinmica).
Por ejemplo, todas las clases en Java heredan de la clase Object. Una instancia de sta
clase se declara como:
Object aux=new Object();
La variable aux es una referencia a un objeto de la clase Object que permite saber la
ubicacin de dicho objeto dentro de la memoria, informacin suficiente para poder
operar con l. Intuitivamente, la referencia es como una "flecha" que nos indica la
posicin del objeto que apunta:
Listas enlazadas
Una lista enlazada es una serie de nodos, conectados entre s a travs de una referencia,
en donde se almacena la informacin de los elementos de la lista. Por lo tanto, los nodos
de una lista enlazada se componen de dos partes principales:
class NodoLista
{
Object elemento;
NodoLista siguiente;
}
La referencia lista indica la posicin del primer elemento de la lista y permite acceder a
todos los elementos de sta: basta con seguir las referencias al nodo siguiente para
recorrer la lista.
NodoLista aux=lista;
aux=aux.siguiente;
Siguiendo con el ejemplo anterior, para insertar un nuevo nodo justo delante del nodo
referenciado por aux se deben modificar las referencias siguiente del nodo aux y del
nodo a insertar.
{
NodoLista aux=lista;
while (aux!=null)
{
System.out.println(aux.elemento);
aux=aux.siguiente;
}
}
Para invertir el orden de la lista, es decir, que el ltimo elemento de la lista ahora sea el
primero, que el penltimo elemento de la lista ahora sea el segundo, etc..., modificando
slo las referencias y no el contenido de los nodos, es necesario realizar una sola pasada
por la lista, y en cada nodo visitado se modifica la referencia siguiente para que apunte
al nodo anterior. Es necesario mantener referencias auxiliares para acordarse en donde
se encuentra el nodo anterior y el resto de la lista que an no ha sido modificada:
void invertir(NodoLista lista)
{
NodoLista siguiente=lista;
NodoLista anterior=null;
while(lista!=null)
{
siguiente=lista.siguiente;
lista.siguiente=anterior;
anterior=lista;
lista=siguiente;
}
}
La implementacin vista de los nodos tambin se conoce como lista de enlace simple,
dado que slo contiene una referencia al nodo siguiente y por lo tanto slo puede
recorrerse en un solo sentido. En una lista de doble enlace se agrega una segunda
referencia al nodo previo, lo que permite recorrer la lista en ambos sentidos, y en
general se implementa con una referencia al primer elemento y otra referencia al ltimo
elemento.
Una lista circular es aquella en donde la referencia siguiente del ltimo nodo en vez de
ser null apunta al primer nodo de la lista. El concepto se aplica tanto a listas de enlace
simple como doblemente enlazadas.
En muchas aplicaciones que utilizan listas enlazadas es til contar con un nodo
cabecera, tambien conocido como dummy o header, que es un nodo "falso", ya que no
contiene informacin relevante, y su referencia siguiente apunta al primer elemento de
la lista. Al utilizar un nodo cabecera siempre es posible definir un nodo previo a
cualquier nodo de la lista, definiendo que el previo al primer elemento es la cabecera.
Si se utiliza un nodo cabecera en una lista de doble enlace ya no es necesario contar con
las referencias primero y ltimo, puesto que el nodo cabecera tiene ambas referencias:
su referencia siguiente es el primer elemento de la lista, y su referencia anterior es el
ltimo elemento de la lista. De esta forma la lista de doble enlace queda circular de una
manera natural.
rboles
Un rbol se define como una coleccin de nodos organizados en forma recursiva.
Cuando hay 0 nodos se dice que el rbol esta vaco, en caso contrario el rbol consiste
en un nodo denominado raz, el cual tiene 0 o ms referencias a otros rboles, conocidos
como subrboles. Las races de los subrboles se denominan hijos de la raz, y
consecuentemente la raz se denomina padre de las races de sus subrboles. Una visin
grfica de esta definicin recursiva se muestra en la siguiente figura:
Los nodos que no poseen hijos se denominan hojas. Dos nodos que tienen el padre en
comn se denominan hermanos.
Un camino entre un nodo n1 y un nodo nk est definido como la secuencia de nodos n1,
n2, ..., nk tal que ni es padre de ni+1, 1 <= i < k. El largo del camino es el nmero de
referencias que componen el camino, que para el ejemplo son k-1. Existe un camino
desde cada nodo del rbol a s mismo y es de largo 0. Ntese que en un rbol existe un
nico camino desde la raz hasta cualquier otro nodo del rbol. A partir del concepto
de camino se definen los conceptos de ancestro y descendiente: un nodo n es ancestro
de un nodo m si existe un camino desde n a m; un nodo n es descendiente de un nodo m
si existe un camino desde m a n.
Se define la profundidad del nodo nk como el largo del camino entre la raz del arbol y
el nodo nk. Esto implica que la profundidad de la raz es siempre 0. La altura de un
nodo nk es el mximo largo de camino desde nk hasta alguna hoja. Esto implica que la
altura de toda hoja es 0. La altura de un rbol es igual a la altura de la raz, y tiene el
mismo valor que la profundidad de la hoja ms profunda. La altura de un rbol vaco se
define como -1.
La siguiente figura muestra un ejemplo de los conceptos previamente descritos:
rboles binarios
Un rbol binario es un rbol en donde cada nodo posee 2 referencias a subrboles (ni
ms, ni menos). En general, dichas referencias se denominan izquierda y derecha, y
consecuentemente se define el subrbol izquierdo y subrbol derecho del arbol.
Los nodos en s que conforman un rbol binario se denominan nodos internos, y todas
las referencias que son null se denominan nodos externos.
In = suma del largo de los caminos desde la raz a cada nodo interno (largo de
caminos internos).
En = suma del largo de los caminos desde la raz a cada nodo externo (largo de
caminos externos).
Se tiene que:
En = In+2n
Demostracin: induccin sobre n (ejercicio).
Propiedad 3:
Cuntos rboles binarios distintos se pueden construir con n nodos internos?
n bn
0 1
1 1
2 2
3 5
bn?
Si la raz del rbol es una constante o una variable se retorna el valor de sta.
Si la raz resulta ser un operador, entonces recursivamente se evalan los
subrboles izquierdo y derecho, y se retorna el valor que resulta al operar los
valores obtenidos de las evaluaciones de los subrboles con el operador
respectivo.
rboles generales
En un rbol general cada nodo puede poseer un nmero indeterminado de hijos. La
implementacin de los nodos en este caso se realiza de la siguiente manera: como no se
sabe de antemano cuantos hijos tiene un nodo en particular se utilizan dos referencias,
una a su primer hijo y otra a su hermano ms cercano. La raz del rbol necesariamente
tiene la referencia a su hermano como null.
class NodoArbolGeneral
{
Object elemento;
NodoArbolGeneral hijo;
NodoArbolGeneral hermano;
}
Ntese que todo rbol general puede representarse como un rbol binario, con la
salvedad que el hijo derecho de la raz es siempre null. Si se permite que la raz del
rbol tenga hermanos, lo que se conoce como bosque, entonces se tiene que el conjunto
de los bosques generales es isomorfo al conjunto de los rboles binarios. En efecto, las
propiedades vistas en los rboles binarios se siguen cumpliendo en los rboles
generales.
TDA lista.
TDA pila.
TDA cola.
TDA cola de prioridad
Un Tipo de dato abstracto (en adelante TDA) es un conjunto de datos u objetos al cual
se le asocian operaciones. El TDA provee de una interfaz con la cual es posible realizar
las operaciones permitidas, abstrayndose de la manera en como estn implementadas
dichas operaciones. Esto quiere decir que un mismo TDA puede ser implementado
utilizando distintas estructuras de datos y proveer la misma funcionalidad.
El paradigma de orientacin a objetos permite el encapsulamiento de los datos y las
operaciones mediante la definicin de clases e interfaces, lo cual permite ocultar la
manera en cmo ha sido implementado el TDA y solo permite el acceso a los datos a
travs de las operaciones provistas por la interfaz.
En este captulo se estudiarn TDA bsicos como lo son las listas, pilas y colas, y se
mostrarn algunos usos prcticos de estos TDA.
TDA lista
Una lista se define como una serie de N elementos E1, E2, ..., EN, ordenados de manera
consecutiva, es decir, el elemento Ek (que se denomina elemento k-simo) es previo al
elemento Ek+1. Si la lista contiene 0 elementos se denomina como lista vaca.
Las operaciones que se pueden realizar en la lista son: insertar un elemento en la
posicin k, borrar el k-simo elemento, buscar un elemento dentro de la lista y preguntar
si la lista esta vaca.
Una manera simple de implementar una lista es utilizando un arreglo. Sin embargo, las
operaciones de insercin y borrado de elementos en arreglos son ineficientes, puesto
que para insertar un elemento en la parte media del arreglo es necesario mover todos los
elementos que se encuentren delante de l, para hacer espacio, y al borrar un elemento
es necesario mover todos los elementos para ocupar el espacio desocupado. Una
implementacin ms eficiente del TDA se logra utilizando listas enlazadas.
A continuacin se presenta una implementacin en Java del TDA utilizando listas
enlazadas y sus operaciones asociadas:
TDA pila
Una pila (stack o pushdown en ingls) es una lista de elementos de la cual slo se puede
extraer el ltimo elemento insertado. La posicin en donde se encuentra dicho elemento
se denomina tope de la pila. Tambin se conoce a las pilas como listas LIFO (LAST IN
- FIRST OUT: el ltimo que entra es el primero que sale).
Nota: algunos autores definen desapilar como sacar el elemento del tope de la pila sin
retornarlo.
TDA cola
Una cola (queue en ingls) es una lista de elementos en donde siempre se insertan
nuevos elementos al final de la lista y se extraen elementos desde el inicio de la lista.
Tambin se conoce a las colas como listas FIFO (FIRST IN - FIRST OUT: el primero
que entra es el primero que sale).
Al igual que con el TDA pila, una cola se puede implementar tanto con arreglos como
con listas enlazadas. A continuacin se ver la implementacin usando un arreglo.
Las variables de instancia necesarias en la implementacin son:
class ColaArreglo
{
private Object[] arreglo;
private int primero, ultimo, numElem;
private int MAX_ELEM=100; // maximo numero de elementos en la cola
public ColaArreglo()
{
arreglo=new Object[MAX_ELEM];
primero=0;
ultimo=MAX_ELEM-1;
numElem=0;
}
public void encolar(Object x)
{
if (numElem<MAX_ELEM) // si esta llena se produce OVERFLOW
{
ultimo=(ultimo+1)%MAX_ELEM;
arreglo[ultimo]=x;
numElem++;
}
}
public Object sacar()
{
if (!estaVacia()) // si esta vacia se produce UNDERFLOW
{
Object x=arreglo[primero];
primero=(primero+1)%MAX_ELEM;
numElem--;
return x;
}
}
public boolean estaVacia()
{
return numElem==0;
}
}
Heaps
Un heap es un rbol binario de una forma especial, que permite su almacenamiento en
un arreglo sin usar punteros.
Un heap tiene todos sus niveles llenos, excepto posiblemente el de ms abajo, y en este
ltimo los nodos estn lo ms a la izquierda posible.
Ejemplo:
La numeracin por niveles (indicada bajo cada nodo) son los subndices en donde cada
elemento sera almacenado en el arreglo. En el caso del ejemplo, el arreglo sera:
La caracterstica que permite que un heap se pueda almacenar sin punteros es que, si se
utiliza la numeracin por niveles indicada, entonces la relacin entre padres e hijos es:
Hijos del nodo j = {2*j, 2*j+1}
Padre del nodo k = floor(k/2)
Un heap puede utilizarse para implementar una cola de prioridad almacenando los datos
de modo que las llaves estn siempre ordenadas de arriba a abajo (a diferencia de un
rbol de bsqueda binaria, que ordena sus llaves de izquierda a derecha). En otras
palabras, el padre debe tener siempre mayor prioridad que sus hijos (ver ejemplo).
Implementacin de las operaciones bsicas
Insercin:
La insercin se realiza agregando el nuevo elemento en la primera posicin libre del
heap, esto es, el prximo nodo que debera aparecer en el recorrido por niveles o,
equivalentemente, un casillero que se agrega al final del arreglo.
Despus de agregar este elemento, la forma del heap se preserva, pero la restriccin de
orden no tiene por qu cumplirse. Para resolver este problema, si el nuevo elemento es
mayor que su padre, se intercambia con l, y ese proceso se repite mientras sea
necesario. Una forma de describir esto es diciendo que el nuevo elemento "trepa" en el
rbol hasta alcanzar el nivel correcto segn su prioridad.
Este algoritmo tambin demora un tiempo proporcional a la altura del rbol en el peor
caso, esto es, O(log n).
TDA diccionario
1. Implementaciones sencillas.
o Bsqueda binaria.
o Bsqueda secuencial con probabilidades de acceso no uniforme.
2. Arboles de bsqueda binaria.
3. Arboles AVL.
4. Arboles 2-3.
5. Arboles B.
6. Arboles digitales.
7. Arboles de bsqueda digital.
8. Skip lists.
9. ABB ptimos.
10. Splay Trees.
11. Hashing.
o Encadenamiento.
o Direccionamiento abierto.
o Hashing en memoria secundaria.
Dado un conjunto de elementos {X1, X2, ..., XN}, todos distintos entre s, se desea
almacenarlos en una estructura de datos que permita la implementacin eficiente de las
operaciones:
Implementaciones sencillas
Una manera simple de implementar el TDA diccionario es utilizando una lista, la cual
permite implementar la insercin de nuevos elementos de manera muy eficiente,
definiendo que siempre se realiza al comienzo de la lista. El problema que tiene esta
implementacin es que las operaciones de bsqueda y eliminacin son ineficientes,
puesto que como en la lista los elementos estn desordenados es necesario realizar una
bsqueda secuencial. Por lo tanto, los costos asociados a cada operacin son:
insertar un nuevo elemento. Sin embargo, la ventaja que tiene mantener el orden es que
es posible realizar una bsqueda binaria para encontrar el elemento buscado.
Bsqueda binaria
Suponga que se dispone del arreglo a, de tamao n, en donde se tiene almacenado el
conjunto de elementos ordenados de menor a mayor. Para buscar un elemento x dentro
del arreglo se debe:
En cada iteracin:
Si el conjunto es vaco (j-i < 0), o sea si j < i, entonces el elemento x no est en
el conjunto (bsqueda infructuosa).
En caso contrario, m = (i+j)/2. Si x = a[m], el elemento fue encontrado
(bsqueda exitosa). Si x < a[m] se modifica j = m-1, sino se modifica i = m+1 y
se sigue iterando.
Implementacin:
Mtodos auto-organizantes
Idea: cada vez que se accesa un elemento Xk se modifica la lista para que los accesos
futuros a Xk sean ms eficientes. Algunas polticas de modificacin de la lista son:
son menores que X, y los valores almacenados en el subrbol derecho de N son mayores
que X.
Los ABB permiten realizar de manera eficiente las operaciones provistas por el TDA
diccionario, como se ver a continuacin.
Bsqueda en un ABB
Esta operacin retorna una referencia al nodo en donde se encuentra el elemento
buscado, X, o null si dicho elemento no se encuentra en el rbol. La estructura del rbol
facilita la bsqueda:
Ntese que el orden en los cuales se realizan los pasos anteriores es crucial para
asegurar que la bsqueda en el ABB se lleve a cabo de manera correcta.
Por lo tanto, la ecuacin que relaciona los costos de bsqueda exitosa e infructuosa es:
(*)
Esto muestra que a medida que se insertan ms elementos en el ABB los costos de
bsqueda exitosa e infructuosa se van haciendo cada vez ms parecidos.
El costo de insercin de un elemento en un ABB es igual al costo de bsqueda
infructuosa justo antes de insertarlo ms 1. Esto quiere decir que si ya haban k
elementos en el rbol y se inserta uno ms, el costo esperado de bsqueda para este
ltimo es 1+C'k. Por lo tanto:
Reemplazando en (**):
Insercin en un ABB
Para insertar un elemento X en un ABB, se realiza una bsqueda infructuosa de este
elemento en el rbol, y en el lugar en donde debiera haberse encontrado se inserta.
Como se vio en la seccin anterior, el costo promedio de insercin en un ABB es
O(log(n)).
Eliminacin en un ABB
Primero se realiza una bsqueda del elemento a eliminar, digamos X. Si la bsqueda fue
infructuosa no se hace nada, en caso contrario hay que considerar los siguientes casos
posibles:
Si X tiene un solo hijo, entonces se cambia la referencia del padre a X para que
ahora referencie al hijo de X.
Ntese que el rbol sigue cumpliendo las propiedades de un ABB con este mtodo de
eliminacin.
Si de antemano se sabe que el nmero de eliminaciones ser pequeo, entonces la
eliminacin se puede substituir por una marca que indique si un nodo fue eliminado o
no. Esta estrategia es conocida como eliminacin perezosa (lazy deletion).
Arboles AVL
Definicin: un rbol balanceado es un rbol que garantiza costos de bsqueda,
insercin y eliminacin en tiempo O(log(n)) incluso en el peor caso.
Un rbol AVL (Adelson-Velskii y Landis) es una rbol de bsqueda binaria que asegura
un costo O(log(n)) en las operaciones de bsqueda, insercin y eliminacin, es decir,
posee una condicin de balance.
La condicin de balance es: un rbol es AVL si para todo nodo interno la diferencia de
altura de sus 2 rboles hijos es menor o igual que 1.
Problema: para una altura h dada, cul es el rbol AVL con mnimo nmero de nodos
que alcanza esa altura?. Ntese que en dicho rbol AVL la diferencia de altura de sus
hijos en todos los nodos tiene que ser 1 (demostrar por contradiccin). Por lo tanto, si Ah
representa al rbol AVL de altura h con mnimo nmero de nodos, entonces sus hijos
deben ser Ah-1 y Ah-2.
En la siguiente tabla nh representa el nmero de nodos externos del rbol AVL con
mnimo nmero de nodos internos.
h
Ah
nh
13
Insercin en un AVL
La insercin en un AVL se realiza de la misma forma que en un ABB, con la salvedad
que hay que modificar la informacin de la altura de los nodos que se encuentran en el
camino entre el nodo insertado y la raz del rbol. El problema potencial que se puede
producir despus de una insercin es que el rbol con el nuevo nodo no sea AVL:
Dado que el primer y ltimo caso son simtricos, asi como el segundo y el tercero, slo
hay que preocuparse de dos casos principales: una insercin "hacia afuera" con respecto
a N (primer y ltimo caso) o una insercin "hacia adentro" con respecto a N (segundo y
tercer caso).
Rotacin simple
El desbalance por insercin "hacia afuera" con respecto a N se soluciona con una
rotacin simple.
Rotacin doble
Claramente un desbalance producido por una insercin "hacia adentro" con respecto a N
no es solucionado con una rotacin simple, dado que ahora es C quien produce el
desbalance y como se vio anteriormente este subrbol mantiene su posicin relativa con
una rotacin simple.
Para el caso de la figura (tercer caso), la altura de N antes de la insercin era G+1. Para
recuperar el balance del rbol es necesario subir C y E y bajar A, lo cual se logra
realizando dos rotaciones simples: la primera entre d y f, y la segunda entre d, ya
Eliminacin en un AVL
La eliminacin en rbol AVL se realiza de manera anloga a un ABB, pero tambin es
necesario verificar que la condicin de balance se mantenga una vez eliminado el
elemento. En caso que dicha condicin se pierda, ser necesario realizar una rotacin
simple o doble dependiendo del caso, pero es posible que se requiera ms de una
rotacin para reestablecer el balance del rbol.
Arboles 2-3
Los rboles 2-3 son rboles cuyos nodos internos pueden contener hasta 2 elementos
(todos los rboles vistos con anterioridad pueden contener slo un elemento por nodo),
y por lo tanto un nodo interno puede tener 2 o 3 hijos, dependiendo de cuntos
elementos posea el nodo. De este modo, un nodo de un rbol 2-3 puede tener una de las
siguientes formas:
Una propiedad de los rboles 2-3 es que todas las hojas estn a la misma profundidad,
es decir, los rboles 2-3 son rboles perfectamente balanceados. La siguiente figura
muestra un ejemplo de un rbol 2-3:
Ntese que se sigue cumpliendo la propiedad de los rboles binarios: nodos internos +
1 = nodos externos. Dado que el rbol 2-3 es perfectamente balanceado, la altura de ste
esta acotada por:
Si el nodo donde se inserta X tena una sola llave (dos hijos), ahora queda con
dos llaves (tres hijos).
Si el nodo donde se inserta X tena dos llaves (tres hijos), queda transitoriamente
con tres llaves (cuatro hijos) y se dice que est saturado (overflow).
Arboles B
La idea de los rboles 2-3 se puede generalizar a rboles t - (2t-1), donde t>=2 es un
parmetro fijo, es decir, cada nodo del rbol posee entre t y 2t-1 hijos, excepto por la
raz que puede tener entre 2 y 2t-1 hijos. En la prctica, t puede ser bastante grande, por
ejemplo t = 100 o ms. Estos rboles son conocidos como rboles B (Bayer).
Insercin en un rbol B
Eliminacin en un rbol B
Arboles B*: cuando un nodo rebalsa se trasladan hijos hacia el hermano, y slo
se crea un nuevo nodo cuando ambos rebalsan. Esto permite aumentar la
utilizacin mnima de los nodos, que antes era de un 50%.
Arboles B+: La informacin solo se almacena en las hojas, y los nodos internos
contienen los separadores que permiten realizar la bsqueda de elementos.
Arboles digitales
Suponga que los elementos de un conjunto se pueden representar como una secuencia
de bits:
X = b0b1b2...bk
Adems, suponga que ninguna representacin de un elemento en particular es prefijo de
otra. Un rbol digital es un rbol binario en donde la posicin de insercin de un
elemento ya no depende de su valor, sino de su representacin binaria. Los elementos en
un rbol digital se almacenan solo en sus hojas, pero no necesariamente todas las hojas
contienen elementos (ver ejemplo ms abajo). Esta estructura de datos tambin es
conocida como trie.
El siguiente ejemplo muestra un rbol digital con 5 elementos.
Hn son los nmeros armnicos y P(n) es una funcin peridica de muy baja amplitud
(O(10-6))
Los ABD poseen un mejor costo promedio de bsqueda que los ABB, pero tambien es
O(log(n)).
Skip lists
Al principio del captulo se vio que una de las maneras simples de implementar el TDA
diccionario es utilizando una lista enlazada, pero tambin se vio que el tiempo de
bsqueda promedio es O(n) cuando el diccionario posee n elementos. La figura muestra
un ejemplo de lista enlazada simple con cabecera, donde los elementos estn ordenados
ascendentemente:
nodos
Esta idea se puede extender agregando una referencia cada cuatro nodos. En este caso, a
lo ms
El caso lmite para este argumento se muestra en la siguiente figura. Cada 2i nodo posee
una referencia al nodo 2i posiciones ms adelante en la lista. El nmero total de
referencias solo ha sido doblado, pero ahora a lo ms
nodos son examinados
durante la bsqueda. Note que la bsqueda en esta estructura de datos es bsicamente
una bsqueda binaria, por lo que el tiempo de bsqueda en el peor caso es O(log n).
El problema que tiene esta estructura de datos es que es demasiado rgida para permitir
inserciones de manera eficiente. Por lo tanto, es necesario relajar levemente las
condiciones descritas anteriormente para permitir inserciones eficientes.
Se define un nodo de nivel k como aquel nodo que posee k referencias. Se observa de la
figura anterior que, aproximadamente, la mitad de los nodos son de nivel 1, que un
cuarto de los nodos son de nivel 2, etc. En general, aproximadamente n/2i nodos son de
nivel i. Cada vez que se inserta un nodo, se elige el nivel que tendr aleatoriamente en
concordancia con la distribucin de probabilidad descrita. Por ejemplo, se puede lanzar
una moneda al aire, y mientras salga cara se aumenta el nivel del nodo a insertar en 1
(partiendo desde 1). Esta estructura de datos es denominada skip list. La siguiente figura
muestra un ejemplo de una skip list:
ABB ptimos
ABB con probabilidades de acceso no uniforme
Problema: dados n elementos X1 < X2 < ... < Xn, con probabilidades de acceso conocidas
p1, p2, ..., pn, y con probabilidades de bsqueda infructuosa conocidas q0, q1, ..., qn, se
desea encontrar el ABB que minimice el costo esperado de bsqueda.
Por ejemplo, para el siguiente ABB con 6 elementos:
Esto es, el costo del rbol completo es igual al "peso" del rbol ms los costos de los
subrboles. Si la raz es k:
Si el rbol completo es ptimo, entonces los subrboles tambin lo son, pero al revs no
necesariamente es cierto, porque la raz k puede haberse escogido mal. Luego, para
encontrar el verdadero costo ptimo C_opti,j es necesario probar con todas las maneras
posibles de elegir la raz k.
C_opti,j = mini+1<=k<=j {Wi,j + C_opti,k-1 + C_optk,j}
C_opti,i = 0 para todo i=0..n
Tiempo: O(n3).
Una mejora: se define ri,j como el k que minimiza mini+1<=k<=j {W[i,j]+C[i,k1]+C[k,j]}. Intuitivamente ri,j-1 <= ri,j <= ri+1,j, y como ri,j-1 y ri+1,j ya son
conocidos al momento de calcular ri,j, basta con calcular minri,j-1 <= k <= ri+1,j
{W[i,j]+C[i,k-1]+C[k,j]}.
Con esta mejora, se puede demostrar que el mtodo demora O(n2) (Ejercicio:
demostrarlo).
Splay Trees
Esta estructura garantiza que para cualquier secuencia de M operaciones en un rbol,
empezando desde un rbol vaco, toma a lo ms tiempo O(M log(N). A pesar que esto
no garantiza que alguna operacin en particular tome tiempo O(N), si asegura que no
existe ninguna secuencia de operaciones que sea mala. En general, cuando una
secuencia de M operaciones toma tiempo O(M f(N)), se dice que el costo amortizado en
tiempo de cada operacin es O(f(N)). Por lo tanto, en un splay tree los costos
amortizados por operacion son de O(log(N)).
La idea bsica de un splay tree es que despus que un nodo es accesado ste se "sube"
hasta la raz del rbol a travs de rotaciones al estilo AVL. Una manera de realizar esto,
que NO funciona, es realizar rotaciones simples entre el nodo accesado y su padre hasta
dejar al nodo accesado como raz del rbol. El problema que tiene este enfoque es que
puede dejar otros nodos muy abajo en el rbol, y se puede probar que existe una
secuencia de M operaciones que toma tiempo O(M N), por lo que esta idea no es muy
buena.
La estrategia de "splaying" es similar a la idea de las rotaciones simples. Si el nodo k es
accesado, se realizaran rotaciones para llevarlo hasta la raz del rbol. Sea k un nodo
distinto a la raz del rbol. Si el padre de k es la raz del rbol, entonces se realiza una
rotacin simple entre estos dos nodos. En caso contrario, el nodo k posee un nodo padre
p y un nodo "abuelo" a. Para realizar las rotaciones se deben considerar dos casos
posibles (ms los casos simtricos).
El otro caso es una insecin zig-zig, en donde k y p son ambos hijos izquierdo o
derecho. En este caso, se realiza la transformacin indicada en la figura anterior.
El efecto del splaying es no slo de mover el nodo accesado a la raz, sino que sube
todos los nodos del camino desde la raz hasta el nodo accesado aproximadamente a la
mitad de su profundidad anterior, a costa que algunos pocos nodos bajen a lo ms dos
niveles en el rbol.
El siguiente ejemplo muestra como queda el splay tree luego de accesar al nodo d.
Hashing
Suponga que desea almacenar n nmeros enteros, sabiendo de antemano que dichos
nmeros se encuentran en un rango conocido 0, ..., k-1. Para resolver este problema,
basta con crear un arreglo de valores booleanos de tamao k y marcar con valor true los
casilleros del arreglo cuyo ndice sea igual al valor de los elementos a almacenar. Es
fcil ver que con esta estructura de datos el costo de bsqueda, insercin y eliminacin
es O(1).
El valor de k puede ser muy grande, y por lo tanto no habra cupo en memoria
para almacenar el arreglo. Piense, por ejemplo, en todos los posibles nombres de
personas.
Los datos a almacenar pueden ser pocos, con lo cual se estara desperdiciando
espacio de memoria.
Una manera de resolver estos problemas es usando una funcin h, denominada funcin
de hash, que transorme un elemento X, perteneciente al universo de elementos posibles,
en un valor h(X) en el rango [0, ..., m-1], con m << k. En este caso, se marca el casillero
cuyo ndice es h(X) para indicar indicar que el elemento X pertenece al conjunto de
elementos. Esta estructura de datos es conocida como tabla de hashing.
La funcin h debe ser de tipo pseudoaleatorio para distribuir las llaves uniformemente
dentro de la tabla, es decir, Pr( h(X) = z) = 1/m para todo z en [0, ..., m-1]. La llave X se
puede interpretar como un nmero entero, y las funciones h(X) tpicas son de la forma:
Encadenamiento
La idea de este mtodo es que todos los elementos que caen en el mismo casillero se
enlacen en una lista, en la cual se realiza una bsqueda secuencial.
Esto implica que el costo esperado de bsqueda slo depende del factor de carga
no del tamao de la tabla.
,y
Direccionamiento abierto
En general, esto puede ser visto como una sucesin de funciones de hash {h0(X), h1(X),
...}. Primero se intenta con tabla[h0(X)], si el casillero est ocupado se prueba con
tabla[h1(X)], y as sucesivamente.
Linear probing
Es el mtodo ms simple de direccionamiento abierto, en donde las funciones de hash se
definen como:
Cuando la tabla de hashing est muy llena, este mtodo resulta ser muy lento.
Cn
Cn'
.6
1.75
3.63
.7
2.17
6.06
.8
13
.9
5.50
50.50
Marcar el casillero como "eliminado", pero sin liberar el espacio. Esto produce
que las bsquedas puedan ser lentas incluso si el factor de carga de la tabla es
pequeo.
Eliminar el elemento, liberar el casillero y mover elementos dentro de la tabla
hasta que un casillero "verdaderamente" libre sea encontrado. Implementar esta
operacin es complejo y costoso.
Hashing doble
En esta estrategia se usan dos funciones de hash: una funcin
conocida
como direccin inicial, y una funcin
conocida como paso. Por lo
tanto:
Elegir m primo asegura que se va a visitar toda la tabla antes que se empiecen a repetir
los casilleros. Nota: solo basta que m y s(X) sean primos relativos (ejercicio: demostralo
por contradiccin).
El anlisis de eficiencia de esta estrategia es muy complicado, y se estudian modelos
idealizados: muestreo sin reemplazo (uniform probing) y muestreo con reemplazo
(random probing), de los cuales se obtiene que los costos de bsqueda esperado son:
Si bien las secuencias de casilleros obtenidas con hashing doble no son aleatorias, en la
prctica su rendimiento es parecido a los valores obtenidos con los muestreos con y sin
reemplazo.
Cn
Cn'
.6
1.53 2.5
.7
1.72 3.33
.8
2.01 5
.9
2.56 10
Este mtodo es eficiente para un factor de carga pequeo, ya que con factor de carga
alto la bsqueda toma tiempo O(n). Para resolver esto puede ser necesario incrementar
el tamao de la tabla, y as reducir el factor de carga. En general esto implica reconstruir
toda la tabla, pero existe un mtodo que permite hacer crecer la tabla paulatinamente en
el tiempo denominado hashing extendible.
Hashing extendible
Suponga que las pginas de disco son de tamao b y una funcin de hash h(X)>=0 (sin
lmite superior). Sea la descomposicin en binario de la funcin de hash h(X)=(... b2(X)
b1(X) b0(X))2.
Inicialmente, todas las llaves se encuentran en una nica pgina. Cuando dicha pgina
se rebalsa se divide en dos, usando b0(X) para discriminar entre ambas pginas:
Cada vez que una pgina rebalsa, se usa el siguiente bit en la sequencia para dividirla en
dos. Ejemplo:
Compresin de datos
En esta seccin veremos la aplicacin de la teora de rboles a la compresin de datos.
Por compresin de datos entendemos cualquier algoritmo que reciba una cadena de
datos de entrada y que sea capaz de generar una cadena de datos de salida cuya
representacin ocupa menos espacio de almacenamiento, y que permite -mediante un
algoritmo de descompresin- recuperar total o parcialmente el mensaje recibido
inicialmente. A nosotros nos interesa particularmente los algoritmos de compresin sin
prdida, es decir, aquellos algoritmos que permiten recuperar completamente la cadena
de datos inicial.
Codificacin de mensajes
Supongamos que estamos codificando mensajes en binario con un alfabeto de tamao
. Para esto se necesitan
longitud.
Ejemplo: para el alfabeto A,
bits
.-...
--..
Podemos ver en el rbol que letras de mayor probabilidad de aparicin (en idioma
ingls) estn ms cerca de la raz, y por lo tanto tienen una codificacin ms corta que
letras de baja frecuencia.
Problema: este cdigo no es auto-delimitante
Por ejemplo, SOS y IAMS tienen la misma codificacin
Para eliminar estas ambigedades, en morse se usa un tercer delimitador (espacio) para
separar el cdigo de cada letra. Se debe tener en cuenta que este problema se produce
slo cuando el cdigo es de largo variable (como en morse), pues en otros cdigos de
largo fijo (por ejemplo el cdigo ASCII, donde cada caracter se representa por 8 bits) es
directo determinar cuales elementos componen cada caracter.
La condicin que debe cumplir una codificacin para no presentar ambigedades, es
que la codificacin de ningun caracter sea prefijo de otra. Esto nos lleva a definir
rboles que slo tienen informacin en las hojas, como por ejemplo:
, entonces el largo
0.30
00
0.25
10
0.08
0110
0.20
11
0.05
0111
0.12
010
Entropa de Shannon
Shannon define la entropa del alfabeto como:
El teorema de Shannon dice que el nmero promedio de bits esperable para un conjunto
de letras y probabilidades dadas se aproxima a la entropa del alfabeto. Podemos
comprobar esto en nuestro ejemplo anterior donde la entropia de Shannon es:
Algoritmo de Huffman
El algoritmo de Huffman permite construir un cdigo libre de prefijos de costo esperado
mnimo.
Inicialmente, comenzamos con hojas desconectadas, cada una rotulada con una letra
del alfabeto y con una probabilidad (ponderacion o peso).
Consideremos este conjunto de hojas como un bosque. El algoritmo es:
while(nro de rboles del bosque > 1){
- Encontrar los 2 rboles de peso mnimo y
unirlos con una nueva raz que se crea para esto.
- Arbitrariamente, rotulamos las dos
lneas como 0 y 1
- Darle a la nueva raz un peso que es
la suma de los pesos de sus subrboles.
}
Ejemplo:
Si tenemos que las probabilidades de las letras en un mensaje son:
Entonces la construccin del rbol de Huffman es (los nmeros en negrita indican los
rboles con menor peso):
Se puede ver que el costo esperado es de 2,53 bits por letra, mientras que una
codificacin de largo fijo (igual nmero de bits para cada smbolo) entrega un costo de
3 bits/letra.
El algoritmo de codificacin de Huffman se basa en dos supuestos que le restan
eficiencia:
1. supone que los caracteres son generados por una fuente aleatoria independiente,
lo que en la prctica no es cierto. Por ejemplo, la probabilidad de encontrar una
vocal despus de una consonante es mucho mayor que la de encontrarla despus
de una vocal; despus de una q es muy probable encontrar una u, etc
2. Debido a que la codificacin se hace caracter a caracter, se pierde eficiencia al
no considerar las secuencias de caracteres ms probables que otras.
Lempel Ziv
Una codificacin que toma en cuenta los problemas de los supuestos enunciados
anteriormente para la codificacin de Huffman sera una donde no solo se consideraran
caracteres uno a uno, sino que donde adems se consideraran aquellas secuencias de alta
probabilidad en el texto. Por ejemplo, en el texto:
aaabbaabaa
Ejemplo:
Si el mensaje es
Ordenacin
1.
2.
3.
4.
5.
Cota inferior
Quicksort
Heapsort
Bucketsort
Mergesort y Ordenamiento Externo
Cota inferior
Supongamos que deseamos ordenar tres datos A, B y C. La siguiente figura muestra un
rbol de decisin posible para resolver este problema. Los nodos internos del rbol
representan comparaciones y los nodos externos representan salidas emitidas por el
programa.
Como se vio en el captulo de bsqueda, todo rbol de decisin con H hojas tiene al
menos altura log2 H, y la altura del rbol de decisin es igual al nmero de
comparaciones que se efectan en el peor caso.
En un rbol de decisin para ordenar n datos se tiene que H=n!, y por lo tanto se tiene
que todo algoritmo que ordene n datos mediante comparaciones entre llaves debe hacer
al menos log2 n! comparaciones en el peor caso.
Usando la aproximacin de Stirling, se puede demostrar que log2 n! = n log2 n +
O(n), por lo cual la cota inferior es de O(n log n).
Quicksort
Este mtodo fue inventado por C.A.R. Hoare a comienzos de los '60s, y sigue siendo el
mtodo ms eficiente para uso general.
Quicksort es un ejemplo clsico de la aplicacin del principio de dividir para reinar. Su
estructura es la siguiente:
Costo promedio
Si suponemos, como una primera aproximacin, que el pivote siempre resulta ser la
mediana del conjunto, entonces el costo de ordenar est dado (aproximadamente) por la
ecuacin de recurrencia
T(n) = n + 2 T(n/2)
Esto tiene solucin T(n) = n log2 n y es, en realidad, el mejor caso de Quicksort.
Para analizar el tiempo promedio que demora la ordenacin mediante Quicksort,
observemos que el funcionamiento de Quicksort puede graficarse mediante un rbol de
particin:
Por la forma en que se construye, es fcil ver que el rbol de particin es un rbol de
bsqueda binaria, y como el pivote es escogido al azar, entonces la raz de cada
subrbol puede ser cualquiera de los elementos del conjunto en forma equiprobable. En
consecuencia, los rboles de particin y los rboles de bsqueda binaria tienen
exactamente la misma distribucin.
En el proceso de particin, cada elemento de los subrboles ha sido comparado contra la
raz (el pivote). Al terminar el proceso, cada elemento ha sido comparado contra todos
sus ancestros. Si sumamos todas estas comparaciones, el resultado total es igual al largo
de caminos internos.
Usando todas estas correspondencias, tenemos que, usando los resultados ya conocidos
para rboles, el nmero promedio de comparaciones que realiza Quicksort es de:
1.38 n log2 n + O(n)
Por lo tanto, Quicksort es del mismo orden que la cota inferior (en el caso esperado).
Peor caso
El peor caso de Quicksort se produce cuando el pivote resulta ser siempre el mnimo o
el mximo del conjunto. En este caso la ecuacin de recurrencia es
T(n) = n - 1 + T(n-1)
lo que tiene solucin T(n) = O(n2). Desde el punto de vista del rbol de particin, esto
corresponde a un rbol en "zig-zag".
Si bien este peor caso es extremadamente improbable si el pivote se escoge al azar,
algunas implementaciones de Quicksort toman como pivote al primer elemento del
arreglo (suponiendo que, al venir el arreglo al azar, entonces el primer elemento es tan
aleatorio como cualquier otro). El problema es que si el conjunto viene en realidad
ordenado, entonces caemos justo en el peor caso cuadrtico.
Mejoras a Quicksort
Quicksort puede ser optimizado de varias maneras, pero hay que ser muy cuidadoso con
estas mejoras, porque es fcil que terminen empeorando el desempeo del algoritmo.
En primer lugar, es desaconsejable hacer cosas que aumenten la cantidad de trabajo que
se hace dentro del "loop" de particin, porque este es el lugar en donde se concentra el
costo O(n log n).
Algunas de las mejoras que han dado buen resultado son las siguientes:
Quicksort con "mediana de 3"
En esta variante, el pivote no se escoge como un elemento tomado al azar, sino que
primero se extrae una muestra de 3 elementos, y entre ellos se escoge a la mediana de
esa muestra como pivote.
Si la muestra se escoge tomando al primer elemento del arreglo, al del medio y al
ltimo, entonces lo que era el peor caso (arreglo ordenado) se transforma de inmediato
en mejor caso.
De todas formas, es aconsejable que la muestra se escoja al azar, y en ese caso el
anlisis muestra que el costo esperado para ordenar n elementos es
(12/7) n ln n
1.19 n log2 n
Esta reduccin en el costo se debe a que el pivote es ahora una mejor aproximacin a la
mediana. De hecho, si en lugar de escoger una muestra de tamao 3, lo hiciramos con
tamaos como 7, 9, etc., se lograra una reduccin an mayor, acercndonos cada vez
ms al ptimo, pero con rendimientos rpidamente decrecientes.
Uso de Ordenacin por Insercin para ordenar sub-arreglos pequeos
Tal como se dijo antes, no es eficiente ordenar recursivamente sub-arreglos demasiado
pequeos.
En lugar de esto, se puede establecer un tamao mnimo M, de modo que los subarreglos de tamao menor que esto se ordenan por insercin en lugar de por Quicksort.
Claramente debe haber un valor ptimo para M, porque si creciera indefinidamente se
llegara a un algoritmo cuadrtico. Esto se puede analizar, y el ptimo es cercano a 10.
Como mtodo de implementacin, al detectarse un sub-arreglo de tamao menor que M,
se lo puede dejar simplemente sin ordenar, retornando de inmediato de la recursividad.
Al final del proceso, se tiene un arreglo cuyos pivotes estn en orden creciente, y
encierran entre ellos a bloques de elementos desordenados, pero que ya estn en el
grupo correcto. Para completar la ordenacin, entonces, basta con hacer una sola gran
pasada de Ordenacin por Insercin, la cual ahora no tiene costo O(n2), sino O(nM),
porque ningn elemento esta a distancia mayor que M de su ubicacin definitiva.
Ordenar recursivamente slo el sub-arreglo ms pequeo
Un problema potencial con Quicksort es la profundidad que puede llegar a tener el
arreglo de recursividad. En el peor caso, sta puede llegar a ser O(n).
Para evitar esto, vemos primero cmo se puede programar Quicksort en forma no
recursiva, usando un stack. El esquema del algoritmo sera el siguiente (en seudo-Java):
void Quicksort(Object a[])
{
Pila S = new Pila();
S.apilar(1,N); // lmites iniciales del arreglo
while(!S.estaVacia())
{
(i,j) = S.desapilar(); // sacar lmites
if(j-i>0) // al menos dos elementos para ordenar
{
p = particionar(a,i,j); // pivote queda en a[p]
S.apilar(i,p-1);
S.apilar(p+1,j);
}
}
}
Con este enfoque se corre el riesgo de que la pila llegue a tener profundidad O(n). Para
evitar esto, podemos colocar en la pila slo los lmites del sub-arreglo ms pequeo,
dejando el ms grande para ordenarlo de inmediato, sin pasar por la pila:
void Quicksort(Object a[])
{
Pila S = new Pila();
S.apilar(1,N); // lmites iniciales del arreglo
while(!S.estaVacia())
{
(i,j) = S.desapilar(); // sacar lmites
while(j-i>0) // al menos dos elementos para ordenar
{
p = particionar(a,i,j); // pivote queda en a[p]
if(p-i>j-p) // mitad izquierda es mayor
{
S.apilar(p+1,j);
j=p-1;
}
else
{
S.apilar(i,p-1);
i=p+1;
}
}
}
}
Con este enfoque, cada intervalo apilado es a lo ms de la mitad del tamao del arreglo,
de modo que si llamamos S(n) a la profundidad de la pila, tenemos:
S(n) <= 1 + S(n/2)
lo cual tiene solucin log2 n, de modo que la profundida de la pila nunca es ms que
logartmica.
Dado que en realidad se hace slo una llamada recursiva y que sta es del tipo "tail
recursion", es fcil transformar esto en un algoritmo iterativo (hacerlo como ejercicio).
El anlisis de Quickselect es difcil, pero se puede demostrar que el costo esperado es
O(n). Sin embargo, el peor caso es O(n2).
Heapsort
A partir de cualquier implementacin de una cola de prioridad es posible obtener un
algoritmo de ordenacin. El esquema del algoritmo es:
En la fase de ordenacin, se van extrayendo elementos del heap, con lo cual este se
contrae de tamao y deja espacio libre al final, el cual puede ser justamente ocupado
para ir almacenando los elementos a medida que van saliendo del heap (recordemos que
van apareciendo en orden decreciente).
lo cual es igual a n.
Bucketsort
Los mtodos anteriores operan mediante comparaciones de llaves, y estn sujetos, por
lo tanto, a la cota inferior O(n log n). Veremos a continuacin un mtodo que opera
de una manera distinta, y logra ordenar el conjunto en tiempo lineal.
Supongamos que queremos ordenar n nmeros, cada uno de ellos compuesto de k
dgitos decimales. El siguiente es un ejemplo con n=10, k=5.
73895
93754
82149
99046
04853
94171
54963
70471
80564
66496
Imaginando que estos dgitos forman parte de una matriz, podemos decir que a[i,j] es
el j-simo del i-simo elemento del conjunto.
Es fcil, en una pasada, ordenar el conjunto si la llave de ordenacin es un solo dgito,
por ejemplo el tercero de izquierda a derecha:
99046
82149
94171
70471
66496
80564
93754
73895
04853
54963
Este proceso se hace en una pasada sobre los datos, y toma tiempo O(n).
Para ordenar el conjunto por las llaves completas, repetimos el proceso dgito por dgito,
en cada pasada separando los elementos segn el valor del dgito respectivo, luego
recolectndolos para formar una sola cola, y realimentando el proceso con esos mismos
datos. El conjunto completo queda finalmente ordenado si los dgitos se van tomando de
derecha a izquierda (esto no es obvio!).
Como hay que realizar k pasadas y cada una de ellas toma tiempo O(n), el tiempo total
es O(k n), que es el tamao del archivo de entrada (en bytes). Por lo tanto, la
ordenacin toma tiempo lineal en el tamao de los datos.
El proceso anterior se puede generalizar para cualquier alfabeto, no slo dgitos (por
ejemplo, ASCII). Esto aumenta el nmero de colas de salida, pero no cambia
sustancialmente el tiempo que demora el programa.
Archivos con records de largo variable
Si las lneas a ordenar no son todas del mismo largo, es posible alargarlas hasta
completar el largo mximo, con lo cual el algoritmo anterior es aplicable. Pero si hay
algunas pocas lneas desproporcionadamente largas y otras muy cortas, se puede perder
mucha eficiencia.
Es posible, aunque no lo vamos a ver aqu, generalizar este algoritmo para ordenar
lneas de largo variable sin necesidad de alargarlas. El resultado es que la ordenacin se
realiza en tiempo proporcional al tamao del archivo.
Si tenemos dos archivos que ya estn ordenados, podemos mezclarlos para formar un
solo archivo ordenado en tiempo proporcional a la suma de los tamaos de los dos
archivos.
Esto se hace leyendo el primer elemento de cada archivo, copiando hacia la salida al
menor de los dos, y avanzando al siguiente elemento en el archivo respectivo. Cuando
uno de los dos archivos se termina, todos los elementos restantes del otro se copian
hacia la salida. Este proceso se denomina "mezcla", o bien "merge", por su nombre en
ingls.
Como cada elemento se copia slo una vez, y con cada comparacin se copia algn
elemento, es evidente que el costo de mezclar los dos archivos es lineal.
Si bien es posible realizar el proceso de mezcla de dos arreglos contiguos in situ, el
algoritmo es muy complicado y no resulta prctico. Por esta razn, el proceso se
implementa generalmente copiando de un archivo a otro.
Usando esta idea en forma reiterada, es posible ordenar un conjunto. Una forma de ver
esto es recursivamente, usando "dividir para reinar". El siguiente seudo-cdigo ilustra
esta idea:
mergesort(S) # retorna el conjunto S ordenado
{
if(S es vaco o tiene slo 1 elemento)
return(S);
else
{
Dividir S en dos mitades A y B;
A'=mergesort(A);
B'=mergesort(B);
return(merge(A',B'));
}
}
la cual tiene solucin O(n log n), de modo que el algoritmo resulta ser ptimo.
Esto mismo se puede implementar en forma no recursiva, agrupando los elementos de a
dos y mezclndolos para formar pares ordenados. Luego mezclamos pares para formar
cudruplas ordenadas, y as sucesivamente hasta mezclar las ltimas dos mitades y
formar el conjunto completo ordenado. Como cada "ronda" tiene costo lineal y se
realizan log n rondas, el costo total es O(n log n).
La idea de Mergesort es la base de la mayora de los mtodos de ordenamiento externo,
esto es, mtodos que ordenan conjuntos almacenados en archivos muy grandes, en
donde no es posible copiar todo el contenido del archivo a memoria para aplicar alguno
de los mtodos estudiados anteriormente.
Al igual que en los casos anteriores, el costo total es O(n log n).
Bsqueda en texto
1. Algoritmo de fuerza bruta.
2. Algoritmo Knuth-Morris-Pratt (KMP).
3. Algoritmo Boyer-Moore.
o Boyer-Moore-Horspool (BMH).
o Boyer-Moore-Sunday (BMS).
La bsqueda de patrones en un texto es un problema muy importante en la prctica. Sus
aplicaciones en computacin son variadas, como por ejemplo la bsqueda de una
palabra en un archivo de texto o problemas relacionados con biologa computacional, en
donde se requiere buscar patrones dentro de una secuencia de ADN, la cual puede ser
modelada como una secuencia de caracteres (el problema es ms complejo que lo
descrito, puesto que se requiere buscar patrones en donde ocurren alteraciones con
cierta probabilidad, esto es, la bsqueda no es exacta).
En este captulo se considerar el problema de buscar la ocurrencia de un patrn dentro
de un texto. Se utilizarn las siguientes convenciones:
Por ejemplo:
comparaciones de caracteres.
Sea X la parte del patrn que calza con el texto, e Y la correspondiente parte del texto, y
suponga que el largo de X es j. El algoritmo de fuerza bruta mueve el patrn una
posicin hacia la derecha, sin embargo, esto puede o no puede ser lo correcto en el
sentido que los primeros j-1 caracteres de X pueden o no pueden calzar los ltimos j-1
caracteres de Y.
La observacin clave que realiza el algoritmo Knuth-Morris-Pratt (en adelante KMP) es
que X es igual a Y, por lo que la pregunta planteada en el prrafo anterior puede ser
respondida mirando solamente el patrn de bsqueda, lo cual permite precalcular la
respuesta y almacenarla en una tabla.
Por lo tanto, si deslizar el patrn en una posicin no funciona, se puede intentar
deslizarlo en 2, 3, ..., hasta j posiciones.
Se define la funcin de fracaso (failure function) del patrn como:
Intuitivamente, f(j) es el largo del mayor prefijo de X que adems es sufijo de X. Note
que j = 1 es un caso especial, puesto que si hay una discrepancia en b1 el patrn se
desliza en una posicin.
Si se detecta una discrepancia entre el patrn y el texto cuando se trata de calzar bj+1, se
desliza el patrn de manera que bf(j) se encuentre donde bj se encontraba, y se intenta
calzar nuevamente.
Ejemplo:
Para esto se debe cumplir que i=f(j). Si bi+1=bj+1, entonces f(j+1)=i+1. En caso
contrario, se reemplaza i por f(i) y se verifica nuevamente la condicin.
El algoritmo resultante es el siguiente (note que es similar al algoritmo KMP):
// m es largo del patron
// los indices comienzan desde 1
int[] f=new int[m];
f[1]=0;
int j=1;
int i;
while (j<m)
{
i=f[j];
while (i>0 && patron[i+1]!=patron[j+1])
{
i=f[i];
}
if (patron[i+1]==patron[j+1])
{
f[j+1]=i+1;
}
else
{
f[j+1]=0;
}
j++;
}
El tiempo de ejecucin para calcular la funcin de fracaso puede ser acotado por los
incrementos y decrementos de la variable i, que es
.
Por lo tanto, el tiempo total de ejecucin del algoritmo, incluyendo el preprocesamiento
.
del patrn, es
Algoritmo Boyer-Moore
Hasta el momento, los algoritmos de bsqueda en texto siempre comparan el patrn con
el texto de izquierda a derecha. Sin embargo, suponga que la comparacin ahora se
realiza de derecha a izquierda: si hay una discrepancia en el ltimo carcter del patrn y
el carcter del texto no aparece en todo el patrn, entonces ste se puede deslizar m
posiciones sin realizar niguna comparacin extra. En particular, no fue necesario
comparar los primeros m-1 caracteres del texto, lo cual indica que podra realizarse una
bsqueda en el texto con menos de n comparaciones; sin embargo, si el carcter
discrepante del texto se encuentra dentro del patrn, ste podra desplazarse en un
nmero menor de espacios.
El mtodo descrito es la base del algoritmo Boyer-Moore, del cual se estudiarn dos
variantes: Horspool y Sunday.
Boyer-Moore-Horspool (BMH)
El algoritmo BMH compara el patrn con el texto de derecha a izquierda, y se detiene
cuando se encuentra una discrepancia con el texto. Cuando esto sucede, se desliza el
patrn de manera que la letra del texto que estaba alineada con bm, denominada c, ahora
se alinie con algn bj, con j<m, si dicho calce es posible, o con b0, un carcter ficticio a
la izquierda de b1, en caso contrario (este es el mejor caso del algoritmo).
Para determinar el desplazamiento del patrn se define la funcin siguiente como:
Esta funcin slo depende del patrn y se puede precalcular antes de realizar la
bsqueda.
El algoritmo de bsqueda es el siguiente:
// m es el largo del patron
// los indices comienzan desde 1
int k=m;
int j=m;
while(k<=n && j>=1)
{
if (texto[k-(m-j)]==patron[j])
{
j--;
}
else
{
k=k+(m-siguiente(a[k]));
j=m;
}
}
// j==0 => calce!, j>=0 => no hubo calce.
Se puede demostrar que el tiempo promedio que toma el algoritmo BMH es:
En el peor caso, BMH tiene el mismo tiempo de ejecucin que el algoritmo de fuerza
bruta.
Boyer-Moore-Sunday (BMS)
El algoritmo BMH desliza el patrn basado en el smbolo del texto que corresponde a la
posicin del ltimo carcter del patrn. Este siempre se desliza al menos una posicin si
se encuentra una discrepancia con el texto.
Es fcil ver que si se utiliza el carcter una posicin ms adelante en el texto como
entrada de la funcin siguiente el algoritmo tambin funciona, pero en este caso es
necesario considerar el patrn completo al momento de calcular los valores de la
funcin siguiente. Esta variante del algoritmo es conocida como Boyer-Moore-Sunday
(BMS).
Es posible generalizar el argumento, es decir, se pueden utilizar caracteres ms
adelante en el texto como entrada de la funcin siguiente? La respuesta es no, dado que
en ese caso puede ocurrir que se salte un calce en el texto.
Grafos
1.
2.
3.
4.
Definiciones Bsicas
Recorridos de Grafos
rbol Cobertor Mnimo
Distancias Mnimas en un Grafo Dirigido
Definiciones Bsicas
Un grafo consiste de un conjunto V de vrtices (o nodos) y un conjunto E de arcos que
conectan a esos vrtices.
Ejemplos:
V = {v1,v2,v3,v4,v5}
E = { {v1,v2}, {v1,v3}, {v1,v5},
{v2,v3}, {v3,v4}, {v4,v5} }
V = {v1,v2,v3,v4}
E = { (v1,v2), (v2,v2), (v2,v3),
(v3,v1), (v3,v4), (v4,v3) }
Adems de esto, los grafos pueden ser extendidos mediante la adicin de rtulos
(labels) a los arcos. Estos rtulos pueden representar costos, longitudes, distancias,
pesos, etc.
Representaciones de grafos en memoria
Matriz de adyacencia
Un grafo se puede representar mediante una matriz A tal que A[i,j]=1 si hay un arco
que conecta vi con vj, y 0 si no. La matriz de adyacencia de un grafo no dirigido es
simtrica.
Una matriz de adyacencia permite determinar si dos vrtices estn conectados o no en
tiempo constante, pero requieren O(n2) bits de memoria. Esto puede ser demasiado para
muchos grafos que aparecen en aplicaciones reales, en donde |E|<<n2. Otro problema
es que se requiere tiempo O(n) para encontrar la lista de vecinos de un vrtice dado.
Listas de adyacencia
Esta representacin consiste en almacenar, para cada nodo, la lista de los nodos
adyacentes a l. Para el segundo ejemplo anterior,
v1:
v2:
v3:
v4:
v2
v2, v3
v1, v4
v3
Esto utiliza espacio O(|E|) y permite acceso eficiente a los vecinos, pero no hay acceso
al azar a los arcos.
Caminos, ciclos y rboles
Un camino es una secuencia de arcos en que el extremo final de cada arco coincide con
el extremo inicial del siguiente en la secuencia.
Un grafo es conexo si desde cualquier vrtice existe un camino hasta cualquier otro
vrtice del grafo.
Recorridos de Grafos
En muchas aplicaciones es necesario visitar todos los vrtices del grafo a partir de un
nodo dado. Algunas aplicaciones son:
Encontrar ciclos
Encontrar componentes conexas
Encontrar rboles cobertores
Si hubiera ms de una componente conexa, esto no llegara a todos los nodos. Para esto
podemos hacer:
n=0;
ncc=0; // nmero de componentes conexas
for(todo w)
DFN[w]=0;
while(existe v en V con DFN[v]==0)
{
++ncc;
DFS(v);
}
Todo recorrido de un grafo conexo genera un rbol cobertor, consistente del conjunto de
los arcos utilizados para llegar por primera vez a cada nodo.
Para un grafo dado pueden existir muchos rboles cobertores. Si introducimos un
concepto de "peso" (o "costo") sobre los arcos, es interesante tratar de encontrar un
rbol cobertor que tenga costo mnimo.
Find(x):
El tiempo que demora este algoritmo est dominado por lo que demora la ordenacin de
los arcos. Si |V|=n y |E|=m, el tiempo es O(m log m) ms lo que demora realizar m
operaciones Find ms n operaciones Union.
Es posible implementar Union-Find de modo que las operaciones Union demoran
tiempo constante, y las operaciones Find un tiempo casi constante. Ms precisamente, el
costo amortizado de un Find est acotado por log* n, donde log* n es una funcin
definida como el nmero de veces que es necesario tomar el logaritmo de un nmero
para que el resultado sea menor que 1.
Por lo tanto, el costo total es O(m log m) o, lo que es lo mismo, O(m log n) (por
qu?).
Ejemplo:
Para implementar este algoritmo eficientemente, podemos mantener una tabla donde,
para cada nodo de V-A, almacenamos el costo del arco ms barato que lo conecta al
conjunto A. Estos costos pueden cambiar en cada iteracin.
Si se organiza la tabla como una cola de prioridad, el tiempo total es O(m log n). Si se
deja la tabla desordenada y se busca linealmente en cada iteracin, el costo es O(n2).
Esto ltimo es mejor que lo anterior si el grafo es denso, pero no si est cerca de ser un
grafo completo.
cambiando cada vez de nodo origen, pero puede ser ms eficiente encontrar
todos los caminos de una sola vez.
Algoritmo de Dijkstra para los caminos ms cortos desde un nodo "origen"
La idea del algoritmo es mantener un conjunto A de nodos "alcanzables" desde el nodo
origen e ir extendiendo este conjunto en cada iteracin.
Los nodos alcanzables son aquellos para los cuales ya se ha encontrado su camino
ptimo desde el nodo origen. Para esos nodos su distancia ptima al origen es conocida.
Inicialmente A={s}.
Para los nodos que no estn en A se puede conocer el camino ptimo desde s que pasa
slo por nodos de A. Esto es, caminos en que todos los nodos intermedios son nodos de
A. Llamemos a esto su camino ptimo tentativo.
En cada iteracin, el algoritmo encuentra el nodo que no est en A y cuyo camino
ptimo tentativo tiene largo mnimo. Este nodo se agrega a A y su camino ptimo
tentativo se convierte en su camino ptimo. Luego se actualizan los caminos ptimos
tentativos para los dems nodos.
El algoritmo es el siguiente:
A={s};
D[s]=0;
D[v]=cost(s,v) para todo v en V-A; // infinito si el arco no
existe
while(A!=V)
{
Encontrar v en V-A tal que D[v] es mnimo;
Agregar v a A;
for(todo w tal que (v,w) est en E)
D[w]=min(D[w],D[v]+cost(v,w));
}
Implementaciones:
Usando una cola de prioridad para la tabla D el tiempo es O(m log n).
Usando un arreglo con bsqueda secuencial del mnimo el tiempo es O(n2).
Ejemplo:
or 0 1
0 01
1 11
min
infinito <infinito
and 0 1
00
01
Algoritmos Probabilsticos
En muchos casos, al introducir elecciones aleatorias en un algoritmo se pueden obtener
mejores rendimientos que al aplicar el algoritmo determinstico puro.
Un algoritmo tipo Montecarlo asegura un tiempo fijo de ejecucin, pero no est
garantizado que la respuesta sea correcta, aunque lo puede ser con alta probabilidad.
Un algoritmo tipo Las Vegas siempre entrega la respuesta correcta, pero no garantiza el
tiempo total de ejecucin, aunque con alta probabilidad ste ser bajo.
Ejemplo: algoritmo tipo Montecarlo para verificar la multiplicacin de dos
matrices.
Sean A, B, C matrices de N x N. Se desea chequear si
esto toma tiempo O(n3) usando el algoritmo estndar.
Probabilsticamente, se puede chequear si
probabilidad de error. El algoritmo es el siguiente:
. Determinsticamente,
Sea
. Si
para algn i, j, entonces
donde x' es el vector donde se cambia xj por -xj. Por lo tanto, en cada iteracin del
algoritmo se tiene que
que
while (true)
{
Colorear los elementos aleatoriamente;
if (ningn Ci es homogeneo)
break;
}