You are on page 1of 342

Escuela Polit cnica Superior de Alcoy e

Ingeniera T cnica en Inform tica de e a Gesti n o

Estructuras de Datos y Algoritmos

Apuntes Teora
Francisco Nevado, Jordi Linares

Indice general
1. Tipos Abstractos de Datos 1.1. Introducci n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . o 1.2. Ejemplo: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Gesti n Din mica de Memoria. Punteros o a 2.1. Gesti n din mica de memoria. . . . . o a 2.1.1. Variables est ticas . . . . . . a 2.2. Punteros . . . . . . . . . . . . . . . . 2.2.1. Punteros y vectores . . . . . . 2.3. Variables din micas . . . . . . . . . . a 2.4. Ejercicios . . . . . . . . . . . . . . . 5 5 6 8 8 8 9 11 13 19 20 20 20 21 22 24 26 27 28 32 34 34 35 38 42 46

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

3. Estructuras de Datos Lineales 3.1. Introducci n . . . . . . . . . . . . . . . . . . . . . . . . . . . . o 3.2. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1. Operaciones sobre pilas . . . . . . . . . . . . . . . . . 3.2.2. Representaci n vectorial de pilas . . . . . . . . . . . . . o 3.2.3. Representaci n enlazada de pilas con variable din mica o a 3.3. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1. Operaciones sobre colas . . . . . . . . . . . . . . . . . 3.3.2. Representaci n vectorial de colas . . . . . . . . . . . . o 3.3.3. Representaci n enlazada de colas con variable din mica o a 3.4. Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1. Operaciones sobre listas . . . . . . . . . . . . . . . . . 3.4.2. Representaci n vectorial de listas . . . . . . . . . . . . o 3.4.3. Representaci n enlazada de listas con variable din mica o a 3.4.4. Representaci n enlazada de listas con variable est tica . o a 3.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . .

4. Divide y vencer s a 4.1. Esquema general de Divide y Vencer s . . . . . a 4.2. Algoritmos de ordenaci n . . . . . . . . . . . . . . o 4.2.1. Inserci n directa . . . . . . . . . . . . . . o 4.2.2. Selecci n directa . . . . . . . . . . . . . . o 4.2.3. Ordenaci n por mezcla o fusi n: Mergesort o o 4.2.4. Algoritmo por partici n: Quicksort . . . . o 4.2.5. Comparaci n emprica de los algoritmos . o 4.3. B squeda del k- simo menor elemento . . . . . . . u e 4.3.1. An lisis de la eciencia . . . . . . . . . . a 4.4. B squeda binaria . . . . . . . . . . . . . . . . . . u 4.4.1. Eciencia del algoritmo . . . . . . . . . . 4.5. C lculo de la potencia de un n mero . . . . . . . . a u 4.5.1. Algoritmo trivial . . . . . . . . . . . . . . 4.5.2. Algoritmo divide y vencer s . . . . . . . a 4.6. Otros problemas . . . . . . . . . . . . . . . . . . . 4.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

67 67 68 68 72 74 82 93 94 95 98 99 101 101 102 103 104 114 114 116 117 117 119 122 122 125 128 129 131 140 140 140 140 141 141 143 143 144

5. Arboles 5.1. Deniciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2. Recorrido de arboles . . . . . . . . . . . . . . . . . . . . . . . . 5.3. Representaci n de arboles . . . . . . . . . . . . . . . . . . . . . o 5.3.1. Representaci n mediante listas de hijos . . . . . . . . . . o 5.3.2. Representaci n hijo m s a la izquierda hermano dereo a cho . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4. Arboles binarios . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4.1. Representaci n de arboles binarios . . . . . . . . . . . . . o 5.4.2. Recorrido de arboles binarios . . . . . . . . . . . . . . . 5.4.3. Arbol binario completo. Representaci n. . . . . . . . . . o 5.4.4. Propiedades de los arboles binarios . . . . . . . . . . . . 5.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6. Representaci n de Conjuntos o 6.1. Conceptos generales . . . . . . . . . . . . . . . 6.1.1. Representaci n de conjuntos . . . . . . . o 6.1.2. Notaci n . . . . . . . . . . . . . . . . . o 6.1.3. Operaciones elementales sobre conjuntos 6.1.4. Conjuntos din micos . . . . . . . . . . . a 6.2. Tablas de dispersi n o tablas Hash . . . . . . . . o 6.2.1. Tablas de direccionamiento directo . . . 6.2.2. Tablas de dispersi n o tablas Hash . . . . o 2

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

6.3. Arboles binarios de b squeda . . . . . . . . . . . . . . . . . . . . u 6.3.1. Representaci n de arboles binarios de b squeda . . . . . . o u 6.3.2. Altura m xima y mnima de un arbol binario de b squeda a u 6.3.3. Recorrido de arboles binarios de b squeda . . . . . . . . u 6.3.4. B squeda de un elemento en un arbol binario de b squeda u u 6.3.5. B squeda del elemento mnimo y del elemento m ximo . u a 6.3.6. Inserci n de un elemento en un arbol binario de b squeda o u 6.3.7. Borrado de un elemento en un arbol binario de b squeda . u 6.4. Montculos (Heaps). Colas de prioridad. . . . . . . . . . . . . . . 6.4.1. Manteniendo la propiedad de montculo . . . . . . . . . . 6.4.2. Construir un montculo . . . . . . . . . . . . . . . . . . . 6.4.3. Algoritmo de ordenaci n Heapsort . . . . . . . . . . . . o 6.4.4. Colas de prioridad . . . . . . . . . . . . . . . . . . . . . 6.5. Estructura de datos para conjuntos disjuntos: MF-set . . . . . . . 6.5.1. Representaci n de MF-sets . . . . . . . . . . . . . . . . . o 6.5.2. Operaciones sobre MF-sets . . . . . . . . . . . . . . . . . 6.6. Otras Estructuras de Datos para Conjuntos . . . . . . . . . . . . . 6.6.1. Tries . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.2. Arboles Balanceados . . . . . . . . . . . . . . . . . . . . 6.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.7.1. Tablas de dispersi n . . . . . . . . . . . . . . . . . . . . o 6.7.2. Arboles binarios de b squeda . . . . . . . . . . . . . . . u 6.7.3. Montculos (Heaps) . . . . . . . . . . . . . . . . . . . . . 6.7.4. MF-sets . . . . . . . . . . . . . . . . . . . . . . . . . . . 7. Grafos 7.1. Deniciones . . . . . . . . . . . . . . . . 7.2. Introducci n a la teora de grafos . . . . . o 7.3. Representaci n de grafos . . . . . . . . . o 7.3.1. Listas de adyacencia . . . . . . . 7.3.2. Matriz de adyacencia . . . . . . . 7.4. Recorrido de grafos . . . . . . . . . . . . 7.4.1. Recorrido primero en profundidad 7.4.2. Recorrido primero en anchura . . 7.4.3. Ordenaci n topol gica . . . . . . o o 7.5. Caminos de mnimo peso: algoritmo de Dijkstra . . . . . . . . . . . . . . . . . . 7.5.1. Caminos de mnimo peso . . . . . 7.5.2. Algoritmo de Dijkstra . . . . . . 7.6. Arbol de expansi n de coste mnimo . . . o o 7.6.1. Arbol de expansi n . . . . . . . . 3

158 159 161 161 162 165 167 170 180 181 186 192 198 203 204 204 209 209 211 216 216 221 227 232 234 234 238 241 241 243 245 245 249 250 253 253 254 260 260

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

7.6.2. Algoritmo de Kruskal . . . . . . . . . . . . . . . . . . . 261 7.6.3. Algoritmo de Prim . . . . . . . . . . . . . . . . . . . . . 266 7.7. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 8. Algoritmos Voraces 8.1. Introducci n . . . . . . . . . . . . . . . . . . . o 8.1.1. Ejemplo: Cajero autom tico . . . . . . a 8.2. Esquema general Voraz . . . . . . . . . . . . . 8.3. El problema de la compresi n de cheros . . . o 8.4. El problema de la mochila con fraccionamiento 8.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . Ex menes de la asignatura a 279 279 280 280 282 287 291 294

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

Tema 1 Tipos Abstractos de Datos


1.1. Introducci n o

Entendemos como Tipo de Datos (TD) el conjunto de valores que una variable puede tomar. En otras palabras podramos decir que un TD es la clase a la que pertenece una variable (p.e. un n mero que pertenezca a los enteros es de tipo u entero). Aunque a nivel l gico los TDs se representen de diferentes maneras (n meros o u enteros, caracteres, etc) a nivel interno el computador los tratar como agrupaa ciones de bits. La necesidad de utilizar TDs viene fundamentada por dos razones: Que el compilador pueda elegir la representaci n interna optima para una o variable. Poder aprovechar las caractersticas de un TD, por ejemplo, las operaciones aritm ticas de los n meros enteros. e u Los lenguajes de programaci n suelen admitir cuatro tipos distintos de TD: eno teros, reales, booleanos y caracteres. A estos TDs se les conoce como TD simples, elementales o primitivos. Cada uno de estos tipos tiene asociadas operaciones simples, como puede ser la multiplicaci n de enteros, etc. o Pero, qu ocurrira si en uno de nuestros programas quisieramos trabajar con e matrices de n meros? Como no son un TD simple, esto nos induce a pensar que u necesitaramos denir un TD matriz y tambi n c mo se llevan a cabo las opera e o ciones asociadas a ese tipo, como la multiplicaci n de matrices, por ejemplo. o Con esto llegamos al concepto de Tipo Abstracto de Datos (TAD o TDA1 ). Un TAD es un modelo matem tico con una serie de operaciones denidas en ese a modelo (se suelen conocer como los procedimientos u operaciones de ese TAD).
1

de Tipo de Datos Abstracto, como se encuentra a veces en la literatura.

Un TAD es una generalizaci n de los TD simples y los procedimientos son o generalizaciones de operaciones primitivas. Uno de los aspectos m s importantes de los TADs es su capacidad de encapa sulaci n o abstracci n que nos permite localizar la denici n del TAD y sus o o o operaciones asociadas en un lugar determinado del programa. Pudiendo denir libreras especcas para un determinado TAD. Esta caracterstica permite cambiar la implementaci n seg n la adecuaci n al problema, as como m s facilidades en o u o a la depuraci n de c digo y mayor orden y estructuraci n en los programas. Por o o o ejemplo, podramos denir un TAD matriz, pero posteriormente elegir entre una implementaci n tradicional de matrices o utilizar estructuras especiales para mao trices dispersas, en el caso de que fueran m s utiles para nuestro problema. a Realizar la implementaci n de un TAD signica traducir en instrucciones o de un lenguaje de programaci n la declaraci n que dene a una variable como o o perteneciente a ese tipo, adem s de un procedimiento en ese lenguaje por cada a operaci n denida para el TAD. o Una implementaci n de un TAD elige una estructura de datos (ED) para repo resentar el TAD; cada ED se construye a partir de TDs simples del lenguaje, usando los dispositivos de estructuraci n disponibles en el lenguaje de programaci n o o utilizado como pueden ser los vectores o registros.

1.2.

Ejemplo:
En primer lugar estara la denici n del tipo: o M ATRIZ DE E NTEROS es una agrupaci n de n meros enteros ordenados o u por las y por columnas. El n mero de elementos en cada la es el mismo. u Tambi n el n mero de elementos en cada columna es el mismo. Un n mero e u u entero que forme parte como elemento de la matriz se identica por su n mero de la y n mero de columna. u u Representaci n l gica: o o 3 7 9 M = 4 5 6 2 3 5 y tenemos que M [2, 1] = 4. La operaciones denidas para el TAD matriz son: Suma Mat(A,B: M ATRIZ) devuelve C: M ATRIZ suma dos matrices que tienen igual n mero de las y de columnas. u 6

Vemos como podra denirse un TAD Matriz de Enteros:

La suma se realiza sumando elemento a elemento cada n mero entero u de la matriz A con el correspondiente n mero entero de la matriz B u que tenga igual n mero de la e igual n mero de columna. u u Multiplicar Mat(A,B: M ATRIZ) devuelve C: M ATRIZ multiplica dos matrices que cumplen la condici n de que el n mero o u de columnas de la matriz A coincide con el n mero de las de la matriz u B. La multiplicaci n se lleva a cabo . . . . o Inversa Mat(A: M ATRIZ) devuelve C: M ATRIZ en el caso de que la matriz A posea inversa, esta se calcula . . . . Con esta denici n el TAD Matriz de Enteros se podra implementar de esta o manera en lenguaje C: #dene NUM_FILAS 10 #dene NUM_COLUMNAS 10 /* Definimos el TAD */ typedef int t_matriz[NUM_FILAS][NUM_COLUMNAS]; /* Definimos una variable */ t_matriz M; y la operaci n Suma_Mat() sera: o void Suma_Mat(t_matriz A, t_matriz B, t_matriz C) { int i,j; for (i=0;i<NUM_FILAS;i++) for (j=0;j<NUM_COLUMNAS;j++) C[i][j] = A[i][j] + B[i][j]; }

Tema 2 Gesti n Din mica de Memoria. o a Punteros


2.1. Gesti n din mica de memoria. o a

Para poder comenzar este tema, se espera que el alumno conozca los distintos tipos b sicos de variables que permiten denir la mayora de los lenguajes de a programaci n. o Durante este tema veremos la diferencia entre memoria est tica y memoria a din mica, adem s discutiremos la necesidad de utilizar esta ultima y como gesa a tionarla. Veremos tambi n varios ejemplos para explicar c mo se dene memoria e o din mica y como se maneja con el lenguaje de programaci n C. a o

2.1.1.

Variables est ticas a

Supongamos que tenemos un programa en el cual hemos denido varias vari ables. Cuando ejecutamos el programa, este se carga en memoria y reserva espacio para las variables que tiene denidas. En ese espacio se guardar n los valores que a se asignen a las variables, adem s ese espacio de memoria que ha reservado es el a mismo desde que se lanza a ejecuci n el programa hasta que se termina. o Cuando en la codicaci n de un programa declaramos una variable, denimos o a su vez de qu tipo es. El proceso de compilaci n determinar despu s el tama o e o a e n de memoria que va a ocupar esa variable en el programa ejecutable. Por ejemplo, si tenemos la denici n: o int x; Cuando comience la ejecuci n se reservar un espacio de memoria que poo a dr almacenar un entero y al cual se podr acceder con la etiqueta x. a a

x A la memoria consumida de esta manera, esto es, para variables declaradas en tiempo de compilaci n, la llamaremos memoria est tica. A las variables que o a consuman memoria est tica las llamaremos variables est ticas. a a As pues, usando la denici n del ejemplo anterior, son v lidas las siguientes o a operaciones: En el c digo o x=5; x=x+2; En ejecuci n o x x 5 7

Para variables que constituyan estructuras m s complejas, como los registros a o vectores, la situaci n es la misma: o En el c digo o int v[3]; struct {oat r,i;} im; En ejecuci n o v im Instrucci n v lida o a v[0]=1; im.r=5.0; im.i=3.0;

2.2.

Punteros

Un mecanismo que muchos lenguajes de programaci n ofrecen es poder aco ceder al espacio de memoria ocupado por una variable por medio de su direcci n o de memoria y no por el nombre de la variable (su identicador o etiqueta), as, por ejemplo, tambi n podramos modicar variables en memoria sin hacer referencia e a la variable en s. Este mecanismo suele implementarse en los lenguajes de programaci n por o medio de lo que se conoce como punteros. Vamos a comprender el concepto de puntero mediante un ejemplo. Si tenemos la denici n: o int *px;

Estamos deniendo la variable px como un puntero a entero y no como entero. px puede contener una direcci n a otra zona de memoria que s que puede cono tener un entero. B sicamente, px es una variable que puede guardar la direcci n a o de memoria de otra variable. Para denir un puntero en lenguaje C, debe utilizarse la siguiente sintaxis: tipo_datos *nombre_variable_puntero; Donde el operador * indica que la variable nombre_variable_puntero es un puntero a otra variable de tipo tipo_datos. Un puntero puede tomar un valor especial NULL para indicar que no apunta a ning n lugar accesible. u As pues, px es una variable, pero no podr tener valores como la x, es decir, a no podr contener un 2 o un 5, sino que contendr s lo direcciones de memoria. a a o Normalmente, a nivel gr co representaremos una direcci n de memoria con una a o echa que apunta a una zona de memoria determinada o con un smbolo que represente que el puntero tiene un valor NULL.

px

px = NULL

o px = NULL

Figura 2.1: Representaci n gr ca de punteros. o a

Si queremos acceder al contenido de la posici n de memoria a la que se ala o n px, tenemos que hacerlo utilizando la expresi n *px, adem s, hay que tener en o a cuenta que se accede a una variable del tipo para la cual se deni el puntero, en o este caso un entero. Adem s del mecanismo *px para acceder a la zona de memoria donde apunta a px, en lenguaje C se dispone del operador & que permite recuperar la direcci n o de una variable que ya existe en memoria. Este nuevo operador se utilizar para a poder obtener la direcci n de memoria de una variable y asignarla a un puntero. o Vemos a continuaci n algunos ejemplos de como denir y trabajar con puno teros en lenguaje C:

10

En el c digo o

En ejecuci n o x 1

Comentario px est indenido; x contiene un 1. a

int *px, x=1; px

px=&x; *px=2; px=1; px=NULL; *px=1;

px px px px px

E1 E2 E2

x x x

En px guardamos la direcci n de x. o Modicamos x a trav s de px. e ERROR: no se puede asignar un entero a px. px toma valor NULL. ERROR: px no apunta a un lugar accesible.

2 x 2 x

2.2.1.

Punteros y vectores

Y si hacemos que un puntero apunte a una variable de tipo vector en vez de a una variable simple como un entero? Entonces obtenemos una caracterstica interesante... Una caracterstica importante de los punteros en C es que pueden apuntar a variables con una estructura m s compleja, como un vector o un registro (struct). a Adem s un puntero permite apuntar dentro de esas estructuras a cualquier a posici n cuyo tipo de datos se corresponda con el tipo al que apunta el puntero. o De esa manera si denimos una variable puntero a entero, px, y un vector de enteros, v[3], son posibles operaciones como las siguientes:

11

En el c digo o int *px, v[3];

En ejecuci n o px v

Comentario px y los elementos de v est n ina denidos. px apunta a la primera posici n del o vector. La primera posici n del vector se o modica a trav s de px. e px apunta a la segunda posici n del o vector. ?

px=&v[0];

px

v


*px=1;

px

v 1


px=px+1;

px

v 1


v[1]=v[0]+*(px-1); px

v 1

2


El resultado de la ultima instrucci n v[1]=v[0]+*(px-1); es asignar un o 2 a la segunda posici n del vector v debido a que asignamos a v[1] el valor o resultante de la expresi n v[0]+*(px-1). Interpretando esta ultima expresi n, o o tendremos que v[0] tiene un valor 1, mientras que *(px-1) se reere al contenido de la posici n de memoria se alada por (px-1), que al ser px un puno n tero a un entero indica la posici n del vector de enteros anterior a la posici n o o a la que apunta px, esto es, el contenido de la posici n 0 del vector; as pues, o v[1]=1+1;. Profundizamos ahora en la relaci n entre punteros y vectores en lenguaje C o que se va a traducir en poder manejar las mismas estructuras de memoria mediante vectores o punteros obteniendo los mismos resultados y utilizando un mecanismo u otro segun convenga para la resoluci n del problema. o Si denimos un vector: int v[3]; en lenguaje C podemos interpretar esa denici n de la siguiente manera: o v La variable v se puede ver como un puntero a una o m s posiciones de memoria a consecutivas. Aunque de aqu en adelante podamos pensar en los vectores de esta manera, seguiremos representando gr camente los vectores como antes, y los a punteros con variables que apuntan a otras variables. De esa manera son posibles operaciones como las siguientes: 12


En el c digo o

En ejecuci n o v

Comentario px y los elementos de v est n indenidos. a Por comodidad representamos v como antes. px apunta a la primera posici n del veco tor. La primera posici n de v se modica a o trav s de px. e

int *px, v[3]; px

px=v;

px

v


px[0]=1;

px

v 1


La forma de acceso al vector utilizando la expresi n px[0] = 1; es equivo alente a usar la expresi n *px = 1;. o

2.3.

Variables din micas a

Ahora bien, si en un programa tuvieramos que crear temporalmente una gran cantidad de informaci n para realizar ciertas operaciones con ella y no volverla o a usar durante el resto del programa, d nde almacenaramos esa informaci n? o o qu pasara si ni tan siquiera conocieramos la cantidad de memoria que se necee sita para guardar esa informaci n? c mo almacenaramos esa informaci n? o o o Parece interesante pensar en un mecanismo de uso de memoria diferente al de declarar variables est ticas, ya que estas variables deberan ser lo sucientea mente grandes para poder almacenar toda la informaci n que podamos necesitar o y adem s ese espacio de memoria reservada tan grande s lo se usara cuando se a o opere con esos datos, pero durante el resto de la ejecuci n del programa estara o consumiendo memoria in tilmente. u Los lenguajes de programaci n han de tener una facilidad para reservar espao cio en memoria y liberarlo, por ejemplo, para poder almacenar grandes cantidades de informaci n temporales. o A la memoria que se reserva y libera durante la ejecuci n de un programa o la llamamos memoria din mica. A las variables que utilizaremos para acceder a a esas zonas de memoria din mica las llamaremos variables din micas. Durante la a a codicaci n y compilaci n se dene el tipo de una variable din mica. Durante la o o a ejecuci n del programa se reserva la cantidad de memoria necesaria. o Vemos ahora c mo crear y gestionar memoria din mica, es decir, reservar o a espacio en memoria durante la ejecuci n de un programa y utilizar ese espacio. o 13

Para ello, ser necesario usar las siguientes funciones del lenguaje C: a Para reserva de memoria: las funciones malloc y calloc permiten reservar memoria din micamente. La funci n malloc no inicializa la memoria a o reservada, mientras que calloc la inicializa a 0 (introduce el valor 0 en los bytes de memoria reservada). Para liberaci n de memoria: la funci n free permite liberar memoria preo o viamente reservada. Para calcular la talla de un tipo de datos: sizeof devuelve el n mero de u bytes que necesita un tipo. La sintaxis de malloc y calloc es: void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); La funci n malloc recibe como par metro el n mero de bytes de memoria o a u que queremos reservar y devuelve un puntero sin tipo que apunta al principio de la zona de memoria reservada.. La funci n calloc recibe como par metros el n mero de elementos que o a u queremos reservar y el n mero de bytes de cada elemento y devuelve un puntero u sin tipo que apunta al principio de la zona de memoria reservada. La sintaxis para la funci n de liberaci n de memoria reservada, free, es: o o void free(void *ptr); Esta funci n recibe como par metro un puntero al principio de una zona v lida o a a de memoria que habr sido reservada previamente, y libera dicha memoria para a que pueda ser utilizada por otras reservas de memoria que se quieran realizar, etc. La sintaxis de la funci n sizeof es: o sizeof(type); La funci n recibe como par metro un tipo de datos, tanto de los tipos origio a nales del lenguaje C (int, oat, etc.), como de los tipos de datos denidos por el usuario. A continuaci n vemos varios ejemplos de la denici n y uso de memoria o o din mica utilizando las funciones descritas. a Si suponemos una denici n como la siguiente: o int *v; Podramos realizar las siguientes instrucciones:

14

En el c digo o

Comentario

v=(int *)malloc(n*sizeof(int )); Reservamos un vector de n enteros. Forzamos a la funci n malloc a devolver un puntero a o entero que se guarda en v. v[0]=1; free(v); Indexamos el vector de la manera habitual. Liberamos la memoria previamente reservada. La informaci n almacenada es a partir de ahoo ra inaccesible. ERROR: v ya no apunta a una zona v lida. a

v[0]=1;

Como se puede observar en el ejemplo anterior, en la llamada a la funci n malo loc para reservar memoria para estructuras vectoriales, se utilizar como par metro a a normalmente el resultado de una multiplicaci n donde los t rminos ser n por un o e a lado el n mero de elementos del vector y por otro el tama o en bytes de cada uno u n de los elementos del vector. Sin embargo, no se debe confundir el operador * de multiplicaci n con el operador * de indirecci n. o o Si sabemos que int *x; permite denir tanto un puntero a entero como un vector de enteros, entonces, qu permitir denir int **x;?. Esta expresi n se e a o usar para denir un vector de punteros a enteros, con lo que tambi n podr verse a e a como un vector de vectores de enteros, o lo que es lo mismo, una matriz de enteros. Si tenemos las siguientes deniciones: int **v,*pv,i,j; /* v es un puntero a uno o mas */ /* punteros a enteros. */ El siguiente segmento de c digo permitira denir una matriz cuadrada de o enteros: /* Definimos un vector de punteros. */ v=(int **)malloc(n*sizeof(int *)); for (i=0; i<n; i++) { /* Definimos un vector de enteros. */ v[i]=(int *)malloc(n*sizeof(int )); for (j=0; j<n; j++) /* Inicializamos cada fila. */ v[i][j]=0; } El siguiente segmento de c digo tendra el mismo efecto que el anterior, es o decir, denira una matriz cuadrada de enteros: 15

/* Definimos un vector de punteros. */ v=(int **)malloc(n*sizeof(int *)); /* Definimos un vector de enteros. */ pv=(int *)malloc(n*n*sizeof(int )); for (i=0; i<n*n; i++) /* Inicializamos la matriz. */ pv[i]=0; for (i=0; i<n; i++) /* Cada componente de v apunta a n */ v[i]=&pv[i*n]; /* posiciones consecutivas de pv. */ Qu haramos para denir una matriz no cuadrada, de tama o n*m? La solue n ci n a este problema sera cambiar una de las ns de las reservas de memoria y o bucles vistos arriba. Vemos m s ejemplos de manejo de punteros y memoria din mica. Sabiendo a a que para denir nuevos tipos de datos en lenguaje C utilizamos la construcci n o typedef definicion nombre_tipo; Podemos considerar las siguientes deniciones: typedef struct { int a, b; } tupla; tupla *t, **tt; /* tt es un vector de apuntadores a tupla. */ int *p,i; Hay que recordar que en lenguaje C cuando se accede a un elemento de una struct mediante un puntero se tiene que utilizar el operador -> en vez del . para acceder a los diferentes campos del registro. Entonces podemos ejecutar las siguientes instrucciones:

16

En el c digo o

Comentario

t=(tupla *)malloc(sizeof(tupla)); Reservamos un registro. Forzamos a la funci n malloc a devolver un puntero al o tipo deseado. t->a=0; p=&t->b; Modicamos un campo del registro. Asignamos a p la direcci n de un camo po del registro, que es una zona v lida de a memoria. Modicamos un campo del registro. Liberamos la memoria previamente reservada. ERROR: p ya no apunta a una zona v lia da.

*p=0; free(t);

*p=0;

Si consideramos las mismas deniciones de tipos y variables que para el ejemplo anterior, vamos a construir e inicializar una estructura de datos un poco m s a compleja con las siguientes instrucciones: En el c digo o Comentario

tt=(tupla**)malloc(n*sizeof(tupla*)); Reservamos un vector de punteros a tupla. t=(tupla*)malloc(n*sizeof(tupla)); for (i=0;i<n;i++) { tt[i]=&t[i]; tt[i]->a=0; tt[i]->b=0; } Reservamos un vector de tuplas. Despu s enlazamos cada elemento e de tt con un elemento de t.

La estructura que se crea e inicializa con las anteriores instrucciones se muestra en la gura 2.2.

17

tt
0 1 2

t (0,0) (0,0) (0,0)


0 1

(0,0)

Figura 2.2: Representaci n gr ca de la estructura denida en el ejemplo. o a

Paso de vectores a funciones Si deseamos pasar como argumento un vector a una funci n o procedimiento o en lenguaje C, entonces tendremos que pasarlo mediante un puntero a un vector, debido a que realizar una copia local del vector cada vez que se llame a la funci n resulta demasiado costoso. A nivel esquem tico, podemos ver en el siguiente o a pedazo de c digo c mo se pasara un vector a un procedimiento: o o void f(int *v) { ... } ... int main() { int p[n]; ... f(p); ... } Esta obligaci n de pasar los vectores a las funciones o procedimientos meo diante un puntero implicar que el vector se pasa por referencia, esto es, que a u cualquier modicaci n que se haga en el vector quedar reejada en el a n deo a spu s de acabar la funci n. Debe prestarse atenci n a las manipulaciones de vece o o tores en las funciones. Si no queremos que las modicaciones dejen alterado el vector original una vez haya terminado la funci n, deberemos crear una copia loo cal del vector en la funci n y trabajar con la copia mientras ejecutemos la funci n. o o 18

2.4.

Ejercicios

Ejercicio 1: -Construye un peque o programa en lenguaje C de manera que denas un tipo de n datos aula que sea un registro que contenga una cadena de 6 caracteres llamada nombre y un entero llamado capacidad. En el programa principal dene un puntero p_aulas a este tipo de datos, despu s construye un vector de 4 elemene tos aula con la reserva de memoria adecuada y haz que se pueda acceder a este vector mediante el puntero p_aulas. La situaci n de memoria que debes obtener es la siguiente: o
0 p_aulas nombre capacidad 1 nombre capacidad 2 nombre capacidad 3 nombre capacidad

Ahora asigna al aula que est en la posici n 2 del vector de aulas una capacidad a o de 100 y el nombre F2A2. Soluci n: o En el ejercicio se solicita el siguiente programa en C: #include <stdio.h> typedef struct { char nombre[6]; int capacidad; } aula; void main() { aula *p_aulas;

/* nombre del aula */ /* capacidad del aula */

/* Puntero a aula, sera el vector de aulas */

/* Reservamos memoria del vector de aulas */ p_aulas = (aula *) malloc(4 * sizeof(aula)); /* Realizamos modificaciones */ p_aulas[2].capacidad = 100; strcpy(p_aulas[2].nombre, "F2A2"); } 19

Tema 3 Estructuras de Datos Lineales


3.1. Introducci n o

En este tema introduciremos nuevos tipos abstractos de datos conocidos como pilas, colas y listas. A estos tads se les suele denominar lineales porque se utilizan para representar alg n tipo de ordenaci n espacial lineal entre los datos que alu o macenan y adem s se construyen mediante estructuras que mantienen un esquema a lineal de los datos. Para cada tad estudiaremos su denici n y cu les son sus operaciones m s o a a comunes, despu s veremos diferentes estructuras de datos que se pueden aplicar a e cada tad seg n la representaci n que se desee utilizar, y para cada representaci n u o o veremos la implementaci n de las operaciones en lenguaje C. o

3.2.

Pilas

Denimos una pila como una estructura de datos para almacenar objetos que se caracteriza por la manera de acceder a los datos: el ultimo que entra es el primero en salir (sigue una estructura LIFO1 ). Con esto queremos expresar que el ultimo elemento que se haya insertado en la pila (el m s recientemente insertado) a ser el primero que se extraiga de la pila, en el caso en que queramos extraer alg n a u elemento de esta. Junto con la pila que almacena los datos, existe un marcador llamado tope que indica cu l es el ultimo elemento insertado y por tanto cu l es el elemento a a que se puede extraer ahora. Se accede a los elementos unicamente por el tope de la pila, es decir, cuando queramos extraer un elemento de una pila, extraeremos el elemento que est en el tope de la pila (si no est vaca), y si queremos insertar un e a
1

Del ingl s,Last In First Out. e

20

elemento en la pila, lo pondremos encima del elemento que estuviera en el tope y cambiaremos el tope de manera que se ale al elemento reci n insertado. n e Una representaci n gr ca de la estructura de datos pila es la siguiente, donde o a se observa c mo se inserta y se extrae un elemento mientras tope siempre se ala o n la cima de la pila: Pn+1
tope tope

Pn+1 Pn+1 Pn . . . P2 P1
tope

Pn . . . P2 P1

Pn . . . P2 P1

Posibles aplicaciones de la estructura de datos pila son: La evaluaci n de expresiones aritm ticas para calcular el resultado de o e una expresi n aritm tica es necesario ir calculando valores de las subexpreo e siones conforme a la prioridad de operadores, los elementos de estas subexpresiones se apilan hasta que se han ledo todos los operandos, entonces se calcula el valor de la subexpresi n y se apila. Se continua calculando los valo ores de las subexpresiones hasta que se calcule el de la expresi n aritm tica o e global. Gesti n de la recursi n la pila de recursi n debe almacenar el estado o o o de una funci n (variables locales, etc.) antes de realizar una nueva llamada o recursiva. El mantener el estado almacenado permite que despu s de la llae mada recursiva se continue la ejecuci n con los mismos valores de estado o con los que se estaba ejecutando.

3.2.1.

Operaciones sobre pilas

Para cada operaci n que denamos, veremos una representaci n gr ca del o o a funcionamiento de la funci n. En algunas operaciones se ver n el estado de la o a pila antes de la ejecuci n y el estado despu s. o e Las operaciones m s comunes que se realizan sobre una estructura de datos a pila son las siguientes: crearp(): crea una pila vaca.
/

21

apilar(p,e): a ade el elemento e a la pila p. (push en ingl s) n e

tope tope

Pn . . . P2 P1
/

e Pn . . . P2 P1

desapilar(p): elimina el elemento del tope de la pila p. (pop en ingl s) e

tope

Pn . . . P2 P1

tope

Pn1 . . . P2 P1

tope(p): consulta el tope de la pila p. (top en ingl s) e

tope

Pn Pn1 . . .
/

Devuelve Pn

P1 vaciap(p): consulta si la pila p est vaca. a

tope

Pn Pn1 . . .
/

Verdadero si n = 0 Falso si n > 0

P1

3.2.2.

Representaci n vectorial de pilas o

Una posible representaci n para pilas sera almacenar los elementos en posio ciones consecutivas de un vector a partir de la primera posici n del vector, y utio lizar una variable entera, tope, que indique d nde se encuentra el tope de la pila. o De manera gr ca: a 22

n1

P1

P2

Pn
tope

Un inconveniente de esta representaci n es que el n mero de elementos que o u puede almacenar la pila queda determinado por el tama o fsico del vector, con lo n que el c digo que implemente las operaciones de la pila para esta representaci n o o deber realizar las comprobaciones necesarias para no acceder a una posici n a o fuera del vector y a adir elementos de m s. n a La denici n del tipo de datos en lenguaje C sera: o #dene maxP ... /* Talla maxima de vector. */

typedef struct { int v[maxP]; /* Vector definido en tiempo de compilacion. */ /* Trabajamos con pilas de enteros. */ int tope; /* Marcador al tope de la pila. */ } pila; La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o pila *crearp() { pila *p; /* Reservamos memoria para la pila. */ p = (pila *) malloc(sizeof(pila)); p->tope = -1; /* Inicializamos el marcador al tope. */ return(p); /* Devolvemos un puntero a la pila creada. */ } pila *apilar(pila *p, int e) { if (p->tope + 1 == maxP) /* Comprobamos si cabe el elemento. */ /* Si no cabe hacemos un tratamiento de error. */ tratarPilaLlena(); else { /* Si cabe, entonces */ p->tope = p->tope + 1; /* actualizamos el tope e */ p->v[p->tope] = e; /* insertamos el elemento. */ } return(p); /* Devolvemos un puntero a la pila modificada. */ }

23

pila *desapilar(pila *p) { p->tope = p->tope - 1; /* Decrementamos el marcador al tope. */ return(p); /* Devolvemos un puntero a la pila modificada. */ } int tope(pila *p) { return(p->v[p->tope]); /* Devolvemos el elemento senyalado por tope. */ } int vaciap(pila *p) { return(p->tope < 0); /* Devolvemos 0 (falso) si la pila no esta vacia, */ /* y 1 (cierto) en caso contrario. */ }

3.2.3.

Representaci n enlazada de pilas con variable din mica o a

Otra representaci n posible para pilas sera construyendo din micamente un o a registro para cada elemento que quiera insertarse en la pila y mantener estos registros enlazados mediante punteros desde un registro a otro, de manera que los punteros indicar n qu elemento est almacenado detr s de qu elemento, y por a e a a e lo tanto podremos simular el orden de almacenamiento de los elementos en la pila. En cierto modo lo que estamos haciendo es simular un vector, excepto que en este caso, las posiciones de los elementos se corresponden con punteros, y en el registro correspondiente a cada posici n se guarda tambi n la direcci n de o e o memoria donde se guarda el siguiente elemento. Adem s se necesitar una variable de tipo puntero que apuntar al registro a a a que sea el primer registro de la pila (el tope); y a partir de ese elemento estar n a enlazados todos los dem s conforme al orden que deban seguir en la pila. Una a representaci n gr ca de esta estructura de datos para pilas sera: o a
E Pn X $ $$

Pn1
$

P2

P1

X $ $$$

X $ $$

Cada uno de los registros que forman la pila enlazada se denomina nodo. Con este tipo de estructura, cuando se vaya a insertar un nuevo elemento en la pila, se crear un nuevo nodo (con una funci n de reserva de memoria) para a o el nuevo elemento y se enlazar con los dem s nodos de la pila ocupando el la a a

24

primera posici n. Cuando se quiera extraer el elemento del tope de la pila, baso tar con liberar la memoria que ocupa el registro que forme el tope de la pila y a actualizar el puntero que indica d nde est el tope. o a Usando esta representaci n, el n mero m ximo de elementos que puede alo u a macenar una pila queda limitado por el n mero de nodos que se puedan crear, es u decir, el n mero de registros que se puedan reservar sin agotar la memoria del u computador. As pues, esta representaci n de pilas no tiene el problema de tener o limitado el n mero de elementos a almacenar, como ocurra con la representaci n u o vectorial de listas. La denici n del tipo de datos en lenguaje C sera: o typedef struct _pnodo { int e; /* Variable para almacenar un elemento de la pila. */ struct _pnodo *sig; /* Puntero al siguiente nodo que contiene un elemento. */ } pnodo; /* Tipo nodo. Cada nodo contiene un elemento de la pila. */ typedef pnodo pila; La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o pila *crearp() { return(NULL); /* Devolvemos un valor NULL para inicializar */ } /* el puntero de acceso a la pila. */ int tope(pila *p) { return(p->e); /* Devolvemos el elemento apuntado por p */ } pila *apilar(pila *p, int e) { pnodo *paux; paux = (pnodo *) malloc(sizeof(pnodo)); /* Creamos un nodo. */ paux->e = e; /* Almacenamos el elemento e. */ paux->sig = p; /* El nuevo nodo pasa a ser tope de la pila. */ return(paux); /* Devolvemos un puntero al nuevo tope. */ }

25

pila *desapilar(pila *p) { pnodo *paux; paux = p; /* Guardamos un puntero al nodo a borrar. */ p = p->sig; /* El nuevo tope sera el nodo apuntado por el tope actual. */ free(paux); /* Liberamos la memoria ocupada por el tope actual. */ return(p); /* Devolvemos un puntero al nuevo tope. */ } int vaciap(pila *p) { return(p == NULL); /* Devolvemos 0 (falso) si la pila no esta vacia, */ /* y 1 (cierto) en caso contrario. */ }

3.3.

Colas

Denimos una cola como una estructura de datos para almacenar objetos que se caracteriza por la manera de acceder a los datos: el primero que entra es el primero en salir (sigue una estructura FIFO2 ). Con esto queremos expresar que el primer elemento que se insert en la cola (el m s antiguo temporalmente hablano a do) ser el primero que se obtenga si se realiza una extracci n. Podemos decir que a o los elementos se introducen por el nal y se extraen por la cabeza. As, necesitaremos una estructura que guarde los elementos de la cola indi cando el orden en el que fueron insertados, y necesitaremos un marcador para la cabeza de la cola, y as saber qu elemento se puede extraer en un determinado e momento, y otro marcador al nal de la cola, para saber por donde se pueden insertar nuevos elementos. A estos dos marcadores los denominaremos respectivamente pcab y pcol. Una representaci n gr ca de la estructura de datos cola es la siguiente, donde o a se observa como se encolan los elementos al insertarlos y como se extraen elementos por la cabeza de la cola:

Del ingl s, First In First Out. e

26

pcab

pcol

Q1
pcab

Q2 Q2
pcab

Qn Qn Qn Qn Qn+1

pcol

Qn+1

Q1 Q1

pcol

Q2
pcab

Qn+1
pcol

Q2

Qn+1

Una de las aplicaciones m s comunes de la estructura de datos cola es la a gesti n de la cola de procesos para acceder a un recurso com n, como puede o u ser la cola de impresi n de una determinada impresora. o

3.3.1.

Operaciones sobre colas

Para cada operaci n que denamos, veremos una representaci n gr ca del o o a resultado de la funci n, mostrando el resultado de la operaci n o el estado de la o o cola antes y despu s de la ejecuci n. e o Las operaciones m s comunes que se realizan sobre una estructura de datos a cola son las siguientes: crearq(): crea una cola vaca.
/

encolar(q,e): a ade el elemento e a la cola q. n


pcab pcol pcab pcol

Q1

Q2

Qn

Q1

Q2

Qn

desencolar(q): elimina el elemento de la cabeza de q.


pcab pcol pcab pcol

Q1

Q2

Qn

Q2

Q3

Qn

cabeza(q): consulta el primer elemento de la cola q.


pcab pcol

Q1

Q2

Qn

Devuelve Q1

vaciaq(q): consulta si la cola q est vaca. a


pcab pcol

Q1

Q2

Qn 27

Verdadero si n = 0 Falso si n > 0

3.3.2.

Representaci n vectorial de colas o

Una posible representaci n para colas sera almacenar los elementos de la o cola en posiciones consecutivas de un vector entre dos posiciones determinadas y utilizar dos variables enteras pcab y pcol para indicar esas dos posiciones que marcar n la cabeza y el nal de la cola, es decir, las posiciones de donde se a extraen e insertan elementos respectivamente. Cada vez que insertemos un elemento en la cola, tendremos que incrementar pcol, y cada vez que extraigamos un elemento de la cola tendremos que incrementar pcab. Con lo que si se han producido varias inserciones y varios borrados, una situaci n general de una cola usada por un programa podra ser: o
0 maxC1

Q1
pcab

Q2

Qn
pcol

Ahora bien, si realizamos m s inserciones en la cola que posiciones tiene el a vector donde se almacenan los datos, entonces el marcador pcol se saldra del vec tor, con lo que tendremos que comprobar que no nos saldremos de las posiciones del vector fsico. Adem s podra ocurrir que mientras tanto se hayan realizado a algunas extracciones de elementos, con lo que el marcador pcab no se ale la n primera posici n del vector, sino que haya posiciones libres al principio del veco tor. Esa situaci n sera la siguiente: o
0 maxC1

Q1
pcab

Q2

Qn
pcol

Entonces estaramos diciendo que no podemos insertar m s elementos en la a cola, pero s que quedaran posiciones libres para insertar elementos en el princi pio del vector. Para poder aprovechar ese espacio, haremos que pcol vuelva al principio del vector e inserte los posibles nuevos elementos ah. Con esto hemos convertido el vector en lo que se conoce como cola circular, donde tanto los mar cadores pcol y pcab pueden pasar de la ultima posici n del vector a la primera, o y podr n alcanzarse en el caso en que la cola se quede vaca o llena de elementos, a pero nunca cruzarse. Para conseguir que pcol y pcab vuelvan al principio del vector utilizaremos la operaci n modulo (tambi n conocido como resto), que nos o e da el resto de la divisi n entera de dos n meros; si dividimos un n mero positivo o u u cualquiera entre maxC, el resto de esta divisi n ser un n mero entre 0 y maxC-1 o a u y adem s si se incrementa en 1 el dividendo, el resto se incrementa en 1, con lo a que estaremos identicando posiciones correlativas del vector. As, cuando los marcadores pcol y pcab se incrementen, se dividir n entre maxC y su nuevo a 28

valor ser el resto de esa divisi n. Una visi n gen rica de una cola circular es la a o o e siguiente:
0 maxC1

Qn
pcol

Q1
pcab

Q2

Ahora podemos plantear otro problema que puede ocurrir con la representaci n o adoptada de cola circular, en una situaci n gen rica en la que se hayan producio e do varias inserciones y extracciones de elementos podemos representar la cola de esta manera:
maxC-1 0

'$ '$
f f

&%
f f

&%

pcol

rr

pcab

Ahora bien, imaginemos que llegamos a un punto en el que se hayan producido primero 20 inserciones de elementos y posteriormente se hayan producido 20 extracciones, con lo que la cola estar vaca y por ello los marcadores pcol y pcab a tendr n el mismo valor (se alar n a la misma posici n). Y ahora imaginemos otra a n a o situaci n en la que hemos insertado elementos en la cola hasta llenarla, a donde o se alar n los marcadores pcol y pcab? Estar n se alando a la misma posici n n a a n o igualmente, puesto que pcol habr dado una vuelta completa al vector hasta ala canzar a pcab. Gr camente las dos situaciones se representar n de esta manera: a a

29

'$ '$
maxC-1 0

f f

&%
Con lo que necesitaremos alg n mecanismo adicional para distinguir cu ndo u a una cola est llena o est vaca, esto es, distinguir entre el caso en que pcol y a a pcab se alen a la misma posici n. Para ello, la cola tendr asociada una variable n o a entera llamada talla que indicar el n mero de elementos que tiene la cola en un a u determinado momento. Cuando pcol y pcab se alen a la misma posici n solo n o tendremos que comprobar la variable talla para comprobar si la cola est vaca a o no. Con la representaci n vectorial de colas tendremos el mismo problema que o ocurra con la representaci n vectorial de pilas, el n mero m ximo de elementos o u a estar limitado por el tama o fsico del vector. a n La denici n del tipo de datos en lenguaje C para una cola con representaci n o o vectorial sera: #dene maxC ... /* Talla maxima del vector. */

&%

rr

pcab pcol

typedef struct { int v[maxC]; /* Vector definido en tiempo de compilacion. */ int pcab, pcol; /* Marcador a la cabeza y a la cola. */ int talla; /* Numero de elementos. */ } cola; La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o

30

cola *crearq() { cola *q;

q = (cola *)malloc(sizeof(cola)); /* Reservamos memoria para la col q->pcab = 0; /* Inicializamos el marcador a la cabeza */ q->pcol = 0; /* Inicializamos el marcador a la cola */ q->talla = 0; /* Inicializamos la talla */ return(q); /* Devolvemos un puntero a la cola creada. */ } cola *encolar(cola *q, int e) { if (q->talla == maxC) /* Comprobamos si cabe el elemento. */ /* Si no cabe hacemos un tratamiento de error. */ tratarColaLlena(); else { /* Si cabe, entonces */ q->v[q->pcol] = e; /* guardamos el elemento, */ q->pcol = (q->pcol + 1)%maxC; /* incrementamos marcador de cola, q->talla = q->talla + 1; /* e incrementamos la talla. */ } return(q); /* Devolvemos un puntero a la cola modificada. */ } cola *desencolar(cola *q) { /* Avanzamos el marcador de cabeza. */ q->pcab = (q->pcab + 1) % maxC; q->talla = q->talla - 1; /* Decrementamos la talla. */ return(q); /* Devolvemos un puntero a la cola modificada. */ } int cabeza(cola *q) { return(q->v[q->pcab]); /* Devolvemos el elemento que hay en cabeza. */ } int vaciaq(cola *p) { return(q->talla == 0); /* Devolvemos 0 (falso) si la cola */ /* no esta vacia, y 1 (cierto) en caso contrario.*/ }

31

3.3.3.

Representaci n enlazada de colas con variable din mica o a

Al igual que con las pilas, almacenaremos cada elemento en un registro que se crea (se reserva memoria) cuando se inserta el elemento. Cada registro contendr tambi n, un puntero al siguiente registro correspondiente en la secuencia a e de elementos. En este caso necesitaremos dos punteros, a los que llamaremos pcab y pcol, que apuntar n al primer registro de la cola (la cabeza de la cola), y al ultimo a registro de la cola (el nal de la cola). Una representaci n gr ca de esta estructura para colas es: o a
pcab pcol

E Q1

Q2

Qn1
$$

Qn
X $ $ B

X $$$ $

X $ $$$

A cada registro que forme parte de la secuencia enlazada lo llamaremos nodo, al igual que para la representaci n enlazada de pilas. o Con esta representaci n din mica para colas, no se sufrir la limitaci n del o a a o n mero m ximo de elementos que puede almacenar la cola, como ocurra con la u a representaci n vectorial. En este caso, el n mero m ximo de elementos que puede o u a almacenar la cola queda limitado por el n mero m ximo de nodos que se puedan u a reservar en memoria. La denici n del tipo de datos en lenguaje C sera: o typedef struct _cnodo { int e; /* Variable para almacenar un elemento de la cola. */ struct _cnodo *sig; /* Puntero al siguiente nodo que contiene un elemento. */ } cnodo; /* Tipo nodo. Cada nodo contiene un elemento de la cola. */ typedef struct { cnodo *pcab, *pcol; } cola;

/* Punteros a la cabeza y la cola. */

La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o

32

cola *crearq() { cola *q; q = (cola*) malloc(sizeof(cola)); /* Creamos una cola. */ q->pcab = NULL; /* Inicializamos a NULL los punteros. */ q->pcol = NULL; return(q); /* Devolvemos un puntero a la cola creada.*/ } cola *encolar(cola *q, int e) { cnodo *qaux; qaux = (cnodo *) malloc(sizeof(cnodo)); /* Creamos un nodo. */ qaux->e = e; /* Almacenamos el elemento e. */ qaux->sig = NULL; if (q->pcab == NULL) /* Si no hay nigun elemento, entonces */ q->pcab = qaux; /* pcab apunta al nuevo nodo creado, */ else /* y sino, */ q->pcol->seg = qaux; /* el nodo nuevo va despues del que apunta pcol. */ q->pcol = qaux; /* El nuevo nodo pasa a estar apuntado por pcol. */ return(q); /* Devolvemos un puntero a la cola modificada. */ } cola *desencolar(cola *q) { cnodo *qaux; qaux = q->pcab; /* Guardamos un puntero al nodo a borrar. */ q->pcab = q->pcab->sig; /* Actualizamos pcab. */ if (q->pcab == NULL) /* Si la cola se queda vacia, entonces */ q->pcol = NULL; /* actualizamos pcol. */ free(qaux); /* Liberamos la memoria ocupada por el nodo. */ return(q); /* Devolvemos un puntero a la cola modificada. */ } int cabeza(cola *q) { return(q->pcab->e); /* Devolvemos el elemento que hay en la cabeza. */ }

33

int vaciaq(cola *q) { return(q->pcab == NULL); /* Devolvemos 0 (falso) si la cola */ /* no esta vacia, y 1 (cierto) en caso contrario. */ }

3.4.

Listas

Denimos una lista como una estructura de datos formada por una secuencia de objetos. Cada objeto se referencia mediante la posici n que ocupa en la o secuencia. Un ejemplo claro de aplicaci n de esta estructura es la representaci n de una o o lista de alumnos. Si tenemos una lista con n elementos, las posiciones de la lista ir n de la 1 a a la n (no se debe contar a partir de la posici n 0). o

3.4.1.

Operaciones sobre listas

Para cada operaci n que denamos, veremos una representaci n gr ca del o o a resultado de la funci n, mostrando el resultado de la operaci n o el estado de la o o lista antes y despu s de la ejecuci n. e o Las operaciones m s comunes que se realizan sobre una estructura de datos a lista son las siguientes: crearl(): crea una lista vaca.
1 n

insertar(l,e,p): inserta e en la posici n p de la lista l. Los elemeno tos que est n a partir de la posici n p se desplazan una posici n. a o o
1 p n 1 p p+1 n+1

L1

Lp

Ln

L1

Lp

Ln

borrar(l,p): borra el elemento de la posici n p de la lista l. o


1 p n 1 p n1

L1

Lp

Ln

L1

Lp+1

Ln

34

recuperar(l,p): devuelve el elemento de la posici n p de la lista l. o


1 p n

L1

Lp

Ln

Devuelve Lp

vacial(l): consulta si la lista l est vaca. a


1 n

L1

Ln

Verdadero si n = 0 Falso si n > 0

fin(l): devuelve la posici n que sigue a la ultima en la lista l. o


1 n

L1

Ln

Devuelve n + 1

principio(l): devuelve la primera posici n de la lista l. o


1 n

L1

Ln

Devuelve 1

siguiente(l,p): devuelve la posici n siguiente a p de la lista l. o


1 p n

L1

Lp

Ln

Devuelve p + 1

3.4.2.

Representaci n vectorial de listas o

Como con las estructuras de datos anteriores, para almacenar una colecci n o de datos podemos utilizar un vector, adem s, en el caso de las listas donde cada a elemento se accede por su posici n en la lista resultar muy c modo asignar a o a o cada elemento una posici n en el vector que se corresponda con su posici n en la o o lista. Al trabajar con lenguaje C, donde los vectores comienzan desde la posici n o 0, la primera posici n del vector ser la posici n 0, que se corresponder con la o a o a posici n 1 de la lista (el primer elemento), la posici n 1 del vector se correspono o der con la posici n 2 de la lista (el segundo elemento) y as sucesivamente. a o Es conveniente tambi n tener delimitadas las posiciones de la lista, es decir e d nde empieza y acaba la lista, o, lo que es lo mismo, cu l es la primera posici n o a o y cu l es la ultima. Con esta representaci n vectorial la primera posici n de la a o o lista queda marcada por la posici n 0 del vector, pero ser necesario utilizar una o a variable ultimo de tipo entero que se ale cu l es la ultima posici n ocupada del n a o vector. Una representaci n gr ca de una lista almacenada mediante un vector es la o a siguiente: 35

n1

maxL1

L1

L2

Ln
ultimo

De la misma manera que con las representaciones vectoriales vistas para pilas y colas, esta representaci n tendr limitado el numero m ximo de elementos que o a a se pueden almacenar en la lista al tama o del vector. n La denici n del tipo de datos en lenguaje C sera: o #dene maxL ... /* Talla maxima del vector. */

typedef int posicion; /* Cada posicion se referencia con un entero. */ typedef struct { int v[maxL]; /* Vector definido en tiempo de compilacion. */ posicion ultimo; /* Posicion del ultimo elemento. */ } lista; La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o lista *crearl() { lista *l l = (lista *) malloc(sizeof(lista)); /* Creamos la lista. */ l->ultimo = -1; /* Inicializamos el marcador al ultimo. */ return(l); /* Devolvemos un puntero a la lista creada. */ }

36

lista *insertar(lista *l, int e, posicion p) { posicion i; if (l->ultimo == maxL-1) /* Comprobamos si cabe el elemento. */ /* Si no cabe hacemos un tratamiento de error. */ tratarListaLlena(); else { /* Si cabe, entonces */ /* hacemos un vacio en la posicion p, */ for (i=l->ultimo; i>=p; i--) l->v[i+1] = l->v[i]; l->v[p] = e; /* guardamos el elemento, */ /* e incrementamos el marcador al ultimo. */ l->ultimo = l->ultimo + 1; return(l); /* Devolvemos un puntero a la lista modificada. */ } } lista *borrar(lista *l, posicion p) { posicion i; /* Desplazamos los elementos del vector. */ for (i=p; i<l->ultimo; i++) l->v[i] = l->v[i+1]; /* Decrementamos el marcador al ultimo. */ l->ultimo = l->ultimo - 1; return(l); /* Devolvemos un puntero a la lista modificada. */ } int recuperar(lista *l, posicion p) { return(l->v[p]); /* Devolvemos el elemento que hay en la posicion p. */ } int vacial(lista *l) { return(l->ultimo < 0); /* Devolvemos 0 (falso) si la lista */ /* no esta vacia, y 1 (cierto) en caso contrario. */ }

37

posicion fin(lista *l) { return(l->ultimo + 1); /* Devolvemos la posicion siguiente a la ultima. */ } posicion principio(lista *l) { return(0); /* Devolvemos la primera posicion. */ } posicion siguiente(lista *l, posicion p) { return(p+1); /* Devolvemos la posicion siguiente a la posicion p. */ }

3.4.3.

Representaci n enlazada de listas con variable din mica o a

La representaci n enlazada para listas usando variables din micas va a ser muy o a similar a la t cnica usada para las representaciones enlazadas din micas de pilas y e a colas. Cada elemento estar almacenado en un registro junto con un puntero que a apuntar al registro que guarde el siguiente elemento de la lista, o dicho de otra a manera, apuntar a la siguiente posici n de la lista. a o En esta representaci n las posiciones de los elementos se corresponder n con o a punteros, esto es, una posici n de la lista es un puntero. o Ahora bien, ante este tipo de representaci n tenemos dos posibilidades a la o hora de almacenar un elemento en su posici n correspondiente: o 1. Dada una posici n p, el elemento Lp est en el nodo apuntado por p, es o a decir:
p

E Lp X $ $$$

Si suponemos que el campo del nodo donde se almacena el valor de los elementos de la lista se denomina e, entonces para acceder al valor del elemento Lp utilizaremos la expresi n p->e. o Sin embargo esta opci n tiene un inconveniente para las operaciones de ino serci n y borrado de elementos, ya que el coste de estas operaciones ser de o a O(n), siendo n el n mero de elementos de la lista. Esto es debido a que para u insertar o borrar de una determinada posici n (puntero) en la lista, es neceo sario conocer el puntero del nodo previo al nodo a insertar o borrar, para 38

ello habr que hacer un recorrido por los nodos de la lista desde el principio a de esta hasta que encontremos la posici n anterior a la que vamos a insertar o o borrar, de aqu el coste temporal lineal. La otra opci n posible para almacenar los elementos de una lista es: o 2. Dada una posici n p, el elemento Lp est en el nodo apuntado por p->sig, o a es decir:
p

E Lp1

Lp

X $ X $ $$$ $$$

Con esta opci n, la manera de acceder al valor del elemento Lp correspono diente a la posici n p es con la expresi n p->sig->e. o o Adem s, esta forma de referenciar los elementos nos permite realizar las a operaciones de inserci n y borrado de elementos con un coste O(1), puesto o que en este caso s que es conocido el puntero al nodo previo al nodo a in sertar o borrar. Usaremos pues, esta representaci n para las listas enlazadas o mediante variable din mica; ello determinar los pasos a seguir en las opa a eraciones de consulta, inserci n, borrado, etc. o Al usar la segunda opci n de las vistas anteriormente para listas enlazadas con o variable din mica, donde dada una posici n p, el elemento Lp est en p->sig, a o a el primer elemento de la lista tambi n necesitar un nodo previo al nodo donde el e a est almacenado. Este nodo ser un nodo que no contendr ning n valor y al que e a a u llamaremos nodo centinela. El nodo centinela permitir optimizar las operaciones a de actualizaci n. o Adem s, al igual que para la representaci n vectorial de listas, necesitaremos a o conocer donde est la primera y ultima posici n de la lista (en este caso ser n a o a dos punteros), para lo que tendremos dos variables primero y ultimo que guardar n dicha informaci n. Representando gr camente una lista enlazada usa o a ando variables din micas: a
primero ultimo

L1

Ln1
X $ $$$

Ln
B

X $ $ X $$$ $$$

Al igual que ocurra con la representaci n enlazada con variables din micas o a para pilas y colas, aqu el n mero m ximo de elementos que puede almacenar una u a lista viene determinado por el n mero m ximo de nodos que se puedan crear en u a memoria. 39

La denici n del tipo de datos en lenguaje C sera: o typedef struct _lnodo { int e; /* Variable para almacenar un elemento de la lista. */ struct _lnodo *sig; /* Puntero al siguiente nodo que contiene un elemento. */ } lnodo typedef lnodo *posicion; /* Cada posicion se referencia con un puntero. */ typedef struct { /* Definimos el tipo lista con un puntero */ posicion primero, ultimo; /* al primero y ultimo nodos. */ } lista; La implementaci n de las operaciones denidas anteriormente se muestra a o continuaci n: o lista *crearl() { lista *l; l = (lista *) malloc(sizeof(lista)); /* Creamos una lista. */ l->primero = (lnodo *) malloc(sizeof(lnodo)); /* Creamos el centinela */ l->primero->sig = NULL; l->ultimo = l->primero; return(l); /* Devolvemos un puntero a la lista creada. */ }

40

lista *insertar(lista *l, int e, posicion p) { posicion q; q = p->sig; /* Dejamos q apuntando al nodo que se desplaza. */ p->sig = (lnodo *) malloc(sizeof(lnodo)); /* Creamos un nodo. */ p->sig->e = e; /* Guardamos el elemento. */ p->sig->sig = q; /* El sucesor del nuevo nodo esta apuntado por q. */ /* Si el nodo insertado ha pasaso a ser el ultimo, */ if (p == l->ultimo) l->ultimo = p->sig; /* actualizamos ultimo. */ return(l); /* Devolvemos un puntero a la lista modificada. */ } lista *borrar(lista *l, posicion p) { posicion q; if (p->sig == l->ultimo) /* Si el nodo que borramos es el ultimo, l->ultimo = p; /* actualizamos ultimo. q = p->sig; /* Dejamos q apuntando al nodo a borrar. p->sig = p->sig->sig; /* p->sig apuntara a su sucesor. free(q); /* Liberamos la memoria ocupada por el nodo a borrar. return(l); /* Devolvemos un puntero a la lista modificada. } int recuperar(lista *l, posicion p) { return(p->sig->e); /* Devolvemos el elemento que hay en la posicion p. */ } int vacial(lista *l) { return(l->primero->sig == NULL); /* Devolvemos 0 (falso) si la lista */ /* no esta vacia, y 1 (cierto) en caso contrario. */ } 41

*/ */ */ */ */ */

posicion fin(lista *l) { return(l->ultimo); /* Devolvemos la ultima posicion. */ } posicion principio(lista *l) { return(l->primero); /* Devolvemos la primera posicion. */ } posicion siguiente(lista *l, posicion p) { return(p->sig); /* Devolvemos la posicion siguiente a la posicion p. */ }

3.4.4.

Representaci n enlazada de listas con variable est tica o a

Hasta ahora s lo hemos visto representaciones enlazadas para las estructuras o de datos usando variables din micas. Ahora vamos a ver una representaci n ena o lazada de listas utilizando variables est ticas que van a ser en primer lugar un a vector para almacenar los valores de los elementos (hasta ahora nada nuevo) y despu s otro vector adicional en el que vamos a almacenar el orden de los elementos, e es decir, las posiciones. En este vector de posiciones se indicar que elemento va a detr s de otro en la lista mediante ndices que indican posiciones fsicas del vector. a As pues, en esta representaci n para listas las posiciones ser n n meros enteros. o a u As pues, la estructura de datos estar compuesta por 2 vectores v y p de igual a tama o, uno para los valores de los elementos y otro para los ndices que enlazan n los diferentes elementos de la lista en el orden adecuado. Como para las otras representaciones de listas necesitaremos conocer donde empieza la lista (la primera posici n de la lista) y donde termina (la ultima posio ci n de la lista), para ello tendremos dos variables enteras p1 y u1 que se alar n o n a el principio y nal de la lista respectivamente. A cada par formado por un valor del vector v y su correspondiente del vector p lo llamaremos nodo, al igual que para el resto de estructuras de datos enlazadas. Ahora bien, cuando queramos insertar un nuevo elemento en la lista, necesitaremos tomar una de las posiciones de los vectores v y p que no est n ocupadas e ya por la lista y convertirla en un nuevo nodo que pertenezca a la lista enlaz ndolo a correctamente con el resto de nodos. Para conocer qu posiciones del vector est n e a libres para insertar nuevos elementos vamos a aprovechar las posiciones del vector p para enlazarlas en una lista simple. Es decir, a partir de uno de los nodos vacos podemos conocer el resto de nodos vacos siguiendo los enlaces que los unen for

42

mando una lista simple. El principio de esta lista de nodos vacos lo guardaremos en una variable entera p2 que se alar al primer nodo de la lista de nodos vacos. n a Una representaci n gr ca de todo lo expuesto es la siguiente: o a

maxL1

...
p2

L1
p1

Lp

...
Bu

L2
B

Ln1

...

Ln
B u1

...
B

Al trabajar con una representaci n con variable est tica existir nuevamente o a a una limitaci n del n mero m ximo de elementos que se pueden almacenar en la o u a lista, determinada por el tama o fsico del vector. n La denici n del tipo de datos en lenguaje C sera: o #dene maxL ... /* Talla maxima del vector. */

typedef posicion int; /* El tipo posicion se define como un entero. */ typedef struct { int v[maxL]; /* Vector definido en tiempo de compilacion. */ posicion p[maxL]; /* Vector de posiciones creado en tiempo de execucion. */ posicion p1, /* Marcador al principio de la lista. */ u1, /* Marcador al fin de la lista. */ p2; /* Marcador al principio de la lista de nodos vacios. */ } lista; Como trabajamos con una representaci n enlazada de listas, tendramos dos o opciones a la hora de identicar el lugar donde se almacena un elemento con la posici n que le corresponde. Una opci n es aquella donde una posici n p se ala o o o n a la posici n del vector donde est almacenado su elemento correspondiente Lp . o a Otra opci n es aquella donde una posici n p se ala la posici n del vector previa o o n o a la posici n del vector donde se almacena el elemento correspondiente Lp ; por o previa entenderemos que es la posici n anterior en la lista, pero no necesariamente o la anterior posici n fsica del vector. Gr camente podemos representarlo de esta o a manera:

43

...

...
B

Lp

...

La forma de acceder al valor del elemento Lp es mediante la expresi n L = l.v[l.p[p]]. o Escogeremos esta forma de acceder a los elementos de la lista puesto que optimizar las actualizaciones (inserciones, borrados, etc). Debemos tener en cuenta a que al usar esta representaci n, existir un nodo centinela al principio de la lista o a de elementos. A continuaci n se muestra tan solo la implementaci n de las operaciones o o crearl, insertar y borrar: lista *crearl() { lista *l; int i; l = (lista *) malloc(sizeof(lista)); /* Creamos la lista. */ l->p1 = 0; /* El nodo 0 es el centinela. */ l->u1 = 0; l->p[0] = -1; l->p2 = 1; /* La lista de nodos vacios comienza en el node 1. */ /* Construimos la lista de nodos vacios. */ for (i=1; i<maxL-1; i++) l->p[i] = i+1; l->p[maxL-1] = -1; /* El ultimo nodo vacio no senyala a ningun lugar. */ return(l); /* Devolvemos un puntero a lista construida. */ }

44

lista *insertar(lista *l, int e, posicion p) { posicion q; if (l->p2 == -1) /* Si no quedan nodos vacios, */ tratarListaLlena(); /* hacemos un tratamiento de error. */ else { q = l->p2; /* Dejamos un marcador al primer nodo vacio. */ l->p2 = l->p[q]; /* El primer nodo vacio sera el sucesor de q. */ l->v[q] = e; /* Guardamos el elemento en el nodo reservado. */ l->p[q] = l->p[p]; /* Su sucesor pasa a ser el de la pos. p. */ l->p[p] = q; /* El sucesor del nodo apuntado por p pasa a ser q. */ /* Si el nodo que hemos insertado pasa a ser el ultimo, */ if (p == l->u1) l->u1 = q; /* actualizamos el marcador u1. */ return(l); /* Devolvemos un puntero a la lista modificada. */ } lista *borrar(lista *l, posicion p) { posicion q; if (l->p[p] == l->u1) /* Si el nodo que borramos es el ultimo, */ l->u1 = p; /* actualizamos u1. */ q = l->p[p]; /* Dejamos q senyalando al nodo a borrar. */ l->p[p] = l->p[q]; /* El sucesor del nodo senyalado por p pasa a */ /* ser el sucesor del nodo apuntado por q. */ l->p[q] = l->p2; /* El nodo que borramos sera el primero de los vacios. */ l->p2 = q; /* El principio de la lista de nodos vacios comienza en q. */ return(l); }

45

3.5.

Ejercicios

Ejercicio 1: -Calcular el coste temporal asint tico para cada una de las operaciones vistas para o el TAD pila, tanto con la representaci n vectorial como con la representaci n o o enlazada con variable din mica. a Ejercicio 2: -Calcular el coste temporal asint tico para cada una de las operaciones vistas para o el TAD cola, tanto con la representaci n vectorial como con la representaci n o o enlazada con variable din mica. a Ejercicio 3: -Calcular el coste temporal asint tico para cada una de las operaciones vistas para o el TAD lista, tanto con la representaci n vectorial como con la representaci n o o enlazada con variable din mica. a

46

Ejercicio 4: -Realizar un programa que invierta un peque o chero de datos F utilizando una n estructura din mica como soporte auxiliar (no se deben usar otros cheros). Ofrea cer la soluci n al problema en lenguaje C. Deben utilizarse los nombres de las o operaciones vistas en clase. Los elementos almacenados en el chero son caracteres, es decir, es un chero de texto. Nota: Recordar que en una pila los elementos estar n apilados en el orden a inverso al de entrada en la pila, con lo que podemos considerar la PILA como la estructura tpica de inversi n. o

3 1 2 3 eof 2 1 PILA F F 3 2 1 eof

Soluci n: o La estructura tpica de inversi n es la pila. Una pila es toda aquella estructura o sobre la cual el ultimo elemento en entrar es el primero en salir, y por consiguiente, el primero que entr ser el ultimo en salir; los elementos estar n en orden inverso o a a al de entrada. Una pila se puede implementar de muchas formas, pero cada una de ellas tiene unas limitaciones. En este caso la implementaci n m s correcta sera una o a implementaci n mediante variables din micas debido a que no se conoce a priori o a el n mero de elementos que forman el chero. u

47

void inviertef(char *fich) { FILE *f; pila *p; char e; /* leemos el fichero y guardamos sus elementos en la pila */ f = fopen(fich,"r"); p = crearp(); while ((e=fgetc(f)) != NULL) apilar(p, e); fclose(f); /* escribimos los elementos en el fichero a la inversa */ f = fopen(fich,"w"); while (!vaciap(p)) { e = tope(p); desapilar(p); fputc(e, f); } fclose(f); }

48

Ejercicio 5: -Dise ar en lenguaje C un algoritmo que dada una pila de enteros, calcule la suma n y el producto de los elementos de la pila. Soluci n: o void operaciones_pila (pila *p) { int suma, producto, e; /* el elemento neutro de la suma es el 0 */ suma = 0; /* el elemento neutro del producto es el 1 */ producto = 1; /* leemos la pila y acumulamos la suma y el producto */ while (!vaciap(p)) { e = tope(p); desapilar(p); suma = suma + e; producto = producto * e; } printf("la suma es %d\n", suma); printf("el producto es %d\n", producto); }

Ejercicio 6: -Dada cierta pila p de n meros reales y un valor real x, se desea realizar una u operaci n multiplicaPila(p, x) que multiplique todos los elementos de o la pila p por dicho valor x. Dicha operaci n deber realizarse sin hacer uso de o a ninguna otra estructura de datos auxiliar, aparte de la propia p inicial y utilizando exclusivamente las operaciones de la clase pila. Ejemplo: // Inicialmente : p = 2.3 | 1.2 | -1.16 | 168 multiplicaPila(p, 3.0); // Finalmente : p = 6.9 | 3.6 | -3.48 | 504 Se pide implementar recursivamente la operaci n multiplicaPila(p, x) o utilizando solo las operaciones de la clase pila y sin hacer uso de ninguna otra estructura de datos auxiliar. 49

Nota: Al emplear un esquema recursivo, las variables locales de la funci n o pueden servir para guardar el estado de la pila antes de cada llamada recursiva y poder reconstruir la pila de la manera deseada. Soluci n: o La estrategia a seguir es la misma que se utiliza en los algoritmos de inversi n recursiva de una cola o una lista a partir de una posici n determinada; es o o decir, para lograr una reconstrucci n in-situ de la estructura original pero invertio da, utilizamos la estructura local (registro de activaci n) que soporta cada llamada o recursiva, en particular la variable local e para memorizar los estados de la pila en cada llamada antes de la inversi n. Utilizando este mismo mecanismo para o modicar una pila, al realizarse el acceso siempre por su tope, una vez realizada la modicaci n de todos sus elementos (todas las llamadas recursivas) podemos o obtener una reconstrucci n in-situ de la pila original pero con sus elementos mulo tiplicados por x. void multiplicaPila(pila *p, oat x) oat e; if (!vaciap(p)) { e = x * tope(p); desapilar(p); multiplicaPila(p, x); apilar(p, e); } }

50

Ejercicio 7: -Dada la especicaci n del TAD lista vista en clase, dise ar una funci n longitud(l) o n o que devuelva el n mero de elementos que tiene una lista l dada. Utilizar unicau mente las operaciones de la especicaci n. o Soluci n: o int longitud(lista *l) { tipo_posicion pos; int i; if (vacial(l)) /* la lista tiene 0 elementos */ return(0); else { /* recorreremos toda la lista y contaremos cuantos */ /* elementos tiene */ pos = principio(l); i=1; while (pos != fin(l)) { pos = siguiente(l, pos); if (pos != fin(l)) i++; } return(i); } }

51

Ejercicio 8: -Suponer que queremos a adir las siguientes funciones para manipular el TAD n lista: localiza (l, x) : devuelve la posici n de x en la lista l. Si x aparece o m s de una vez en l, devuelve la posici n de la primera aparici n. Si x no a o o se encuentra en la lista devuelve fin(l);. borra (l, x) : borra el elemento x de la lista l. Si x aparece m s de a una vez en l, borra la primera aparici n. Si x no se encuentra en la lista, no o hace nada. Implementar en lenguaje C dichas operaciones a partir de una representaci n o vectorial y una representaci n enlazada con variables din micas como las vistas o a en clase. Suponer que trabajamos con listas de enteros. Soluci n: o REPRESENTACION VECTORIAL: posicion localiza(lista *l, int x) { posicion pos; pos = 0; while ((pos != l->ultimo) && (l->v[pos] != x)) pos++; return(pos); } lista *borra(lista *l, int x) { posicion pos, i; pos = 0; while ((pos != l->ultimo) && (l->v[pos] != x)) pos++; for (i=pos; i<l->ultimo; i++) l->v[i] = l->v[i+1]; l->ultimo = l->ultimo - 1; return(l); }

52

REPRESENTACION ENLAZADA CON VARIABLES DINAMICAS: posicion localiza(lista *l, int x) { posicion pos; pos = l->primero; while ((pos != l->ultimo) && (pos->seg->e != x)) pos = pos->seg; return(pos); } lista *borra(lista *l, int x) { posicion pos, q; pos = l->primero; while ((pos != l->ultimo) && (pos->seg->e != x)) pos = pos->seg; if (pos->seg == l->ultimo) l->ultimo = pos; q = pos->seg; pos->seg = pos->seg->seg; free(q); return(l); }

53

Ejercicio 9: -Dise ar un algoritmo que tome como argumento dos listas de enteros l1 y l2, n y d como resultado otra lista l3 solo con aquellos elementos de l1 que no est n e e en l2 y los de l2 que no est n en l1. S lo se permite utilizar las operaciones del e o TAD lista visto en clase y las vistas en el ejercicio num. 5. Soluci n: o lista * diferencia_de_listas(lista *l1, lista *l2) { lista *l3; posicion pos; elemento e; /* recorremos la lista l1 e insertamos en l3 todos */ /* sus elementos que no esten en l2 */ pos = principio(l1); while (pos != fin(l1)) { e = recuperar(l1, pos); if (localiza(l2, e) == fin(l2)) insertar(l3, e, 1); pos = siguiente(l1, pos); } /* recorremos la lista l2 e insertamos en l3 todos */ /* sus elementos que no esten en l1 */ pos = principio(l2); while (pos != fin(l2)) { e = recuperar(l2, pos); if (localiza(l1, e) == fin(l1)) insertar(l3, e, 1); pos = siguiente(l2, pos); } return(l3); }

54

Ejercicio 10: -Dise ar una funci n recursiva que, dada una cola, la invierte. S lo se permite n o o utilizar las operaciones del TAD cola visto en clase, no se conoce la longitud de la cola y no hay que utilizar ninguna estructura auxiliar. Nota: Utilizar las variables locales de la funci n recursiva de manera que la o pila de llamadas recursivas pueda servirnos para invertir la cola. Soluci n: o Vamos a intentar comprender el esquema recursivo de inversi n: supongamos o que tenemos una cola, si nos guardamos el elemento que est en la cabeza de la a cola y lo desencolamos, entonces el resto de la cola formara una nueva cola, si conseguimos invertir esta nueva cola, obtener la inversi n de la cola original o consistir en encolar el elemento que habamos extrado de la cabeza y el problea ma estara resuelto. As pues, nuestro problema ahora es obtener la inversi n de o la nueva cola, que resulta ser el mismo problema que estamos tratando desde el principio, con lo que hemos encontrado nuestro esquema recursivo. cola * invierte_cola (cola *c) { elemento e; /* comprobamos si la cola tiene mas de un elemento */ if (cabeza(c) != cola(c)) { /* guardamos la cabeza y la desencolamos */ e = cabeza(c); desencolar(c); /* invertimos el resto de la cola */ c = invierte_cola(c); /* encolamos al final de la cola el elemento */ /* que era el primero */ c = encolar(c, e); } return(c); }

55

Ejercicio 11: -El recorrido de los autobuses de una lnea interurbana que recorre varios pueblos se puede interpretar mediante una cola, donde cada nodo es una parada. La informaci n contenida en cada nodo es: o Nombre del pueblo (String de 15 caracteres). Distancia en Km al anterior pueblo en la cola del recorrido del autob s. u Puntero a la siguiente parada (es decir, al nodo del siguiente pueblo). Una persona desea saber cuantos Kilometros va a recorrer en autob s entre dos u pueblos del recorrido. Entre estos dos pueblos puede haber varios pueblos m s. a Se pide: Escribir la denici n del tipo de datos en lenguaje C. Debe utilizarse una o representaci n enlazada de colas puesto que a priori no se sabe el n mero o u de paradas que va a tener una linea de autob s. u Implementar en lenguaje C una funci n para esa representaci n enlazada o o de colas que reciba como par metros la cola de las paradas del autob s, y a u dos nombres de dos de las paradas en la cola y que devuelva la distancia en Km entre esas dos paradas. (Suponemos que las paradas se proporcionan a la funci n en el orden de aparici n en el camino, es decir, la primera o o parada que se especica aparece antes en la cola que la segunda parada especicada). Un ejemplo de cola de paradas sera:
Marte 0 Barbate 246 Villarebuzno 25 Disneyland 72

y una llamada a la funcion sera: kilometros = cuenta_km(cola_paradas, "Barbate", "Disneyland"); donde la variable kilometros acabara valiendo 97=25+72.

56

Soluci n: o /* definicion del tipo de datos */ #typedef struct _cnode { char nombre[15]; /* nombre de la parada */ int distancia; /* distancia a la anterior parada */ struct _cnode *siguiente; /* puntero a la siguiente parada */ } cnode; #typedef struct { cnode *pcab, *pcola; } cola;

/* punteros a la cabeza y la cola */

/* funcion para hallar la distancia entre 2 estaciones */ int cuenta_km(cola *c, char *ciudad1, char *ciudad2) { int dist; /* para acumular la distancia */ cnode *aux; /* para recorrer la cola */ aux=c->pcab; /* buscamos la primera ciudad */ while ((aux != c->pcola) && (strcmp(aux->nombre,ciudad1)!=0)) aux = aux->siguiente; /* sumamos las distancias hasta la segunda ciudad */ dist = 0; while ((aux != c->pcola) && (strcmp(aux->nombre,ciudad2)!=0)) { /* OJO!, movemos el puntero antes de */ /* acumular distancias */ aux = aux->siguiente; dist = dist + aux->distancia; } return(dist); }

57

Ejercicio 12: -Tenemos dos matrices CUADRADAS de enteros y del mismo orden (mismo tama o). La estructura de estas dos matrices es din mica y puede verse un esquen a ma en la siguiente gura:
MAT


Se pide realizar las siguientes cuestiones en lenguaje C: Denir el tipo de datos (la estructura) matriz, bas ndose en la gura de a arriba. Dise ar una funci n que devuelva una matriz que sea el resultado de sumar n o dos matrices que recibe como par metros. Utilizar la estructura din mica a a elegida para las matrices. Nota: Para denir la estructura s lo hay que mirar el dibujo para ver que todos o los punteros apuntan a la misma cosa (al mismo tipo de objeto). No conocemos a priori el orden de la matriz, esto es, si es de 3x3 o 4x4, etc, el enunciado s lo nos asegura que es cuadrada, y que las dos matrices con las que o trabaja el programa principal son del mismo orden. Otro aspecto a considerar es que para recorrer una matriz tradicional.o normalse necesitaban dos variables ndices de la y columna; aqu es exactamente igual, con la diferencia de que estas variables ser n punteros. a

12

58

Soluci n: o /* definicion de tipos */ #typedef struct _mat_node { int elemento; /* el entero de este nodo */ struct _mat_mode *horiz; /* puntero horizontal */ struct _mat_mode *vert; /* puntero vertical */ } mat_node; #typedef mat_node matriz; /* tipo de datos matriz */

/* funcion que devuelve la suma de dos matrices */ matriz *suma_mat(matriz *mat1, *mat2) { mat_node *fila1, *fila2; mat_node *col1, *col2; fila1 = mat1; fila2 = mat2; /* Recorremos las 2 matrices, solo es necesario */ /* comprobar que hemos recorrido completamente la */ /* matriz 1 puesto que son del mismo tamao. */ n /* Acumulamos el resultado sobre la matriz 1 */ while (fila1 != NULL) { col1 = fila1; col2 = fila2; while (col1 != NULL) { col1->elemento = col1->elemento + col2->elemento; col1 = col1->horiz; col2 = col2->horiz; } fila1 = fila1->vert; fila2 = fila2->vert; } return(mat1); }

59

Ejercicio 13: -Dada dos listas de n meros enteros L1 y L2, se pide implementar una funci n que u o inserte la lista L2 como una sublista de L1 en la posici n de L1 que se indique. o Esto es, tenemos la lista L1, por otro lado tenemos la lista L2, suponiendo una posicion de L1 indicada, la lista L resultante contendr por el mismo orden en que a estaban almacenados anteriormente todos los elementos de L1 hasta la posici n o indicada, a partir de ah se habr n insertado todos los elementos de L2 y por ultimo a estar n el resto de elementos de L1 que quedaban. a Un esquema de la operaci n se puede observar en la gura: o
0 L1 1 2 3 4 5 6 n-1 an m-1 bm a1 a2 a3 a4 0 L2 1 2 a5 a6 a7 5 6

3 4

b1 b2 b3 b4 b5 b6 inserta_sublista(L1, L2, 5) 0 1 2 3 4 5 6 bm a5

n+m-2 an

a1 a2

a3 a4 b1 b2 b3

Se pide realizar lo siguiente: Implementar la funci n anterior utilizando para ello tan solo las operaciones o del TAD lista vistas en clase. No debe utilizarse ninguna representaci n o interna especca puesto que no es necesario. Implementar en lenguaje C la funci n anterior como si fuera una operaci n o o del TAD lista. En este caso se debe realizar accediendo a la estructura interna de la lista y manej ndola adecuadamente. Deber utilizarse la reprea a sentaci n vectorial de listas vista en clase. Realmente esta operaci n ser una o o a generalizaci n de la operaci n insertar(l, e, p) para listas vista en o o clase, donde en vez de insertar un solo entero se inserta una lista de enteros.

60

Soluci n: o -Implementaci n de la funci n usando operaciones del TAD: o o lista * inserta_sublista(lista *L1, lista *L2, posicion pos) { lista *L; posicion pos1, pos2, posL; int e; /* creamos la nueva lista */ L = crearl(); posL = principio(L); pos1 = principio(L1); /* insertaremos elementos de L1 hasta que lleguemos a */ /* la posicion pos, y entonces insertaremos toda la */ /* lista L2 y despues seguiremos insertando de L1 */ while (pos1 != fin(L1)) { if (pos1==pos) { pos2 = principio(L2); while (pos2 != fin(L2)) { e = recuperar(L2, pos2); L = insertar (L, e, posL); pos2 = siguiente(L2, pos2); posL = siguiente(L, posL); } } else { e = recuperar(L1, pos1); L = insertar (L, e, posL); posL = siguiente(L, posL); } pos1 = siguiente(L1, pos1); } return(L); }

61

-Implementaci n en lenguaje C de la operaci n con una representaci n vectoo o o rial de listas: /* funcion para insertar una lista dentro de otra en */ /* una posicion determinada */ lista *insertar_sublista(lista *L1, *L2, posicion pos) { lista *L; /* nueva lista a crear */ posicion pos1, pos2, posL; /* apuntadores de las posiciones de las */ /* diferentes listas a manejar */ /* creamos la nueva lista */ L = (lista *) malloc(sizeof(lista)); posL = 0; pos1 = 0; /* insertamos en la nueva lista */ while (pos1 != L1->ultimo) { /* si hemos llegado a la posicion pos, se insertan */ /* todos los elementos de L2 seguidos */ if (pos1 == pos) { pos2 = 0; while (pos2 != L2->ultimo) { L->v[posL] = L2->v[pos2]; pos2++; posL++; } } /* sino insertamos un elemento mas de L1 */ else { L->v[posL] = L1->v[pos1]; posL++; } pos1++; } /* actualizamos el tamao de la lista creada */ n L->ultimo = posL; return(L); }

62

Ejercicio 14: -Dada una cola del cine, podemos suponer que cuando llega un amiguete de uno que est en la cola, puede situarse justo detr s de el. a a Suponiendo que la cola de clientes del cine se representa con una cola enlazada mediante variable din mica como la vista en clase, se pide escribir la operaci n a o llega_amiguete() que modica una cola de manera que el nuevo cliente se inserta detr s del cliente especicado. Tanto el nuevo cliente como el que ya a estaba en la cola se especicar n mediante su nombre (una cadena de caracteres). a
pcab

pepe

maria

shinoshuke

popeye

pcol

La denici n del tipo de datos es: o typedef struct _cnodo { char nombre[20]; struct _cnodo *sig; } cnodo; typedef struct { cnodo *pcab, *pcol; } cola;

Soluci n: o Suponiendo que nombre_cola es el nombre de la persona que puede estar en la cola y nombre_amigo es el nombre de la persona nueva que llega:

63

cola *llega_amiguete(cola *q, char *nombre_cola, char *nombre_amigo) { cnodo *qaux, *qnew; qaux = q->pcab; while ((qaux!=NULL) && (strcmp(nombre_cola,qaux->nombre)==0)) quax = quax->sig; if (qaux!=NULL) { qnew = (cnodo *) malloc(sizeof(cnodo)); strcpy(q->new->nombre,nombre_amigo); qnew->sig = qaux->sig; qaux->sig = qnew; if (qaux==q->pcol) q->pcol=qnew; } else return(encolar(q,nombre_amigo)); return(q); }

Ejercicio 15: 1. Escribe la denici n con representaci n enlazada din mica en C para el tipo o o a de datos lista_de_alumnos de manera que los datos para cada alumno sean: nombre. n mero de expediente. u nota. 2. Escribe una operaci n consulta_nota(l,p) que devuelva la nota del o alumno que est en la posici n p. a o 3. Lo mismo que el punto anterior, pero que devuelva el nombre. 4. Suponiendo las operaciones vistas en clase ya denidas, la operaciones del punto 2 y del punto 3 denidas, y suponiendo denida otra operaci n o lista *actualiza_lista(lista *l); que rellena la lista con datos actualizados de un chero poniendo para cada alumno su nombre, n mero u de expediente y nota, y ya deja la lista ordenada por orden alfab tico. e Haz un programa en C que cree la lista, la actualice rellen ndola con los a datos de los alumnos e imprima por pantalla el nombre y la nota de cada uno de los alumnos. 64

5. Escribe una operaci n invierte_lista(l) que cree una nueva lista de o alumnos en orden alfab tico inverso con un coste lineal. e Soluci n: o 1. typedef struct _lnodo { char nombre[50]; int num_exp; int nota; struct _lnodo *sig; } lnodo; typedef lnodo * posicion; typedef struct { posicion primero, ultimo; } lista; 2. int consulta_nota(lista *l, posicion p){ return(p->sig->nota); } char *consulta_nombre(list *l, posicion p){ return(p->sig->nombre); } #include <stdio.h> #include <listas.h> void main(void) { lista *l; posicion p; l=crearl(); l=actualiza_lista(l); p=principio(l); while (p!=fin(l)) { printf("%s %d\n",consulta_nombre(l,p),consulta_nota(l,p)); p=siguiente(l,p); } } 65

3.

4.

5. Hacemos uso de una lista de nodos en la cual iremos insertando los nodos de la lista de alumnos y luego los iremos extrayendo. typedef lnodo *pila; lista *invierte_lista(lista *l) { pila *pi; lnodo *nodo; posicion p; lista *lres; pi=NULL; p=principio(l); while (p!=fin(l)) { nodo=(lnodo *)malloc(sizeof(lnodo)); strcpy(nodo->nombre,p->nombre); nodo->num_exp=p->num_exp; nodo->nota=p->nota; nodo->sig=pi; pi=nodo; p=siguiente(l,p); } lres=crearl(); p=fin(lres); while (pi!=NULL) { lres=insertar(lres,p,pi); nodo=pi; pi=pi->sig; free(nodo); p=fin(lres); } return(lres); }

66

Tema 4 Divide y vencer s a


4.1. Esquema general de Divide y Vencer s a

La t cnica de dise o de algoritmos llamada divide y vencer s consiste en, dado e n a un problema a resolver de talla n: 1. Dividir el problema en problemas de talla m s peque a (subproblemas), a n 2. Resolver independientemente los subproblemas (de manera recursiva), 3. Combinar las soluciones de los subproblemas para obtener la soluci n del o problema original.

Caractersticas
Es un esquema claramente recursivo. Determinados algoritmos tienen un elevado coste temporal en la parte de divisi n del problema en subproblemas, mientras que otros presentan un o elevado coste temporal en la parte de combinaci n de resultados. o El esquema divide y vencer s permite obtener soluciones ecientes cuana do los subproblemas tienen una talla lo m s parecida posible. a

67

4.2.

Algoritmos de ordenaci n o

Problema: Ordenar de manera no decreciente un conjunto de n enteros almacenado en un vector A.

4.2.1.

Inserci n directa o

Estrategia: 1. Se asume que existen dos partes en el vector: una ordenada y otra sin ordenar.

ordenado

2. Se selecciona el elemento que ocupa la primera posici n en la parte sin o ordenar, y se inserta en una posici n de la parte ordenada de tal forma que o se mantenga la ordenaci n en dicha parte. o

Funcionamiento: Inicialmente y debido a que, en general, el vector A no presentar ning n tipo a u de ordenaci n, se considerar que la parte ordenada estar formada unicamente o a a por el primer elemento del vector; siguiendo la estrategia del algoritmo, en los pasos sucesivos la ordenaci n en la parte ordenada siempre se mantendr hasta o a que se consiga la ordenaci n de todo el vector (ver gura 4.1). o

ordenado

ordenado

no ordenado

i no ordenado no ordenado

68

Figura 4.1: Estrategia que sigue el algoritmo de ordenaci n inserci n directa. o o La parte sombreada representa la parte ordenada en el vector. A continuaci n, se presenta una versi n del algoritmo inserci n directa1 : o o o Algoritmo: Argumentos: Inserci n directa o A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector o

void insercion_directa(int *A,int l,int r) { int i,j,aux; for (i=l+1;i<=r;i++) { aux=A[i]; j=i; while ((j>l) && (A[j-1]>aux)) { A[j]=A[j-1]; j--; } A[j]=aux; } }
1

Todos los algoritmos ser n presentados utilizando el lenguaje de programaci n C a o

69

           

     

     

...
N

Ejemplo del funcionamiento del algoritmo llamada a la funci n: o insercion directa(A,0,4)


0 1 2 3 4

vector inicial A: primera iteraci n (i=1,aux=14) o j=1


45 14 33 3 56

45 45 33 3 56 aux

j=0 segunda iteraci n (i=2,aux=33) o j=2

14 45 33 3 56

14 45 45 3 56 aux

j=1 tercera iteraci n (i=3,aux=3) o j=3 j=2 j=1

14 33 45 3 56

14 33 45 45 56 14 33 33 45 56 14 14 33 45 56 aux

j=0 cuarta iteraci n (i=4,aux=56) o

3 14 33 45 56

j=4

70

14 33 45 56

aux

An lisis de la eciencia a El coste temporal del algoritmo depender , en gran medida, del n mero de a u veces que se repitan las comparaciones del bucle m s interno (bucle while); es a decir, de lo que cueste realizar la inserci n de cada elemento en la parte ordenada. o Caso peor Dado un valor x y el vector A[1, . . . , n], tal que A[i] = x; el coste de la inserci n de x en la parte ordenada del vector (A[1, . . . , i1]) ser m ximo cuando o a a x sea menor que A[j] para todo j entre 1 e i 1; es decir, cuando x sea menor que todos los valores almacenados en la parte ordenada. En este caso, se realizar n el a n mero m ximo de comparaciones: i 1. Si el vector A estuviera inicialmente u a ordenado en orden decreciente (sin repeticiones), nos encontraramos ante el peor caso posible ya que para todo valor de i entre 2 y n el coste de la inserci n sera o el m ximo (i 1). Por lo tanto, el coste en el caso peor vendr determinado por la a a expresi n: o
n

(i 1) =
i=2

n(n 1) (n2 ) 2

Caso mejor Siguiendo con el mismo razonamiento que hemos utilizado en la estimaci n o del caso peor, dado un valor x y el vector A[1, . . . , n], tal que A[i] = x; el coste de la inserci n de x en la parte ordenada del vector (A[1, . . . , i 1]) ser mnima o a cuando x sea mayor o igual que A[i 1]; es decir, cuando x sea mayor o igual que el m ximo de los valores almacenados en la parte ordenada. Si el vector A a estuviera inicialmente ordenado en orden creciente, nos encontraramos ante el mejor caso ya que para todo valor de i entre 2 y n unicamente ser necesaria a realizar una comparaci n para realizar la inserci n. Por lo tanto, el coste en el o o caso mejor ser : a
n

1 = n 1 (n)
i=2

Caso medio Para determinar el coste medio del algoritmo, supondremos que los n elementos a ordenar son distintos. En este caso, dado un valor de i, tal que A[i] = x, 71

x puede situarse con igual probabilidad (1/i), en cualquier posici n de la parte o que quedar ordenada tras su inserci n (A[1, . . . , i]); hay que hacer la salvedad de a o que la probabilidad de que se realicen i 1 comparaciones ser 2/i, ya que esto a sucede tanto si x < A[1] como si A[1] x < A[2]. Considerando, pues, que el coste medio de la inserci n de cada elemento i en la parte ordenada, es: o 1 2(i 1) + k i k=1
i2

ci =

i+1 1 (i 1)(i + 2) = 2i 2 i

El coste medio del algoritmo para ordenar n elementos vendr determinado a por la expresi n: o
n n

ci =
i=2 i=2

i+1 1 2 i

n2 + 3n Hn (n2 ) 4

donde Hn = n 1 (log n) i=1 i El coste medio del algoritmo pertenece a (n2 ) ya que el t rmino Hn es dese 2 preciable con respecto al t rmino dominante n . El coste medio del algoritmo es, e 4 pues, la mitad de su coste en el peor de los casos, pero su coste sigue perteneciendo a (n2 ). Una vez analizados cada uno de los casos podemos concluir que el coste temporal del algoritmo inserci n directa es: o (n) O(n2 ) Se puede obtener una informaci n m s detallada sobre el algoritmo inserci n o a o directa en los libros [Ferri, Brassard, Sedgewick].

4.2.2.

Selecci n directa o

Estrategia: Asignar a cada posici n i del vector, el i- simo menor elemento o e almacenado en el vector. Funcionamiento: Dado un vector A[1, . . . , N ] 1. Inicialmente se selecciona el valor mnimo almacenado en el vector y se coloca en la primera posici n; es decir, asignamos el 1- simo menor eleo e mento a la posici n 1. o 72

2. A continuaci n, se selecciona el valor mnimo almacenado en la subsecueno cia A[2, . . . , N ] y se coloca en la segunda posici n; es decir, asignamos el o 2- simo menor elemento a la posici n 2. e o 3. Siguiendo este procedimiento se recorren todas las posiciones i del vector, asignando a cada una de ellas el elemento que le corresponde: el i- simo e menor. A continuaci n se presenta una versi n del algoritmo selecci n directa: o o o Algoritmo: Argumentos: Selecci n directa o A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector o

void seleccion_directa(int *A,int l,int r) { int i,j,min,aux; for (i=l;i<r;i++) { min=i; for (j=i+1;j<=r;j++) if (A[j]<A[min]) min=j; aux=A[i]; A[i]=A[min]; A[min]=aux; } }

Ejemplo del funcionamiento del algoritmo La parte sombreada representa la parte ordenada en el vector.

73

45 56 33 14 3 45 56 33 14 3 3 56 33 14 45 3 14 33 56 45 3 14 33 56 45 3 14 33 45 56

An lisis de la eciencia a Dado un vector de talla n (A[1, . . . , n]), el coste temporal del algoritmo vendr determinado por el n mero de veces que se realice la comparaci n del bucle a u o interno: n i. Debido a que esta instrucci n se encuentra en el interior de dos o bucles for, no habr que distinguir entre caso mejor y peor, ya que el n mero de a u comparaciones ser , en cualquier caso (independientemente del estado original a del vector):
n1

ni=
i=1

n(n 1) (n2 ) 2

Por lo tanto, el coste temporal del algoritmo selecci n directa es: o (n2 ) Se puede obtener una informaci n m s detallada sobre el algoritmo selecci n o a o directa en los libros [Ferri, Brassard, Sedgewick].

4.2.3.

Ordenaci n por mezcla o fusi n: Mergesort o o

Estrategia: Dado un vector A[1, . . . , N ] 1. Se divide el vector en dos subvectores: A[1, . . . , N ] A[ N + 1, . . . , N ]. 2 2 74

2. Se ordenan indepedientemente cada uno de los subvectores. 3. Se mezclan los dos subvectores ordenados de manera que se obtenga un nuevo vector ordenado de talla N . Funcionamiento: El planteamiento del algoritmo sigue un esquema recursivo divide y vencer s. a Para mostrar claramente el funcionamiento del algoritmo, supondremos que la talla del vector N es una potencia de 2. 1. Dividimos repetidamente el vector de talla N en subvectores de talla la mitad hasta obtener subvectores de talla 1 (v1 , v2 , . . . , vN ). 2. Mezclamos los subvectores de talla 1 por pares (v1 , v2 ), (v3 ,v4 ), . . . , (vn1 ,vN ) obteniendo subvectores ordenados de talla 2 (el doble). Repetimos este proceso, obteniendo cada vez subvectores ordenados de talla el doble que la iteraci n anterior, hasta que se obtiene el vector original ordenado. o Ejemplo:
1 2 3 4 5 6 7 8

5 2 4 6 1 3 2 6 5 2 4 6 5 2 5 2 2 5 4 4 6 6 4 6 1 3 2 6 1 3 1 1 3 3 2 6 2 2 6 6

2 4 5 6

1 2 3 6

1 2 2 3 4 5 6 6
Siguiendo el esquema divide y vencer s, el algoritmo primero divide el a problema en subproblemas de talla menor y, posteriormente, combina las soluciones de los subproblemas para obtener la soluci n al problema original. La o combinaci n de cada una de las soluciones de los subproblemas, se llevar a cabo o a mediante un algoritmo de mezcla o fusi n que veremos a continuaci n. o o 75

Algoritmo de mezcla o fusi n o Problema: Mezclar ordenadamente dos vectores (subsecuencias) ordenados.

... ...

...
ordenado

m m+1

...
ordenado

... ...

...
ordenado

A continuaci n se muestra una versi n del algoritmo de mezcla; en esta vero o si n se utiliza la funci n crear vector que reserva el espacio de memoria necesario o o para el vector auxiliar B. El c digo para esta funci n de reserva de memoria es: o o int *crea_vector(int n) { int *aux; aux = (int *) malloc(n*sizeof(int )); return(aux); }

76

Algoritmo: Argumentos:

Merge A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o m: ndice de la posici n que separa las dos partes ordenadas o r: ndice de la ultima posici n del vector o

void merge(int *A, int l, int m, int r) { int i,j,k, *B; B = crea_vector(r-l+1); i = l; j = m + 1; k = 0; while ( (i<=m) && (j<=r) ) { if (A[i] <= A[j]) { B[k] = A[i]; i++; } else { B[k] = A[j]; j++; } k++; } while (i<=m) { B[k] = A[i]; i++; k++; } while (j<=r) { B[k] = A[j]; j++; k++; } for (i=l;i<=r;i++) A[i]=B[i-l]; free(B); } La estrategia que sigue el algoritmo es la de ir colocando sucesivamente en un vector auxiliar, el menor elemento de los que quedan por mezclar. Para ello, el 77

algoritmo utiliza un ndice i que recorre secuencialmente la primera subsecuencia ordenada (A[l, . . . , m]), y un ndice j que recorre la segunda (A[m + 1, . . . , r]); adem s utiliza un ndice k para ir mezclando ordenadamente los valores en el veca tor auxiliar B. Inicialmente (primer bucle while), mientras quedan valores de la primera y segunda subsecuencia por mezclar, va recorriendo cada subsecuencia, de tal forma, que para cada nueva posici n (i j), compara los valores A[i] A[j], o colocando en el vector auxiliar B el menor de los dos, o en caso de igualdad, el que se encuentra en la posici n i; a continuaci n, actualiza el ndice correspono o diente (i = i + 1 o j = j + 1) para que indique la posici n del pr ximo valor a o o comparar. Cuando ya ha recorrido totalmente una de las dos subsecuencias, copia secuencialmente en B los elementos de la subsecuencia que faltan por mezclar (segundo o tercer bucle while). Finalmente, copia el resultado de la mezcla (el vector auxiliar B) en el vector original A (bucle for). Ejemplo del funcionamiento del algoritmo de mezcla
l m r

2 4 5 6 1 2 3 6 2 4 5 6 1 2 3 6
i j

1
k

2 4 5 6 1 2 3 6
i j

1 2
k

2 4 5 6 1 2 3 6
i j

1 2 2
k

2 4 5 6 1 2 3 6
i j

1 2 2 3
k

2 4 5 6 1 2 3 6
i j

1 2 2 3 4
k

2 4 5 6 1 2 3 6
i j

1 2 2 3 4 5
k

2 4 5 6 1 2 3 6
i j

1 2 2 3 4 5 6
k

2 4 5 6 1 2 3 6
i j

1 2 2 3 4 5 6 6
k

El algoritmo de mezcla necesita un espacio de memoria adicional (de talla n = r l + 1) para almacenar el resultado en un vector auxiliar. Existen otras versiones del algoritmo que no necesitan el vector auxiliar, sino que realizan todos 78

los cambios sobre el vector original A; sin embargo, el numero de intercambios necesarios es tan elevado que no resultan muy apropiadas. Eciencia del algoritmo de mezcla El algoritmo de mezcla tiene un coste temporal proporcional a la suma de las tallas de las dos subsecuencias. Por lo tanto, si n = r l + 1 el coste temporal del algoritmo de mezcla (n). Algoritmo Mergesort A continuaci n se presenta el algoritmo Mergesort: o Algoritmo: Argumentos: Mergesort A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector o

void mergesort(int *A,int l,int r) { int m; if (l<r) { m = (int ) ((l+r)/2); mergesort(A,l,m); mergesort(A,m+1,r); merge(A,l,m,r); } } En la gura 4.2 se muestra un ejemplo del funcionamiento del algoritmo utilizando una estructura arb rea, que ayuda a visualizar las distintas llamadas recuro sivas que realiza el algoritmo; adem s, se muestra c mo se combinan los resultaa o dos obtenidos en cada una de ellas para obtener la soluci n al problema original. o Cada nodo del arbol representa una llamada a la funci n mergesort. En cada o uno de estos nodos se representa el vector original que recibe el algoritmo, y el vector resultado que produce (sombreado). Tambi n se indica, entre par ntesis, el e e orden en que se ejecutan cada una de las llamadas y los par metros con que se a invocan.

79

Ejemplo de funcionamiento del algoritmo


0 1 2 3 4 5 6
   

(18) merge(A,0,3,6) (1) mergesort(A,0,3) (11) mergesort(A,4,6)

(10) merge(A,0,1,3)

(17) merge(A,4,5,6)

(2) mergesort(A,0,1)

(6) mergesort(A,2,3)

(12) mergesort(A,4,5)

(16) mergesort(A,6,6)

(5) merge(A,0,0,1) (3) mergesort(A,0,0) 0 (4) mergesort(A,1,1)

(9) merge(A,2,2,3) (8) mergesort(A,3,3)

(15) merge(A,4,4,5) (14) mergesort(A,5,5) 4 5

(7) mergesort(A,2,2) 1 2 3

(13) mergesort(A,4,4)

45

56

47

14

50

Figura 4.2: Ejemplo del funcionamiento del algoritmo mergesort. La parte sombreada representa el vector ordenado que se obtiene en cada llamada. Respecto al funcionamiento del algoritmo, cabe destacar que la talla de la pila de recursividad tendr una altura m xima logartmica. a a An lisis de la eciencia a El comportamiento del algoritmo ser independiente del orden inicial en el que a se encuentren los datos en el vector. Por lo tanto, el coste temporal puede aproximarse con la siguiente relaci n de recurrencia (supondremos por simplicidad que o la talla del problema n, es una potencia de 2): c1 2T ( n ) + c2 n + c3 2 n=1 n>1

T (n) =

80

45 56

45 56

47 14

14 47

50 8

8 50

45 56 47 14

14 45 47 56

50 8 3

8 3 50

45 56 47 14 50 8 3

8 3 14 45 47 50 56

 

 

  

 

  

 

  

Resolviendo por sustituci n: o n T (n) = 2T ( ) + c2 n + c3 2 n n = 2(2T ( 2 ) + c2 + c3 ) + c2 n + c3 2 2 n 2 = 2 T ( 2 ) + 2c2 n + c3 (2 + 1) 2 n 3 = 2 T ( 3 ) + 3c2 n + c3 (22 + 2 + 1) 2 = p iteraciones . . . n = 2p T ( p ) + pc2 n + c3 (2p1 + + 22 + 2 + 1) 2 (p = log2 n) = nc1 + c2 n log2 n + c3 (n 1) (n log n) Por lo tanto, el coste temporal del algoritmo mergesort es: (n log n) Problema del balanceo La descomposici n del problema original en subproblemas de, aproximadao mente, el mismo tama o (balanceo), es fundamental (como ya se dijo) para la n eciencia de los algoritmos que utilizan las t cnicas divide y vencer s. Para e a comprender por qu , veamos qu sucede si se descompone el problema original, e e de talla n, en un subproblema de talla n 1 y en otro de talla 1. En este caso, el coste temporal del algoritmo respondera a la siguiente relaci n de recurrencia: o T (n) = c1 T (n 1) + c2 n + c3 n=1 n>1

Resolviendo por sustituci n: o T (n) = = = = = T (n 1) + c2 n + c3 T (n 2) + c2 n + c2 (n 1) + 2c3 T (n 3) + c2 n + c2 (n 1) + c2 (n 2) + 3c3 ... T (1) + c2 n + c2 (n 1) + + 2c2 + (n 1)c3
n

= c1 + (n 1)c3 + c2
i=2

i (n 1) (n2 ) 2

= c1 + (n 1)c3 + c2 (n + 2) 81

Como se puede observar, el algoritmo mergesort dejara de ser eciente si no realizara las divisiones de la manera m s equilibrada posible; pasara a tener un a coste cuadr tico, al igual que los m todos de ordenaci n directos. a e o Posibles mejoras del algoritmo Tratando los subproblemas que est n por debajo de cierta talla, con un m toe e do de ordenaci n que se comporte bien para tallas peque as (por ejemplo, o n los m todos directos: inserci n, selecci n), evitaremos realizar cierto e o o n mero de llamadas recursivas consiguiendo mejorar el comportamiento u temporal del algoritmo. Se puede evitar el tiempo que se dedica en el algoritmo merge a copiar el vector auxiliar (resultado) en el vector original, realizando las llamadas recursivas de forma que se alternen, en cada nivel, el vector auxiliar y el original. Se puede obtener una informaci n m s detallada sobre el algoritmo Mergeo a sort en los libros [Ferri, Brassard, Sedgewick, Horowitz].

4.2.4.

Algoritmo por partici n: Quicksort o

El algoritmo Quicksort fue propuesto por C. A. R. Hoare en el a o 1960, y ha n resultado ser el algoritmo de ordenaci n m s eciente (en el caso medio) que se o a conoce. Veamos en qu consiste: e Estrategia: Dado un vector A[1, . . . , N ] 1. Se divide el vector A en dos subvectores no vacos: A[1, . . . , q] y A[q + 1, . . . , N ], tal que todo elemento de A[1, . . . , q] sea menor o igual que todo elemento de A[q + 1, . . . , N ]. 2. Se ordenan independientemente cada uno de los dos subvectores obteniendo el vector original ordenado. Funcionamiento: 1. Se elige como pivote uno de los elementos del vector x: se permutan los elementos del mismo de manera que los que son menores que el pivote quedan en la parte izquierda (A[1, . . . , q]), y los que son mayores en la parte derecha (A[q + 1, . . . , N ]) (partici n). Una vez realizada la partici n se o o cumplir que todos los elementos del subvector A[1, . . . , q] ser n menores a a o iguales que todos los elementos de A[q + 1, . . . , N ]. 82

...
1 q

...
N

2. Aplicamos el mismo procedimiento repetidamente a cada uno de los subvectores hasta obtener subvectores de talla 1, momento en el cual se habr obtenido a la ordenaci n del vector original. o

...
1 q

...
N

... ...
1 q q

...
q

...
N

. . . ...
El funcionamiento del algoritmo Quicksort se basa en c mo se realizan las o subdivisiones del problema original en subproblemas de talla menor. Tanto es as, que unicamente realizando subdivisiones sobre el vector original obtenemos, sin necesidad de combinar las soluciones de los subproblemas, su ordenaci n. Las o subdivisiones sucesivas que realiza el algoritmo sobre el vector ser n llevadas a a cabo por un algoritmo de partici n que veremos a continuaci n. o o Algoritmo de partici n o Como ya se vi con el algoritmo Mergesort, los algoritmos que utilizan la o t cnica divide y vencer s basan su eciencia en subdividir el problema original e a en subproblemas de tallas equilibradas (similares). Por esta raz n, la eciencia del o algoritmo Quicksort depender totalmente del modo en que se realicen las subdia visiones en el vector: ser importantsimo que el algoritmo de partici n realice las a o subdivisiones de la forma m s equilibrada posible. a El algoritmo de partici n, como ya se adelant , realizar la divisi n de un veco o a o tor en dos subvectores utilizando, para ello, un valor x denominado pivote. De esta forma, todo elemento contenido en el primer subvector ser menor o igual a que x, y todo elemento contenido en el segundo ser mayor o igual que x. Por lo a tanto, es obvio, que la talla de ambos subvectores estar m s o menos equilibrada a a dependiendo del valor escogido como pivote; es decir, si x es el menor o mayor 83

elemento almacenado en el vector nos encontraremos ante la partici n m s deseo a quilibrada posible: un subvector contendr a x y el otro al resto, mientras que si x a es la mediana nos encontraremos ante la partici n m s equilibrada posible. o a Debido a que un algoritmo que encuentre la mediana es demasiado costoso, una alternativa simple es elegir un elemento cualquiera del vector con el consiguiente riesgo de que los subvectores resultantes no est n equilibrados. e Problema: Dividir un vector en dos partes de manera que todos los elementos de la primera parte sean menores o iguales que los de la segunda. Funcionamiento: Dado un vector A[1, . . . , N ] 1. Se elige arbitrariamente el primer elemento del vector como pivote x = A[1]. 2. Utilizando un ndice i, se recorre incrementalmente desde la posici n 1 una o regi n A[1, . . . , i] hasta encontrar un elemento A[i] x. o 3. Utilizando un ndice j, se recorre decrementalmente desde la posici n N o una regi n A[j, . . . , N ] hasta encontrar un elemento A[j] x. o 4. Se intercambian los valores A[i] y A[j]. 5. Mientras i < j, repetimos los pasos (2,3,4) iniciando el recorrido incremen tal y decremental desde las ultimas posiciones i j, respectivamente. 6. El ndice j, donde 1 j < N , indicar la posici n que separa las dos partes a o del vector A[1, . . . , j] A[j + 1, . . . , N ], tal que todo elemento de A[1, . . . , j] ser menor o igual que todo elemento de A[j + 1, . . . , N ] a A continuaci n, se presenta el algoritmo: o

84

Algoritmo: Argumentos:

Partition A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector o

int partition(int *A, int l, int r) { int i,j,x,aux; i=l-1; j=r+1; while(i<j) { x=A[l];

do { j--; } while (A[j]>x); do { i++; } while (A[i]<x); if (i<j) { aux=A[i]; A[i]=A[j]; A[j]=aux; } } return(j); }

85

Ejemplo de funcionamiento del algoritmo de partici n o


r

5
i

2 6

4 1

3 7
r

x=5
j

5
i

2 6

4 1

3 7
j r

x=5

3
i

2 6

4 1 5 7
j r

x=5

2 6
i

4 1
j

5 7
r

x=5

2 1
i

4 6
j

5 7
r

x=5

2 1

4 6
j q i

5 7

x=5

2 1

4 6

5 7

x=5

A[l,...,q]

A[q+1,...,r]

Eciencia del algoritmo de partici n o El coste temporal del algoritmo de partici n es (n), o donde n = r l + 1.

86

Algoritmo Quicksort A continuaci n, se presenta el algoritmo Quicksort: o Algoritmo: Argumentos: Quicksort A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector o

void quicksort(int *A, int l, int r) { int q; if (l<r) { q = partition(A,l,r); quicksort(A,l,q); quicksort(A,q+1,r); } }

Ejemplo del funcionamiento del algoritmo En la gura 4.3 se muestra un ejemplo del funcionamiento del algoritmo Quicksort. Entre par ntesis se indica el orden en que se realizan las llamadas, e tanto al algoritmo de partici n como las recursivas. o En la gura 4.4 se puede observar una traza del funcionamiento del algoritmo Quicksort. Es una manera alternativa a la de la gura 4.3 de analizar la secuencia de operaciones de un algoritmo para un caso determinado.

87

quicksort(A,0,7)
0 1 2 3 4 5 6 7

5 3 2 6 4 1 3 7
(1) partition(A,0,7)
0 1 2 3 4 5 6 7

3 3 2 1 4 6 5 7
q=4 (2) quicksort(A,0,4)
0 1 2 3 4 5

(15) quicksort(A,5,7)
6 7

3 3 2 1 4
(3) partition(A,0,4)
0 1 2 3 4

6 5 7
(16) partition(A,5,7)
5 6 7

1 2 3 3 4
q=1 (4) quicksort(A,0,1)
0 1

5 6 7
q=5 (18) quicksort(A,6,7)
6

(8) quicksort(A,2,4) (17) quicksort(A,5,5)


2 3 4

1 2
(5) partition(A,0,1)
0 1

3 3 4
2 3 4

6 7 5

1 2
q=0 (6) quicksort(A,0,0) (7) quicksort(A,1,1)

3 3 4
q=2

3 4
q=3 (13) quicksort(A,3,3) (14) quicksort(A,4,4)

Figura 4.3: Ejemplo del funcionamiento del algoritmo quicksort. An lisis de la eciencia a El coste del algoritmo depender directamente de que las tallas de los subveca tores que se generen en cada partici n sean equilibradas o no lo sean. Este hecho o depender del elemento elegido como pivote en cada una de las particiones. Por a lo tanto, para evaluar el coste del algoritmo habr que diferenciar los siguientes a 88







(12) partition(A,3,4)

(10) quicksort(A,2,2)

(11) quicksort(A,3,4) (20) quicksort(A,6,6)


3 4

3 4

(9) partition(A,2,4)

(19) partition(A,6,7)
6 7

6 7
q=6

(21) quicksort(A,7,7)

A= 3

1 , llamada inicial: Quicksort(A,0,4);


0 1 2 3 4

Quicksort(A,0,4) Partition(A,0,4) 1 Quicksort(A,0,1) Partition(A,0,1) 0 Quicksort(A,0,0)// Quicksort(A,1,1)// Quicksort(A,2,4) Partition(A,2,4) 2 Quicksort(A,2,2)// Quicksort(A,3,4) Partition(A,3,4) 3 Quicksort(A,3,3)// Quicksort(A,4,4)//

3
0

2
1

5
2

7
3

1
4

1
0

2 | 5
1

1
0

2
1

1 | 2
0

1
1

2
2 3 4

5
2

7
3

3
4

3 | 7
2

3
3 4

7
3

5
4

5 | 7
3

5
4

Figura 4.4: Traza ejemplo del algoritmo quicksort. casos: Caso peor La partici n m s desequilibrada posible se producir cuando se genere un subo a a vector de talla 1 y otro de talla N 1. Por lo tanto, cuando todas las particiones que realice el algoritmo sean de esta ndole, nos encontraremos ante el peor caso. Este caso ocurre, por ejemplo, cuando inicialmente el vector presenta una ordenaci n creciente (sin elementos repetidos), ya que, para todo partici n, el valor o o 89

elegido como pivote ser el menor. En este caso, el comportamiento del algorita mo responder a la siguiente relaci n de recurrencia: a o T (n) = c1 T (n 1) + c2 n + c3 n=1 n>1

Por lo tanto, T (n) (n2 ) (ver demostraci n en la secci n 4.2.3). o o Caso mejor El caso mejor se producir cuando, en todas las particiones que realice el ala goritmo, se generen dos subvectores de talla la mitad. En este caso, el comportamiento del algoritmo responder a la siguiente relaci n de recurrencia: a o T (n) = c1 2T ( n ) + c2 n + c3 2 n=1 n>1

Por lo tanto, T (n) (n log n) (ver demostraci n en la secci n 4.2.3). o o Caso medio Para evaluar el coste medio, asumiremos que todos los elementos del vector A[1, . . . , n] son distintos. Si no fuera el caso, el coste del algoritmo no variara, pero el an lisis sera m s complicado del que se presenta a continuaci n: a a o Como ya se ha insistido anteriormente, las tallas de los subvectores que se generan en las diferentes particiones, ser n m s o menos equilibradas dependiena a do del valor elegido como pivote. Debido a que, en general, desconocemos el valor que se va a elegir como pivote en cada una de las particiones, y, por lo tanto, las tallas de los subproblemas originados en cada partici n, tendremos que asumir o 1 que con igual probabilidad ( n ) se puede dar una de todas las posibles particiones:

90

A
1 n1

Talla subproblemas 1 n1

Probabilidad
1 n

...
1 n1

...
2 n2

n1

1 n

...
3 n3

n2

1 n

...
...
n2 2

3 ... n2
1

n3 ... 2

1 n

...
1 n

...
n1

...

n1

1 n

Unicamente, la probabilidad de que la partici n generada sea A[1] y A[2, . . . , N ], o es doblemente probable, ya que esto se cumple tanto si el valor elegido como pivote es el menor del vector, como si es el segundo menor. Para evaluar el coste medio, debemos evaluar el coste que supondra ordenar cada uno de los posibles subproblemas, y ponderar el coste obtenido, para cada subproblema, por la probabilidad de que ocurra. Siguiendo este razonamiento, el coste medio de ordenaci n de un vector de talla n puede ser determinado por la o siguiente expresi n: o 1 T (1) + T (n 1) + n
n1

T (n) =

(T (q) + T (n q)) + (n) (4.1)


q=1

Dado que T (1) = (1) y, en el caso peor, T (n 1) = O(n2 ) , podemos deducir que: 1 1 (T (1) + T (n 1)) = ((1) + O(n2 ) n n = O(n) 91

Por lo tanto, y dado que, en la expresi n 4.1, el t rmino (n) puede absorber o e o la expresi n n (T (1) + T (n 1)) , la expresi n 4.1 puede reescribirse como: o 1 1 n
n1

T (n) =

(T (q) + T (n q)) + (n)


q=1

(4.2)

Obs rvese que para k = 1, 2, . . . , n 1 cada t rmino T (k) ocurre , en el e e sumatorio, una vez como T (q) y otra como T (n q). Por lo tanto, la expresi n o 4.2 puede reescribirse como: 2 n
n1

T (n) =

T (k) + (n)
k=1

Una vez hemos obtenido la expresi n que estima el coste medio del algoritmo, o podemos concluir que el comportamiento medio del algoritmo responder a la sia guiente relaci n de recurrencia: o T (n) = c1
2 n n1 k=1

T (k) + c2 n

n1 n>1

Se puede demostrar por inducci n que n > 1, T (n) c3 n log2 n, donde o c3 = 2c2 + c1 , y por lo tanto T (n) O(n log n) [Cormen, Brassard]. Posibles mejoras del algoritmo Tratando los subproblemas que est n por debajo de cierta talla, con un m toe e do de ordenaci n que se comporte bien para tallas peque as (por ejemplo, o n los m todos directos: inserci n, selecci n), evitaremos realizar cierto e o o n mero de llamadas recursivas consiguiendo mejorar el comportamiento u temporal del algoritmo. Elegir el valor utilizado como pivote de forma aleatoria entre todos los valores almacenados en el vector. Para ello, antes de realizar cada partici n, o se intercambiar el elemento utilizado como pivote A[l] con un elemento a del subvector A[l, . . . , r] seleccionado al azar. De esta forma, cualquier elemento del vector tendr la misma probabilidad de ser el pivote y, por a ejemplo, se evitar que, en el caso de que el vector ya est ordenado de a e forma creciente (sin repeticiones), se elija siempre como pivote el valor mnimo del vector, lo que ocasiona un mal comportamiento del algoritmo. 92

Como ya ha sido indicado, para realizar particiones equilibradas, lo ideal sera utilizar como pivote la mediana del vector; sin embargo, obtener la mediana de un vector es demasiado costoso. Para solucionar este problema, se podra utilizar como pivote una pseudomediana que se obtendra de la siguiente forma: se seleccionan varios elementos al azar y se calcula la mediana de ellos. Una posibilidad sera, dado un subvector A[l, . . . , r], seleccionar tres elementos: A[l], A[ l+r ], A[r]. 2 Se puede obtener una informaci n m s detallada sobre el algoritmo quicko a sort en los libros [Ferri, Cormen, Brassard, Sedgewick, Horowitz].

4.2.5.

Comparaci n emprica de los algoritmos o

En la gr ca de la gura 4.5 se muestra el comportamiento, ante muestras a reales, de cada uno de los algoritmos analizados. El eje de abscisas representa el tama o de los vectores (talla de los problemas), y el eje de ordenadas indica n el tiempo (en microsegundos) empleado en la ordenaci n de los vectores. Como o puede observarse, en la pr ctica, el algoritmo quicksort es el que presenta un mejor a comportamiento.

Figura 4.5: Gr ca comparativa de los diferentes algoritmos de ordenaci n. a o

93

4.3.

Busqueda del k- simo menor elemento e

Para la denici n del problema que se presenta a continuaci n, se asumir que o o a los elementos del vector son distintos entre s; aunque conceptualmente todo lo indicado se pueda extender al caso en que existan elementos repetidos. Problema: El problema a resolver se denomina problema de la selecci n, y o consiste en: Dado un conjunto de n enteros almacenados en un vector A[1, . . . , N ], encontrar el k- simo menor elemento; es decir, el elemento x A que es e mayor que exactamente k 1 elementos. Una primera soluci n a este problema se obtendra ordenando el vector y, o posteriormente, seleccionando el elemento que ocupara la posici n k. De esta o forma, si se realizara la ordenaci n del vector, por ejemplo, mediante el algoritmo o mergesort, se resolvera el problema con un coste temporal de (n log n). Sin embargo, este problema puede ser resuelto de una manera m s eciente utilizando a la t cnica de divide y vencer s. e a Estrategia: Dado un vector A[1, . . . , N ] 1. Se utiliza el algoritmo de partici n (partition) utilizado por quicksort para o dividir el vector en dos partes, de manera que todo elemento de la primera parte sea menor o igual que todo elemento de la segunda.

...
1 q

...
N

2. Debido a que, una vez ordenado el vector, el k- simo menor elemento ocue par la posici n k, si se cumple que 1 k q unicamente ser necea o a sario ordenar el subvector A[1, . . . , q]; mientras que si, por el contrario, q < k N unicamente ser necesario ordenar el subvector A[q+1, . . . , N ]. a

94

El siguiente algoritmo resuelve el problema de la selecci n siguiendo la eso trategia indicada: Algoritmo: Selecci n o Argumentos: A: vector A[0, . . . , n 1], n: talla del vector, k: valor de k int seleccion(int *A, int n, int k) { int l,r,q; l = 0; r = n-1; k = k-1; while (l<r) { q = partition(A,l,r); if (k<=q) r=q; else l=q+1; } return(A[l]); }

Ejemplo de funcionamiento del algoritmo


En la gura 4.6 se muestra un ejemplo de c mo funciona el algoritmo de o selecci n, dado un vector A = {31, 23, 90, 0, 77, 52, 49, 87, 60, 15}, y obteniendo o el elemento 4- simo menor. e

4.3.1.

An lisis de la eciencia a

El coste temporal del algoritmo depender de la talla que tengan los sucesivos a subvectores, en los que se va reduciendo el espacio de b squeda despu s de cada u e partici n. o

Caso peor
En el caso en que, despu s de cada partici n, el espacio de b squeda se ree o u duzca unicamente en un elemento, nos encontraremos en el peor caso. Este caso ocurrir , por ejemplo, cuando los elementos del vector est n ordenados crecientea e mente (sin repeticiones), y el elemento buscado sea el n- simo menor. e 95

seleccion(A,10,4)
0 1 2 3 4 5 6 7 8 9

31 23 90 0 77 52 49 87 60 15
partition(A,0,9)
0 1 2 3 4 5 6 7 8

r
9

15 23 0 90 77 52 49 87 60 31
q=2
3 4 5 6 7 8 9

90 77 52 49 87 60 31
r
partition(A,3,9)
3 4 5 6 7 8 9

31 77 52 49 87 60 90
q=8
3 4 5 6 7 8

31 77 52 49 87 60
r
partition(A,3,8)
3 4 5 6 7 8

31 77 52 49 87 60
q=3
3

31
r return(31)

Figura 4.6: Ejemplo del funcionamiento del algoritmo selecci n. o En este caso, el comportamiento temporal del algoritmo responder a la sigua iente relaci n de recurrencia: o T (n) = c1 T (n 1) + c2 n + c3 n=1 n>1

96

Por lo tanto, T (n) (n2 ) (ver demostraci n en la secci n 4.2.3). o o

Caso mejor
El caso mejor se producir cuando el algoritmo de partici n divida siempre el a o vector en dos partes iguales hasta encontrar al elemento buscado. En este caso, el comportamiento temporal del algoritmo responder a la siguiente relaci n de a o recurrencia: T (n) = c1 T ( n ) + c2 n + c3 2 n=1 n>1

Resolviendo por sustituci n (supondremos por simplicidad que la talla del probo lema n, es una potencia de 2): n T (n) = T ( ) + c2 n + c3 2 n n = T ( 2 ) + c2 + c2 n + 2c3 2 2 n n = T ( 2 ) + (n + )c2 + 2c3 2 2 n n n = T ( 3 ) + c2 2 + c3 + (n + )c2 + 2c3 2 2 2 n 1 1 = T ( 3 ) + c2 n(1 + + 2 ) + 3c3 2 2 2 = p iteraciones . . . 1 1 1 n = T ( p ) + c2 n(1 + + 2 + + p1 ) + pc3 2 2 2 2 2(2p 1) n = T ( p ) + c2 n + pc3 2 2p 2(2log2 n 1) (p = log2 n) = T (1) + c2 n + log2 n c3 2log2 n = c1 + 2c2 (n 1) + c3 log2 n O(n)

Caso medio
Como ya vimos al analizar el algoritmo quicksort, el algoritmo de partici n, o en el caso medio, genera subvectores m s o menos equilibrados, lo que ocasiona a que el comportamiento del algoritmo, en el caso medio, se aproxime al comportamiento en el caso mejor. Debido a que el algoritmo de selecci n basa su funo cionamiento en el algoritmo de partici n utilizado por quicksort, an logamente, o a 97

podemos concluir que el comportamiento del algoritmo de selecci n se aproxio mar , en t rmino medio, a su coste en el caso mejor, es decir, O(n) [Cormen]. a e Se puede obtener una informaci n m s detallada sobre el algoritmo seleco a ci n en los libros [Cormen, Sedgewick, Ferri, Horowitz]. o

4.4.

Busqueda binaria

El problema de la b squeda binaria (o dicot mica) consiste b sicamente en u o a buscar la posici n de un determinado elemento en un conjunto ordenado de eleo mentos. Por ejemplo, si tenemos el vector
0 1 2 3

A=

9 ,

el resultado de llamar a una funci n busqueda_binaria(A,5) nos devolvera o la posici n 1 puesto que el elemento 5 est en la segunda posici n. o a o Podemos formular el problema de esta manera: Problema: Dado un vector A ordenado por orden no decreciente de n enteros, buscar la posici n correspondiente a un elemento x. Esto es, buscar un i tal que o A[i]=x. Podemos adoptar una soluci n directa que consistira en recorrer el vector o desde la primera posici n hasta que encontremos el elemento buscado y devolver o la posici n de este. Este es el cl sico algoritmo de b squeda secuencial, que como o a u se puede observar claramente tiene un coste lineal con el tama o del vector, esto n es O(n). Vamos a proponer un esquema de b squeda basado en divide y vencer s, deu a spu s analizaremos su coste temporal y lo compararemos con la soluci n directa e o anterior. La idea es que podemos aprovechar que el vector est ordenado para aplicar el a esquema divide y vencer s. Si tomamos el valor del elemento que est en la mitad a e del vector, primero podremos compararlo con el elemento que estamos buscando, x, si es igual, entonces ya hemos encontrado la posici n resultado que ser la o a mitad del vector. En el caso de que x sea menor que el elemento de la mitad del vector, quiere decir que x se encontrar en el subvector formado por la primera a mitad del vector completo, entonces podemos aplicar el mismo criterio de b squeu da en ese subvector. En el caso de que x sea mayor que el elemento de la mitad del vector, quiere decir que x se encontrar en el subvector formado por la sea gunda mitad del vector completo, entonces podemos aplicar el mismo criterio de b squeda en ese subvector. As podemos ir buscando en subvectores cada vez m s u a peque os hasta que encontremos el elemento x en la mitad del vector o lleguemos n a un subvector de tama o 1 que necesariamente estar formado por x y podremos n a 98

devolver la posici n correspondiente. En el caso de que el elemento x no estuviera o en el vector A, deberamos devolver un codigo de control que lo especifque, por ejemplo, devolver la posici n -1. o El algoritmo para la b squeda binaria en un vector mediante Divide y Vencer s u a es el siguiente: Algoritmo: Argumentos: B squeda binaria u A: vector A[l, . . . , r], l: ndice de la primera posici n del vector, o r: ndice de la ultima posici n del vector, o x: elemento del vector a buscar

int busqueda_binaria(int *A, int l, int r, int x) { int q; if (l==r) if (x==A[l]) return(l); else return(-1); else { q = (int ) (l+r)/2; if (x == A[q]) return(q); else if (x < A[q]) return(busqueda_binaria(A,l,q-1,x)); else return(busqueda_binaria(A,q+1,r,x)); } } Un aspecto a tener en cuenta es que esta es una codicaci n recursiva del algoo ritmo, pero tambi n es sencillo escribir un algoritmo que aplique la metodologa e Divide y Vencer s descrita en el p rrafo anterior con una codicaci n iterativa. La a a o realizaci n de este algoritmo queda propuesta para el alumno. o En la gura 4.7 podemos observar los sucesivos subvectores que emplea el algoritmo de b squeda binaria para buscar la posici n del elemento x=3. u o

4.4.1.

Eciencia del algoritmo

Se puede comprobar que este algoritmo tiene un coste de O(log n) dado un vector de talla n, puesto que en cada posible llamada recursiva, la talla del vector se reduce a la mitad, n/2. M s detalladamente, si tenemos un vector de talla 1, ya tendremos la soluci n a o y la habremos obtenido con un coste constante. Si tenemos un vector de talla 99

x=1

llamada inicial: busqueda binaria(A,0,8,1);


0 1 2 3 4 5 6 7 8

15 22 33 41 52 60 q=4
3

15

q=1
0

1 A[0]=1 = return(0) Figura 4.7: Ejemplo de funcionamiento del algoritmo busqueda binaria para encontrar el elemento x=1 en el vector A={1,3,7,15,22,33,41,52,60}. mayor que 1, lo dividiremos por la mitad y buscaremos o en la primera mitad o en la segunda mitad, pero no en las dos, con lo que se nos plantea la siguiente relaci n de recurrencia: o T (n) = c1 T ( n ) + c2 2 n=1 n>1

Resolviendo por sustituci n: o n T (n) = T ( ) + c2 2 n = T ( 2 ) + 2c2 2 n = T ( 3 ) + 3c2 2 = p iteraciones . . . n = T ( p ) + pc2 2 (p > log2 n) = c1 + c2 log2 n O(log n)

100

4.5.

C lculo de la potencia de un numero a


N = bn , n 0

Problema: Se quiere calcular:

4.5.1.

Algoritmo trivial

Una aproximaci n trivial a la resoluci n del problema, sera descomponerlo o o de la siguiente forma: N = b b bb b nveces El siguiente algoritmo resuelve el problema siguiendo esta estrategia trivial: Algoritmo: Potenciaci n o Argumentos: b: base, n: exponente int potencia(int b, int n) { int res, i; res = 1; for (i=1; i<=n; i++) res = res * b; return(res); } El coste temporal de este algoritmo es lineal en funci n del exponente y, por o lo tanto, es O(n).

101

4.5.2.

Algoritmo divide y vencer s a

Siguiendo la estrategia divide y vencer s el problema podra resolverse de a la siguiente forma: bn = b2b2 n n b2b2b
n n

si n es par si n es impar

El siguiente algoritmo resuelve el problema siguiendo esta estrategia: Algoritmo: Potenciaci n o Argumentos: b: base, n: exponente int potencia(int b, int n) { int res, aux; if (n == 0) res = 1; else { aux = potencia(b,n/2); if ((n % 2) == 0) res = aux * aux; else res = aux * aux * b; } return(res); }

Eciencia del algoritmo La relaci n de recurrencia que dene el comportamiento de este algoritmo es: o T (n) = c1 T ( n ) + c2 2 n<1 n1

Como se demostr en la secci n 4.4.1, esta relaci n induce un coste para el o o o algoritmo de O(log n).

102

4.6.

Otros problemas

Otros problemas cl sicos que tienen una soluci n m s eciente mediante Dia o a vide y Vencer s que con algoritmos directos o cl sicos son los siguientes: a a B squeda binaria en 2 dimensiones (en una matriz en vez de en un vector). u B squeda de una subcadena en cadenas de caracteres. u Hallar el mnimo de una funci n c ncava. o o B squeda del mnimo y del m ximo en un vector de enteros. u a Producto de enteros grandes. Multiplicaci n de matrices. o

103

4.7.

Ejercicios

Ejercicio 1: -Realiza una traza del algoritmo mergesort al aplicarlo sobre el vector A ={12, 8, 21, 19, 26}. Soluci n: o La traza del algoritmo mergesort sobre el vector A = 12 8 21 19 26 podra representarse de esta manera, donde los n meros entre par ntesis indican u e el orden de llamada a las funciones:
mergesort(A,0,4)
0 1 2 3 4 0 1 2 3 4

12 8

21 19 26 (12) merge(A,0,2,4)

8 12 19 21 26

(1) mergesort(A,0,2)

(8) mergesort(A,3,4)

12 8

21 (7) merge(A,0,1,2)

12 21

19 26 (11)merge(A,3,3,4)

19 26

(2)mergesort(A,0,1)

(6) mergesort(A,2,2)

(9)mergesort(A,3,3)

(10)mergesort(A,4,4)

12

8 (5) merge(A,0,0,1)

12

21

19

26

(3)mergesort(A, 0,0)

(4)mergesort(A,1,1)

12

104

o bien de esta otra manera: llamada inicial: Mergesort(A,0,4);


0 1 2 3 4

Mergesort(A,0,4) Mergesort(A,0,2) Mergesort(A,0,1) Mergesort(A,0,0) Mergesort(A,1,1)

12
0

8
1

21 19 26
2

12
0

8
1

21

12
0

12
1

8
0 1

Merge(A,0,0,1) Mergesort(A,2,2)

12
2

21
0 1 2

Merge(A,0,1,2) Mergesort(A,3,4) Mergesortsort(A,3,3) Mergesort(A,4,4)

12 21
3 4

19 26
3

19
4

26
3 4

Merge(A,3,3,4)
0 1 2

19 26
3 4

Merge(A,0,2,4)

12 19 21 26

105

Ejercicio 2: -Realiza una versi n del algoritmo mergesort que divida el vector en tres partes, o para ello, deber s escribir otro algoritmo de mezcla merge() que act e sobre a u tres subsecuencias ordenadas. OJO! ten cuidado al obtener los puntos de corte, pues la f rmula no es exactao mente igual que cuando se divide por la mitad. Ponte unos cuantos ejemplos para verlo. Ah! y ten cuidado porque a lo mejor hay m s de un caso base, ya que antes a el caso base era cuando quedaba un subvector de un solo elemento y ya estaba ordenado, ponte alg n ejemplo y ver s que pueden ocurrir otros casos. u a Soluci n: o Es f cil modicar la versi n del algoritmo mergesort vista en clase. Solo hay a o que tener cuidado al obtener los puntos de corte para que queden subproblemas balanceados. Despu s habr que modicar el algoritmo merge (de fusi n) para que e a o fusione 3 subsecuencias ordenadas, con lo que tendremos que hacer unas cuantas comprobaciones m s. Los dos algoritmos son: a void mergesort(int *A, int l, int r) { int m1, m2; int aux; if (l<r) /* si tiene tamanyo>1, lo ordenamos. */ if (l=(r-1)) { /* subvector de tamanyo 2. */ if (A[l]>A[r]) { aux = A[l]; A[l] = A[r]; A[r] = aux; } } else { m1 = (int ) (((r-l)/3)+l); m2 = (int ) (((2*(r-l))/3)+l); mergesort(A, l, m1); mergesort(A, m1 + 1, m2); mergesort(A, m2 + 1, r); merge(A, l, m1, m2, r); } }

106

void merge(int *A, int l, int m1, int m2, int r) { int i,j,k,q; int *B; B = (int *) malloc((r-l+1)*sizeof(int )); i=l; j=m1 + 1; k=m2 + 1; q=0; while ((i<=m1) && (j<=m2) && (k<=r)) { if ((A[i] <= A[j]) && (A[i] <= A[k])) { B[q] = A[i]; i++; } else if ((A[j] <= A[i]) && (A[j] <= A[k])) { B[q] = A[j]; j++; } else { B[q] = A[k]; k++; } q++; } while ((i<=m1) && (j<=m2)) { if (A[i] <= A[j]) { B[q] = A[i]; i++; } else { B[q] = A[j]; j++; } q++; }

107

while ((i<=m1) && (k<=r)) { if (A[i] <= A[k]) { B[q] = A[i]; i++; } else { B[q] = A[k]; k++; } q++; } while ((j<=m2) && (k<=r)) { if (A[j] <= A[k]) { B[q] = A[j]; j++; } else { B[q] = A[k]; k++; } q++; } while (i<=m1) { B[q] = A[i]; i++; q++; } while (j<=m2) { B[q] = A[j]; j++; q++; } while (k<=r) { B[q] = A[k]; k++; q++; } /* copiamos al vector original */ for (i=l; i<=r; i++) A[i] = B[i]; free(B); }

108

Ejercicio 3: -Realiza una traza del algoritmo de partici n que utiliza Quicksort, al aplicarlo o sobre el vector A={21, 27, 15, 5, 20}. Soluci n: o La traza del algoritmo partition (o de partici n), sobre el vector A= 21 27 15 o es la siguiente:
0 1 2 3 4 20 pivote=21 21 27 15 5

20

i 21 27 15 5

j 20 pivote=21

i 21 27 15 5

j 20 pivote=21

i 20 27 15 5

j 21 pivote=21

i 20 27 15 5

j 21 pivote=21

i 20 27 15

j 5 21 pivote=21

20 5 15 27 21 pivote=21

20 5 15 27 21 pivote=21

20 5 15 27 21 pivote=21 q=2 j i

109

Ejercicio 4: -Qu valor de q devuelve el algoritmo de partici n cuando todos los elementos e o del vector tienen el mismo valor? Y cuando el elemento escogido como pivote es el segundo menor del vector, si suponemos que no hay elementos repetidos en el vector? Soluci n: o Para responder la primera pregunta podemos ver antes un ejemplo donde todos los elementos del vector sean iguales y aplicar el algoritmo de partici n: o
l 5 5 5 r 5 piv=5

piv=5

i 5 5 5

j 5 piv=5

i 5 5 5

j 5 piv=5

Vemos que en el ejemplo devuelve la posici n j de la mitad del vector. Aunque o f cilmente podemos ver que la soluci n ser siempre la posici n q = (int)(l + a o a o r)/2, o sea, la posici n de la mitad del vector. Adem s se habr n intercambiado o a a todos los elementos del vector. El que se realicen todos estos intercambios es debido a que todos los elementos que comprueba el bucle de la j (del algoritmo partici n) son menores o iguales (realmente son iguales) que el pivote y por tanto o tiene que moverlos, y a su vez, todos los elementos que comprueba el bucle de la i son mayores o iguales (son iguales) que el pivote y por tanto tiene que moverlos. La segunda pregunta fue respondida en clase cuando se realiz el an lisis o a de casos para el coste medio de Quicksort. Si suponemos que no hay elementos repetidos, nos devolvera un valor q = 1, o lo que es lo mismo, un subvector formado por el elemento A[1] y otro subvector formado por los elementos A[2]. . . A[n].

110

Esto es debido a que desde el principio del vector hasta la posici n q s lo poo o dr n quedar elementos menores o iguales que el pivote, como no hay elementos a repetidos en nuestro caso solo podr haber dos elementos como m ximo, el mnia a mo del vector y el pivote, ya que el pivote es el segundo menor del vector. Sin embargo, el pivote tampoco estar en ese subvector porque siempre se mueve a la a parte de la derecha de q (por c mo se deni el algoritmo partici n), con lo que o o o s lo nos queda el elemento mnimo del vector desde el inicio del vector hasta q, o por tanto q = 1.

111

Ejercicio 5: -Dado un entero n, n > 0 se dene la raz cuadrada entera de n como n . Dicho de otro modo, es la parte entera del n mero real que representa la n. Podemos u denir la raz cuadrada entera de esta manera: n = z | z es entero y z 2 n < (z + 1)2 Siguiendo esquema Divide y Vencer s, escribe una funci n que calcule eel a o n . A esta funci n la podemos llamar raiz_entera. o cientemente Nota: La idea es que buscamos un n mero z que cumpla z 2 n < (z + 1)2 . u Hay que jarse que la raz entera estar incluida en un intervalo [x, y], donde los a n meros x e y son tales que 1 x y n. As pues, nuestra idea Divide y u Vencer s va a ser aplicar una reducci n eciente en ese intervalo. Es decir, hacer a o cada vez m s peque o el intervalo [x, y] hasta que demos con la soluci n. a n o El esquema del algoritmo va a ser, dado un n mero n, comenzaremos conu siderando el intervalo [1, n], es decir, x = 1 e y = n, a partir de ah: Comprobar si x = y, entonces estaremos buscando en un intervalo con un solo n mero y habremos dado con la soluci n, devolveremos z = x. En u o cualquier otro caso: intentaremos recortar el intervalo de b squeda de la soluci n, para ello, lo u o partimos por la mitad y vemos en cual de las dos mitades debemos seguir buscando la soluci n. Hacemos z = x+y y ahora hay que saber si debemos o 2 seguir buscando en [x, z ] o en [z + 1, y], para ello: como buscamos un z que cumpla z 2 n, entonces si (z )2 > n = es que buscamos un n mero m s bajo que z , as pues, la z a buscar u a estar en [x, z ], con lo que aplicaremos la funci n raz_entera al a o intervalo [x, z ] y la soluci n sera la que nos devuelva esta funci n. o o como buscamos un z que cumpla n < (z + 1)2 , entonces si (z + 1)2 n = es que buscamos un n mero m s alto que z , as pues, u a la z a buscar estar en [z + 1, y], con lo que aplicaremos la funci n a o raz_entera al intervalo [z + 1, y] y la soluci n sera la que nos o devuelva esta funci n. o en cualquier otro caso es que est bamos buscando la z , as pues dea volvemos z .

112

Soluci n: o Dado un n mero entero n > 0, la llamada inicial a la funci n que calcuu o la su raz cuadrada entera mediante el anterior esquema divide y vencer s sera a raiz_entera(1,n,n). Y la funci n en lenguaje C sera: o int raiz_entera(int x, y, n){ int z, z_cuadrado, z_mas_1_cuadrado; if (x==y) return(x); else { z = (int ) (x+y)/2; /* calculo la mitad del intervalo [x,y] */ /* Ahora hay que averiguar si toca buscar la raiz */ /* de n en el intervalo [x,z] o en [z+1,y] */ z_cuadrado = z*z; /* z_cuadrado = z2 */ z_mas_1_cuadrado = (z+1)*(z+1); /* z_mas_1_cuadrado = (z+1)2 */ if (z_cuadrado > n) return( raiz_entera(x,z,n) ); if (z_mas_1_cuadrado <= n) return( raiz_entera(z+1,y,n) ); return(z); } } Si nos jamos podemos observar que el coste de este algoritmo sera O(log n), ya que el an lisis del coste sera igual que en el caso de la b squeda binaria. a u Vamos partiendo un intervalo de tama o n por la mitad hasta que encontramos la n soluci n. o /* la raiz esta en un intervalo */ /* de un solo numero */

113

Tema 5 Arboles
5.1. Deniciones
Un arbol de tipo base T puede ser denido como [Wirth]: 1. Un conjunto vaco es un arbol, 2. Un nodo n de tipo T , junto con un n mero nito de conjuntos disjuntos u de elementos de tipo base T , llamados sub rboles, es un arbol. El nodo a n se denomina raz. Se dice que el nodo n es padre de las races de los sub rboles, y que las a races de los sub rboles son hijos de n. a Por ejemplo, sea el tipo de base T el conjunto de las letras del alfabeto; un arbol puede ser representado mediante un grafo de la siguiente forma:

A B E C F D J

GH I K LM
Figura 5.1: Representaci n de un arbol o Se denomina camino de n1 a nk , a una sucesi n de nodos de un arbol o n1 , n2 , . . . , nk , en la que ni es padre de ni+1 para 1 i < k. Por ejemp lo, en el arbol de la gura 5.1, el camino de A hasta I es: A C E I.

114

La longitud de un camino es el n mero de nodos del camino menos 1. Por u lo tanto, hay un camino de longitud cero de cualquier nodo a s mismo. Por ejemplo en el arbol de la gura 5.1, la longitud del camino de A hasta I es: 3. Si existe un camino de un nodo ni a otro nj , entonces ni es un antecesor de nj , y nj es un descendiente o sucesor de ni . Por lo tanto, en un camino n1 , n2 , . . . , nk se dice que n1 , n2 , . . . , ni , 1 i k son antecesores de ni ; an logamente, ni , ni+1 , . . . , nk , 1 i k son descendientes de ni . a Obs rvese que cada nodo es a la vez un antecesor y un descendiente de e s mismo. Por ejemplo, en el arbol de la gura 5.1, los antecesores de E son: < A, C, E > y los descendientes: < E, G, H, I >. Un antecesor o un descendiente de un nodo que no sea el mismo recibe el nombre de antecesor propio o descendiente propio, respectivamente. En un arbol, la raz ser el unico nodo que no tiene antecesores propios. Por a ejemplo, en el arbol de la gura 5.1, los antecesores propios de E son: < A, C > y los descendientes propios: < G, H, I >. En un camino n1 , n2 , . . . , nk , ni es antecesor directo de ni+1 , y ni+1 es descendiente directo de ni , para 1 i < k. Por ejemplo, en el arbol de la gura 5.1, el antecesor directo de E es: < C > y los descendientes directos: < G, H, I >. Una hoja o nodo terminal es un nodo sin descendientes propios. Por ejem plo, en el arbol de la gura 5.1, los nodos terminales son: < B, G, H, I, F , K, L, M >. Un nodo interior es un nodo con descendientes propios. Por ejemplo, en el arbol de la gura 5.1, los nodos interiores son: < A, C, D, E, J >. Se denomina grado de un nodo al n mero de descendientes directos que u tiene. Por ejemplo, en el arbol de la gura 5.1, el grado del nodo E es: 3. Se denomina grado de un arbol al m ximo de los grados de todos los nodos a que pertenecen a el. Por ejemplo, el arbol de la gura 5.1 es de grado 3. Denimos recursivamente nivel como: 1. La raz del arbol est a nivel 1. a 2. Si un nodo est en el nivel i, entonces sus descendientes directos est n a a al nivel i + 1.

115

Se denomina altura de un nodo, a la longitud del camino m s largo desde a ese nodo hasta una hoja. La altura del nodo raz es la altura del arbol. Por ejemplo, en el arbol de la gura 5.1, la altura del arbol es: 3, y la altura del nodo E: 1. Se denomina profundidad de un nodo a la longitud del camino unico desde la raz hasta ese nodo. Por ejemplo, en el arbol de la gura 5.1, la profundi dad del nodo E es: 2.

5.2.

Recorrido de arboles

Una tarea muy habitual cuando utilizamos un arbol, es realizar una determi nada operaci n P con cada uno de los elementos del arbol. Para ello es necesario o recorrer cada uno de los nodos del arbol. Las tres formas m s importantes de a recorrer un arbol son: recorrido en orden previo (preorden), recorrido en orden sim trico (inorden), e recorrido en orden posterior (postorden). Dependiendo del tipo de recorrido que se realice, la acci n P se llevar a cabo o a en cada nodo de la siguiente forma: si un arbol A es vaco, entonces no se realiza la acci n P ; o si A contiene un unico nodo, entonces se realiza la acci n P sobre ese nodo; o si A es un arbol con raz n y sub rboles A1 , A2 , . . . , Ak de izquierda a a derecha, entonces: En el recorrido en preorden se realiza la acci n P sobre el nodo raz n, o y, a continuaci n, se recorren en preorden los sub rboles A1 , A2 , . . . , Ak o a de izquierda a derecha. En el recorrido en inorden se recorre en inorden el sub rbol A1 , dea spu s se realiza la acci n P sobre el nodo raz n, y, a continuaci n, se e o o recorren en inorden los sub rboles A2 , . . . , Ak de izquierda a derecha. a En el recorrido en postorden se recorren en postorden los sub rboles a A1 , A2 , . . . , Ak de izquierda a derecha, y despu s se realiza la acci n e o P sobre el nodo n.

116

Cualquiera de los tres recorridos de arboles descritos tiene un coste O(n x), siendo n el n mero de nodos del arbol y x el coste de la acci n P . u o Ejemplo: o Si se recorriera el arbol de la gura 5.1 en preorden, la acci n P sobre cada nodo se realizara en el siguiente orden: A, B, C, E, G, H, I, F , D, J, K, L, M . Si se recorriera el arbol de la gura 5.1 en inorden, la acci n P sobre cada o nodo se realizara en el siguiente orden: B, A, G, E, H, I, C, F , K, J, L, M , D. o Si se recorriera el arbol de la gura 5.1 en postorden, la acci n P sobre cada nodo se realizara en el siguiente orden: B, G, H, I, E, F , C, K, L, M , J, D, A. Ejemplo: Un ejemplo del uso de recorrido de arboles sera ver c mo se puede evaluar o una expresi n artim tica con la cl sica precedencia de operadores representada o e a mediante un arbol. Para ello necesitaremos realizar un recorrido en postorden que para cada nodo se encargar de obtener primero los valores de sus nodos a hijos, esto es, los valores de los operandos evaluando recursivamente cada uno de ellos. Por ultimo se llevar a cabo la operaci n asociada a ese nodo: a o /S / / / 5

  

SS SS S

DD DD D

+E  EEE EE  

30

 CC  CCC  C 

+C  CCC  CC 

+E  EEE EE  

30

 CC  CCC  C    

+C

CC CC C

30

 BB  BBB  B 

+C  CCC  CC 

5.3.
5.3.1.

Representaci n de arboles o
Representaci n mediante listas de hijos o

Una manera importante y util de representar arboles consiste en formar una lista de los hijos de cada nodo. Debido a que el n mero de hijos que puede tener u cada nodo es variable, las listas enlazadas resultar n ser la representaci n m s a o a apropiada para representar las listas de hijos. La estructura de datos utilizada para este tipo de representaci n, consistir en: o a 1. Un vector v para almacenar la informaci n de cada nodo; o

117

2. cada elemento (nodo) del vector apunta a una lista enlazada de elementos (nodos) que indican cu les son sus nodos hijos. Los elementos de la lista a encabezada por v[i] ser n los hijos del nodo i. a Es conveniente tener una variable que indique cual es el nodo raz, aunque no sera necesaria, puesto que para obtener la raz se podra buscar aquel nodo que no tiene padre. Sin embargo, conocer la raz evitara el coste de esa b squeda. u Ejemplo de representaci n de un arbol mediante listas de hijos o
1 2 3 4 5 6 raiz 7 8 9 10 11 12 13 14 4
/ / /

7 3 1 5 8 12 11

1
/

8 5

3
/ / /

12

4 2 6 9 13 10

9 11
/

13
/

10

...

Ventajas de esta representaci n o


Es una representaci n simple. o Facilita las operaciones de acceso a los hijos.

Inconvenientes de esta representaci n o


Se desaprovecha memoria. Las operaciones para acceder al padre de un nodo son costosas. Ejercicio Sea la siguiente denici n de tipos en C: o

118

#dene N ... typedef struct snodo { int e; struct snodo *sig; } nodo; typedef struct { int raiz; nodo *v[N]; } arbol; Escribir una funci n recursiva que imprima en preorden los ndices de los o nodos de un arbol T que use esta representaci n. o void preorden(arbol *T, int n) { nodo *aux; aux = T->v[n]; printf("%d ",n); /* Accion sobre nodo */ while (aux != NULL){ preorden(T,aux->e); aux = aux->sig; } }

5.3.2.

Representaci n hijo m s a la izquierda hermano dereo a cho

Otra posible representaci n de un arbol es tener, para cada nodo, una estructura o en la que se guarda la siguiente informaci n: o 1. clave: valor de tipo base T almacenado en el nodo. 2. hijo izquierdo: hijo m s a la izquierda del nodo. a 3. hermano derecho: hermano derecho del nodo.

119

Ejemplo de representaci n de un arbol mediante esta estructura o

A
/

A
B C E G
/

D
/

B E

C F

D J

F
/ /

J
/

GH I K LM
H
/

I
/ /

K
/

L
/

M
/ /

Una posible variante de esta representaci n que facilita el acceso al padre deso de un nodo hijo, es enlazar el nodo hijo que ocupa la posici n m s a la derecha o a con su nodo padre. Para ello ser necesario especicar, en cada nodo, si el puntero a al hermano derecho apunta a un hermano o al padre. Ejemplo de representaci n de un arbol mediante esta variante o

A
/

A B E C F D J

B
/

C E F
/

D J I
/

GH I K LM
G
/

H
/

K
/

L
/

M
/

Ventajas de esta representaci n o


Facilita las operaciones de acceso a los hijos y al padre de un nodo. Uso eciente de la memoria.

120

Inconvenientes de esta representaci n o


El mantenimiento de la estructura es complejo. Ejercicio Sea la siguiente denici n de tipos en C: o typedef struct snodo { char clave[2]; struct snodo *hizq; struct snodo *der; } arbol; Escribir una funci n que calcule de forma recursiva la altura de un arbol T. o Como buscamos una estrategia recursiva, la idea va a ser calcular la altura de cada uno de los hijos del nodo raz y sumarle 1 a la altura m xima que hayamos a obtenido. El esquema recursivo se encuentra en el c lculo de la altura de los nodos a hijos del nodo actual. int altura(arbol *T) { arbol *aux; int maxhsub=0, hsub; if (T == NULL) return(0); else if (T->hizq == NULL) return(0); else { aux = T->hizq; while ( aux != NULL) { hsub = altura(aux); if (hsub > maxhsub) maxhsub = hsub; aux = aux->der; } return(maxhsub + 1); } }

121

5.4.

Arboles binarios

Denici n: Un arbol binario es un conjunto nito de nodos tal que, o est vaco, o a o consiste en un nodo especial llamado raz, y el resto de nodos se agrupan en dos arboles binarios disjuntos llamados sub rbol izquierdo y sub rbol derecho. a a

A B D G
subarbol izquierdo

nodo raiz

C E F
subarbol derecho

Figura 5.2: Ejemplo de arbol binario

A B D E G
(a)

A C F D G
(b)

B E

C F

Figura 5.3: Ejemplo de dos arboles binarios distintos. El nodo E del arbol binario (a) no tiene hijo izquierdo y s que tiene hijo derecho, mientras que en el arbol binario (b), el nodo E s que tiene hijo izquierdo y no tiene hijo derecho.

5.4.1.

Representaci n de arboles binarios o

A continuaci n se muestran diferentes estructuras de datos que sirven para o representar arboles binarios. Representaci n mediante vectores o La estructura de datos utilizada para este tipo de representaci n, consistir en: o a

122

Un vector v para almacenar la informaci n de cada nodo; o cada elemento del vector (nodo), ser una estructura que almacenar la sia a guiente informaci n: o 1. clave: valor de tipo base T almacenado en el nodo. 2. hijo izquierdo: ndice del nodo que es hijo izquierdo. 3. hijo derecho: ndice del nodo que es hijo derecho. Es conveniente tambi n guardar en una variable el ndice del nodo raz para e saber d nde empieza el arbol. o Ejemplo de representaci n de un arbol mediante esta estructura o

A B D G E C F

/ / / 5 / 4 / / 7 /

/ F / A D B C G E

. . .

/ / / 6 / 8 1 / / /

0 1 2 3 raiz 4 5 6 7 8 N1

La siguiente denici n de tipos en C, nos servira para denir una estructura o de datos que se ajuste a esta representaci n. o #dene N ... typedef ... tipo_baseT; typedef struct { tipo_baseT e; int hizq, hder; } nodo; typedef struct { int raiz; nodo v[N]; } arbol; 123

Representaci n mediante variables din micas o a Otra posible representaci n de un arbol binario es tener, para cada nodo, una o estructura en la que se guarda la siguiente informaci n: o 1. clave: valor de tipo base T almacenado en el nodo. 2. hijo izquierdo: puntero al hijo izquierdo. 3. hijo derecho: puntero al hijo derecho.

A B D G E
B C
/

C F
D
/ /

E
/

F
/ /

G
/ /

La siguiente denici n de tipos en C, nos servira para denir una estructura o de datos que se ajuste a esta representaci n. o typedef ... tipo_baseT; typedef struct snodo { tipo_baseT e; struct snodo *hizq, *hder; } arbol; Una posible variante a este tipo de representaci n, que facilitara el acceso al o nodo padre desde un nodo hijo, sera almacenar tambi n, en cada nodo, un puntero e al nodo padre. La siguiente gura muestra este tipo de representaci n. o

124

A B D G E
B

C
/

C F
D
/ /

E
/

F
/ /

G
/ /

La siguiente denici n de tipos en C, nos servira para denir una estructura o de datos que se ajuste a esta representaci n. o typedef ... tipo_baseT; typedef struct snodo { tipo_baseT e; struct snodo *hizq, *hder, *padre; } arbol;

5.4.2.

Recorrido de arboles binarios

El recorrido de arboles binarios podr hacerse, al igual que ocurra para cualquier a tipo de arbol, de tres formas distintas: recorrido en orden previo (preorden) recorrido en orden sim trico (inorden) e recorrido en orden posterior (postorden) Suponiendo que se tuviera que realizar una misma acci n P sobre cada nodo o del arbol, el procedimiento a seguir para cada uno de los distintos recorridos sera: Preorden(x) si x = VACIO entonces AccionP(x) Preorden(hizquierdo(x)) Preorden(hderecho(x)) 125

Inorden(x) si x = VACIO entonces Inorden(hizquierdo(x)) AccionP(x) Inorden(hderecho(x)) Postorden(x) si x = VACIO entonces Postorden(hizquierdo(x)) Postorden(hderecho(x)) AccionP(x) El coste de todos los algoritmos es (n), siendo n el n mero de nodos del u arbol. Ejercicio Suponiendo que la representaci n del arbol binario se realice mediante vario ables din micas, escribir una funci n en C para cada tipo de recorrido, de forma a o que la acci n P sea escribir la clave del nodo. o typedef int tipo_baseT; typedef struct snodo { tipo_baseT e; struct snodo *hizq, *hder; } arbol; void preorden(arbol *a) { if (a != NULL) { printf("%d ",a->e); preorden(a->hizq); preorden(a->hder); } } void inorden(arbol *a) { if (a != NULL) { inorden(a->hizq); printf("%d ",a->e); inorden(a->hder); } }

126

void postorden(arbol *a) { if (a != NULL) { postorden(a->hizq); postorden(a->hder); printf("%d ",a->e); } } Ejercicio Dada la siguiente denici n de tipos para especicar la estructura de un arbol o binario de enteros: typedef int tipo_baseT; typedef struct _nodo { tipo_baseT info; struct _nodo *hizq, *hder; } arbol; y dado un arbol binario T , declarado como: arbol *T; Escribir una funci n en C que elimine todas las hojas del arbol binario, dejano do exclusivamente los nodos del arbol que no lo son. Considerar que eliminar las hojas de un arbol vaco deja el arbol vaco. arbol *elimina_hojas(arbol *T) { if (T == NULL) return(NULL); else if ( (T->hizq == NULL) && (T->hder == NULL) ) { free(T); return(NULL); } else { T->hizq = elimina_hojas(T->hizq); T->hder = elimina_hojas(T->hder); return(T); } }

127

5.4.3.

Arbol binario completo. Representaci n. o

Denici n: Un arbol binario completo es un arbol binario en el cual todos los o niveles tienen el m ximo n mero posible de nodos excepto, puede ser, el ultimo. a u En ese caso, las hojas del ultimo nivel est n tan a la izquierda como sea posible. a En la gura 5.4 se muestra un ejemplo de arbol binario completo.

Figura 5.4: Ejemplo de arbol binario completo Los arboles binarios completos pueden ser representados mediante un vector de la siguiente forma: En la posici n 1 se encuentra el nodo raz del arbol. o Dado un nodo que ocupa la posici n i en el vector: o En la posici n 2i se encuentra el nodo que es su hijo izquierdo. o En la posici n 2i + 1 se encuentra el nodo que es su hijo derecho. o En la posici n i/2 se encuentra el nodo padre si i > 1. o

1 7 2 10 4 3 8 4 9 10 2 13 11 11 19 5 6 5 1 16 7
1 2 3 4 5 6 7 8 9 10 11

7 10 16 3 11 5

1 4 13 2 19

Figura 5.5: Representaci n de un arbol binario completo mediante un vector. o

128

Ejercicio Escribe tres funciones en C que, dado un nodo de un arbol binario completo representado mediante un vector, calculen la posici n del vector en la que se o encuentra el nodo padre, el hijo izquierdo y el hijo derecho, respectivamente. int hizq(int i) { return(2*i); }

int hder(int i) { return((2*i)+1); }

int padre(int i) { return(i/2); }

5.4.4.

Propiedades de los arboles binarios

Por ultimo, veamos cu les son algunas de las propiedades m s importantes de a a los arboles binarios: El m ximo n mero de nodos en el nivel i es 2i1 , i 1. a u En un arbol de i niveles hay como m ximo 2i 1 nodos, i 1. a En un arbol binario no vaco, si n0 es el n mero de hojas y n2 es el n mero u u de nodos de grado 2, se cumple que n0 = n2 + 1. La altura de un arbol binario completo que contiene n nodos es log2 n .

129

Nivel 1 2 altura = 3 3

Figura 5.6: Ejemplo de un arbol binario completo con el m ximo numero de noa dos. Ejercicio: Comprueba sobre el arbol de la gura 5.6 que se cumple cada una de las propiedades citadas. M ximo nodos por nivel a Nivel 1 20 = 1 Nivel 2 21 = 2 Nivel 3 22 = 4 Nivel 4 23 = 8 M ximo nodos en el arbol a 24 1 = 15 N mero de hojas (n0 ) u n2 = 7 n0 = n2 + 1 = 7 + 1 = 8 Altura del arbol binario completo n = 15 log2 n = log2 (15) = 3

130

5.5.

Ejercicios

Ejercicio 1: -Si la acci n P fuera escribir la etiqueta del nodo, indica en qu orden se eso e cribiran cada una de las etiquetas del siguiente arbol, para cada uno de los recor ridos: preorden, inorden y postorden.

A B E HIJK O T
Cu l es la altura del arbol? Y el grado? a Soluci n: o Preorden: A, B, E, H, I, O, T, P, J, K, C, F, G, L, M, N, Q, R, S, D Inorden: H, E, T, O, I, P, J, K, B, A, F, C, L, G, M, Q, N, R, S, D Postorden: H, T, O, P, I, J, K, E, B, F, L, M, Q, R, S, N, G, C, D, A El arbol tiene altura 5 y grado 4.

C F G

LMN Q RS

131

Ejercicio 2: -Para adoptar la representaci n de arboles binarios mediante vectores, se propone o la siguiente denici n de tipos, constantes y variables en lenguaje C: o #dene N 100 typedef int tipo_baseT; typedef struct{ tipo_baseT e; int hizq, hder; } nodo; typedef struct{ int raiz; nodo v[N]; } arbol; arbol *T; a) Escribir una funci n recursiva con el perl: o void preorden(arbol *T, int m) que imprima las claves de los nodos en preorden, cuando la llamada inicial es preorden(T,T->raiz);. b) Escribir una funci n recursiva con el perl: o int altura(arbol *T, int m) que determine la altura de un arbol binario T, cuando la llamada inicial es H=altura(T,T->raiz);. Cu l ser el coste temporal del algoritmo? a a c) Escribir una funci n recursiva con el perl: o void espejo(arbol *T, int m) que intercambie los hijos izquierdo y derecho de cada nodo, cuando la llamada inicial es espejo(T, T->raiz);. Cu l ser el coste temporal a a del algoritmo?

132

Soluci n: o a) Debemos aplicar un recorrido en preorden como el comentado en clase, pero debemos tener en cuenta que en este caso se especica el nodo en que nos encontramos en cada llamada que hacemos a la funci n preorden: o void preorden(arbol *T, int m) { if (T!=NULL) if (m!=-1) { printf("%d, ",T->v[m].e); preorden(T, T->v[m].hizq); preorden(T, T->v[m].hder); } } b) El esquema recursivo de la funci n vendr dado porque la altura de un nodo o a determinado ser uno m s el m ximo de las alturas de sus dos hijos: a a a int altura(arbol *T, int m) { int alt_hizq, alt_hder; if (T!=NULL) if (m!=-1) { alt_hizq = altura(T,T->v[m].hizq); alt_hder = altura(T,T->v[m].hder); if (alt_hizq>alt_hder) return(1+alt_hizq); else return(1+alt_hder); } else return(-1); } El coste del algoritmo es O(n), siendo n el n mero de nodos del arbol, u puesto que tendremos que ir calculando la altura de todos los nodos del arbol para conocer nalmente la altura del arbol, y por ello tendremos que, en cierta manera, recorrer el arbol. c) Una soluci n iterativa (que no se demanda) al problema sera recorrer el vector o que representa al arbol con un bucle for e intercambiar hizq y hder para todos los nodos. El esquema recursivo del procedimiento espejo es debido a que una vez hayamos dado la vuelta a los hijos del nodo actual, quedara por dar la vuelta a todos los hijos del sub rbol derecho y el sub rbol izquierdo del a a nodo actual, con lo que podramos usar el mismo procedimiento aplic ndolo a al hijo izquierdo y al hijo derecho respectivamente: 133

void espejo(arbol *T, int m) { int aux; if (T!=NULL) if (m!=-1) { aux = T->v[m].hizq; T->v[m].hizq = T->v[m].hder; T->v[m].hder = aux; espejo(T, T->v[m].hizq); espejo(T, T->v[m].hder); } } El coste del algoritmo es O(n), siendo n el n mero de nodos del arbol, u puesto que tendremos que dar la vuelta a todos los hijos de todos los nodos internos del arbol. Al igual que en el apartado anterior, tendremos que recorrer todo el arbol. Un ejemplo es:
A B D E G 1 2 3 2 A 3 4 B 1 5 C 6 7 1 1 2 3 C F E G 3 A 2 4 B 1 5 C 6 7 1 1 2 3 C F D A B F C E D G 3 A 2 4 B 1 6 C 5 7 1 A B

4 1 D 1 5 1 E 6 1 F

4 1 D 1 5 1 E 6 1 F

4 1 D 1 5 1 E 6 1 F

7 1 G 1

7 1 G 1

7 1 G 1

Y continuar as hasta que termine el algoritmo.

134

Ejercicio 3: -Para adoptar la representaci n de arboles binarios mediante variables din micas, o a se propone la siguiente denici n de tipos y variables en lenguaje C: o typedef int tipo_baseT; typedef struct _nodo{ tipo_baseT info; struct _nodo *hizq, *hder; } arbol; arbol *T1, *T2; Se solicita: a) Escribir una funci n recursiva con el perl: o arbol *buscar(arbol *T, tipo_baseT m) que devuelva un puntero al nodo de clave m del arbol binario T. Si el nodo de clave m no se encuentra en el arbol, la funci n devolver NULL. Esta o a funci n puede verse como una funci n que devuelve un puntero al sub rbol o o a cuya raz es el nodo de clave m. Cu l ser el coste temporal del algoritmo? a a b) Suponiendo que ya hemos obtenido la funci n buscar de la cuesti n anterior, o o utilizarla para escribir un procedimiento recursivo con el perl: void interseccion(arbol *T1, arbol *T2) que imprima las claves de los nodos que son el resultado de obtener la inter secci n de los arboles binarios T1 y T2; es decir, las claves de los nodos que o pertenecen a la vez a T1 y T2. Cu l ser el coste temporal del algoritmo? a a c) Suponiendo que ya hemos obtenido la funci n buscar de la cuesti n a), utio o lizarla para escribir un procedimiento recursivo con el perl: void diferencia(arbol *T1, arbol *T2) que imprima las claves de los nodos que son el resultado de obtener la diferencia T1-T2; es decir, las claves de los nodos que pertenecen a T1 y no pertenecen a T2. Cu l ser el coste temporal del algoritmo? a a

135

Soluci n: o a) Habr que hacer un recorrido por el arbol hasta que se encuentre el nodo busa cado, la unica diferencia con un recorrido tradicional es que una vez se haya encontrado, debe devolverse el puntero correspondiente y no seguir explorando nuevos nodos. A partir de un esquema similar al del recorrido en preorden podemos obtener esta funci n: o arbol *buscar(arbol *T, tipo_baseT m) { arbol *T_aux; if (T!=NULL) { if (T->info == m) return(T); else { T_aux = buscar(T->hizq, m); if (T_aux != NULL) return(T_aux); else { T_aux = buscar(T->hder, m); return(T_aux); } } } else return(T); } El coste del algoritmo ser O(n), siendo n el n mero de nodos en el arbol. a u Esto es debido a que en el caso peor (que el nodo buscado fuera el ultimo visitado durante el recorrido del arbol) podramos tener que recorrer todos los nodos del arbol. b) El procedimiento debe imprimir aquellos nodos de T1 cuya clave tambi n sea e clave de un nodo de T2. Para ello, recorreremos todos los nodos de T1, y para cada uno de ellos comprobaremos con la funci n buscar si existe un o nodo en T2 con la misma clave, en el caso en que se encuentre en T2, lo imprimiremos: void interseccion(arbol *T1, arbol *T2) { if (T1!=NULL){ if (buscar(T2,T1->info)!= NULL) printf("%d, ",T1->info); interseccion(T1->hizq, T2); interseccion(T1->hder, T2); } } 136

El coste del algoritmo ser O(n1 n2 ), siendo n1 el n mero de nodos del a u arbol T1 y n2 el n mero de nodos del arbol T2. Esto es debido a que hay u que recorrer todos los nodos del arbol T1 y, para cada uno de esos nodos, ejecutar una operaci n buscar sobre T2, que tiene un coste O(n2 ). o c) Es sencillo, el procedimiento debe imprimir aquellos nodos de T1 cuya clave no sea la clave de ning n nodo de T2. Para ello, recorreremos todos los nou dos de T1, y para cada uno de ellos comprobaremos con la funci n buscar o si existe un nodo en T2 con la misma clave, en el caso en que no se encuentre en T2, lo imprimiremos: void diferencia(arbol *T1, arbol *T2) { if (T1 != NULL) { if (buscar(T2,T1->info) == NULL) printf("%d, ",T1->info); diferencia(T1->hizq, T2); diferencia(T1->hder, T2); } } El coste del algoritmo ser O(n1 n2 ), siendo n1 el n mero de nodos del a u arbol T1 y n2 el n mero de nodos del arbol T2. Esto es debido a que hay u que recorrer todos los nodos del arbol T1 y, para cada uno de esos nodos, ejecutar una operaci n buscar sobre T2, que tiene un coste O(n2 ). o

137

Ejercicio 4: -Supongamos que estamos representado arboles geneal gicos en un pas donde o cada pareja s lo puede tener un hijo, por ejemplo: o
T1 R2D2

Han

Leia

Darth

Amidala

La denicion del tipo de datos es: typedef struct snodo { char nombre[20]; struct snodo *papa, *mama, *hijo; } arbol; En el caso en el que tuvieramos otro arbol:
T2 Manolet

Pepet

Marieta

Se pide implementar una funci n nacimiento() de manera que con la llao mada T=nacimiento(T1,T2,"Homer"); se obtenga el arbol:

138

Homer

R2D2

Manolet

Han

Leia

Pepet

Marieta

Darth

Amidala

Soluci n: o arbol *nacimiento(arbol *T1, arbol *T2, char *nombre) { arbol *aux; aux=(arbol *)malloc(sizeof(arbol)); strcpy(aux->nombre,nombre); aux->hijo=NULL; aux->papa=T1; aux->mama=T2; return(aux); }

139

Tema 6 Representaci n de Conjuntos o


6.1. Conceptos generales

Denici n: Un conjunto es una colecci n de miembros o elementos distintos; cada o o miembro de un conjunto puede ser , a su vez, un conjunto, o un atomo o tem. Un conjunto que contiene elementos repetidos se denomina multiconjunto.

6.1.1.

Representaci n de conjuntos o

Un conjunto puede representarse de distintas formas: Representaci n explcita: se enumeran todos los elementos del conjunto o encerr ndolos entre llaves. a C = {1, 4, 7, 11} Representaci n mediante propiedades: se especica alguna propiedad que o caracterice a los elementos del conjunto. C = {x N | x es par}

6.1.2.

Notaci n o

La relaci n fundamental en teora de conjuntos es la de pertenencia, que se o indica por el smbolo .Se dice que un elemento x pertenece al conjunto A, x A, cuando x es un miembro del conjunto A; x puede ser un tem u otro conjunto, pero A no puede ser un tem. Se utiliza x A para se alar que x no es / n un miembro de A. Al n mero de elementos que contiene un conjunto se le denomina cardinaliu dad o talla de un conjunto. 140

Existe un conjunto especial, llamado conjunto vaco o nulo, que no tiene miembros (de cardinalidad 0), y se simboliza con . Se dice que un conjunto A est incluido (o contenido) en un conjunto B, y se a escribe A B o B A, si todo miembro de A tambi n es miembro de B. En e este caso se dice que A es un subconjunto de B. Todo conjunto es un subconjunto de s mismo, y el conjunto vaco es un subconjunto de todo conjunto. Dos conjuntos son iguales si cada uno de ellos est incluido en el otro, A B a y B A; es decir, si sus miembros son los mismos. Un conjunto A es un subconjunto propio de otro B, si A = B y A B.

6.1.3.
cia.

Operaciones elementales sobre conjuntos

Las operaciones elementales con conjuntos son: uni n, intersecci n y difereno o

Uni n de dos conjuntos: Si A y B son conjuntos, entonces la uni n de A y o o B, A B, es el conjunto de los elementos que son miembros de A, de B, o de ambos. Intersecci n de dos conjuntos: Si A y B son conjuntos, la intersecci n de A o o y B, A B, es el conjunto de los elementos que pertenecen, a la vez, tanto a A como a B. Diferencia de dos conjuntos: Si A y B son conjuntos, la diferencia, A B, es el conjunto de los elementos que pertenecen a A y no pertenecen a B.

6.1.4.

Conjuntos din micos a

Un conjunto en el que sus miembros pueden variar a lo largo del tiempo, se denomina conjunto din mico. Tpicamente, un algoritmo que gestione un cona junto din mico, representar cada uno de sus elementos mediante un objeto que a a contendr la siguiente informaci n: a o clave: valor que identica al elemento. informaci n sat lite: informaci n asociada al elemento. o e o En algunos conjuntos din micos puede establecerse una relaci n de orden total a o entre las claves; por ejemplo, si las claves son n meros enteros, reales, palabras u (orden alfab tico), etc. La existencia de un orden total, permitir denir el mnimo e a y el m ximo elemento de un conjunto, o hablar del predecesor o sucesor de un a elemento dado. 141

Operaciones sobre conjuntos din micos a Las operaciones que se pueden realizar sobre conjuntos din micos pueden ser a agrupadas en dos categoras: Consultoras(queries): La operaci n simplemente devuelve alg n tipo de ino u formaci n sobre el conjunto. o Modicadoras: Modican el conjunto. A continuaci n, se enumeran las operaciones m s usuales que se realizan soo a bre conjuntos din micos: a Dado un conjunto S, y un elemento x tal que clave(x) = k. 1. Consultoras: Buscar(S,k): Devuelve el elemento x S tal que clave(x) = k; si x S devuelve un valor especial (valor nulo). / Vaco(S): Indica si el conjunto S es vaco o no. Las siguientes operaciones se pueden realizar si en el conjunto S existe una relaci n de orden total entre las claves de sus miembros. o Mnimo(S): Devuelve el elemento x con la clave k m s peque a del a n conjunto S. M ximo(S): Devuelve el elemento x con la clave k m s grande del a a conjunto S. Predecesor(S,x): Devuelve el elemento de clave inmediatamente inferior a la de x, o un valor especial si x es el elemento de clave mnima. Sucesor(S,x): Devuelve el elemento de clave inmediatamente superior a la de x, o un valor especial si x es el elemento de clave m xima. a 2. Modicadoras: Insertar(S,x): A ade al conjunto S el elemento x. n Borrar(S,x): Elimina del conjunto S el elemento x. Crear(S): Crea el conjunto S vaco.

142

6.2.

Tablas de dispersi n o tablas Hash o

Muchas aplicaciones requieren la utilizaci n de un conjunto sobre el que reo alizar, frecuentemente, las siguientes operaciones: insertar un elemento en el conjunto, borrar un elemento del conjunto, y determinar la pertenencia de un elemento al conjunto. Un conjunto que permita, principalmente, estas operaciones, se denomina diccionario. A continuaci n veremos c mo es posible representar o o ecientemente un diccionario.

6.2.1.

Tablas de direccionamiento directo

El direccionamiento directo es una t cnica que funcionar bien cuando el e a universo U de elementos que puede contener el conjunto, sea razonablemente peque o. Supongamos que cada miembro del conjunto se identica mediante una n clave que es unica; es decir, no existen dos miembros del conjunto que se identiquen por la misma clave. Mediante direccionamiento directo, el conjunto se representa utilizando un vector de tama o igual a la talla del universo |U | T [0, . . . , n |U |-1], en el que cada posici n k del vector referencia al miembro x con clave k. o Una posible representaci n ser , para todo elemento x de clave k perteneciente o a al conjunto, almacenar en la posici n k del vector un puntero a una estructura o donde se guarda la informaci n del elemento x (ver gura 6.1(a)). o Las operaciones sobre el diccionario, en esta implementaci n, son triviales; si o T es el vector, x un elemento y la operaci n clave(x) proporciona la clave k del o elemento x; entonces: insertar(T,x): insertar x en T [clave(x)]. La inserci n supondr crear un nueo a vo nodo con la informaci n del elemento x (clave, informaci n asociada), y o o almacenar en T [clave(x)] la direcci n de memoria del nuevo nodo creado. o buscar(T,k): devolver el valor almacenado en T [k]. Si x no pertenece al conjunto el valor almacenado en T [k] ser el valor NULO (NULL); en caso a contrario, se devolver la direcci n de memoria de la estructura donde se a o guarda la informaci n de x. o borrar(T,x): borrar x y asignar a T [clave(x)] el valor NULO (NULL). El borrado de x se realizar liberando el espacio de memoria ocupado por la a estructura que almacena la informaci n de x. o Cada una de estas operaciones tiene un coste O(1). Una variante sobre esta representaci n, sera almacenar la informaci n del o o elemento en el propio vector (ver gura 6.1(b)). Por lo tanto, en cada posici n o del vector ya no se almacenara un puntero a una estructura donde se guarda la 143

informaci n del elemento, sino que esta informaci n se tendra directamente en la o o posici n del vector. En esta representaci n, puede resultar innecesario almacenar o o en la estructura cu l es la clave del elemento, ya que la posici n que ocupa el a o elemento en el vector indica cu l es su clave. Si las claves no son almacenadas, se a tendr que habilitar un mecanismo que permita distinguir cu ndo la posici n del a a o vector est vaca u ocupada por un elemento del conjunto. a
clave informacion

clave
0 1 1 2 2

informacion

0 1 2 3 4 5
5

1 2

3 4 4

6
6

7 8

. . .
M1
M1

. . .
(a) (b)

Figura 6.1: Representaci n de un diccionario utilizando direccionamiento direco to. En la variante (a) la informaci n del elemento se almacena en una estructura o externa al vector; mientras que en la variante (b) la informaci n del elemento se o almacena en el propio vector

6.2.2.

Tablas de dispersi n o tablas Hash o

Como acabamos de ver, en el direccionamiento directo, cada posici n del veco tor almacena la informaci n de un unico elemento del universo U , por lo que, si o el universo U es demasiado grande o innito, ser imposible almacenar en memoa ria un vector de tama o |U |. Otro inconveniente a adido es que, normalmente, n n el n mero de elementos que, en un momento dado, sean miembros del conjunu to, ser relativamente peque o en comparaci n con el n mero total de elementos a n o u del universo U (|U |), por lo que gran cantidad de espacio ocupado por el vector ser desaprovechado. a 144

La utilizaci n de una tabla de dispersi n o tabla hash solventar en gran meo o a dida estos problemas, ya que el tama o del vector estar limitado a un tama o n a n determinado M : T [0, . . . , M 1]. Para todo elemento x perteneciente al universo U , la posici n que ocupa el elemento en el vector se obtendr tras aplicar sobre o a su clave k una funci n de dispersi n o hashing, que convertir el valor de k en un o o a valor entre 0 y M 1. Denici n: Una tabla de dispersi n o tabla hash es una representaci n de cono o o juntos din micos en la que, la posici n que ocupa en el vector un elemento x con a o clave k, se obtiene tras aplicar una funci n de dispersion o hashing h sobre la o clave k: h(k). De esta forma, la funci n de dispersi n h transforma el universo de o o claves K en un valor comprendido entre 0 y M 1 (ver gura 6.2): h: K {0, 1, . . . , M 1}

A cada posici n del vector le denominaremos cubeta; y diremos que un eleo mento x con clave k se dispersa en la cubeta h(k) de la tabla de dispersi n. o
T
0 1 2
h(k2)

K
k2 k1 k5

3 4 5
k7

h(k1)

6 7 8

h(k5)

h(k7)

. . .
M1

Figura 6.2: Ejemplo de c mo la funci n de dispersi n h transforma las claves en o o o posiciones (cubetas) de la tabla T . El conjunto K representa el universo de claves; y el conjunto L las claves de los elementos que, en un momento dado, pertenecen al conjunto. L gicamente, debido a que la talla del universo de claves K ser mayor que la o a talla M de la tabla, podr ocurrir que dos o m s claves se dispersen en una misma a a cubeta, lo que provocar una colisi n (ver gura 6.3). Dos m todos que resolver n a o e a ecientemente las colisiones son: por encadenamiento, por direccionamiento abierto [Cormen]. Nosotros estudiaremos uno de los m todos: resoluci n de colisiones e o por encadenamiento.

145

T
0 1 2
h(k2)

K
k2 k1 k5

3 4 5
k4 k7

h(k1)

6 7 8

h(k5)

h(k7)=h(k4)

. . .
M1

Figura 6.3: Ejemplo de colisi n. Las claves k7 y k4 se dispersan en la misma o cubeta debido a que h(k7 ) = h(k4 ) = 8. Resoluci n de colisiones por encadenamiento o Estrategia: Los elementos que se dispersen en una misma cubeta se organizan mediante una lista enlazada. Cada cubeta j de la tabla contiene un puntero a la cabeza de una lista enlazada, que contiene los elementos que han sido dispersados en la cubeta j; si la cubeta no tiene elementos contendr el valor NULO (NULL) a (ver gura 6.4).
T
0 1
k9 k6 k2

K
k9

2 3
k2 k1 k5 k7 k8 k1 k8

L
k3

k6

4 5

k4

6 7 8

k5

k4

k7

k3

. . .
M1

Figura 6.4: Ejemplo en el que se muestra c mo se resuelven las colisiones por o encadenamiento. Por ejemplo, h(k9 ) = h(k6 ) = h(k2 ) = 1. Las operaciones sobre un diccionario representado mediante una tabla de dispersi n T , son f ciles de realizar cuando las colisiones se resuelven por encadeo a 146

namiento. Dado una tabla T , y un elemento x, tal que clave(x) = k: Insertar(T,x): inserta el elemento x en la cabeza de la lista apuntada por T [h(clave(x))]. Buscar(T,k): busca un elemento con clave k en la lista T [h(k)]. Borrar(T,x): borra x de la lista encabezada por T [h(clave(x))]. An lisis del coste de las operaciones a Para analizar el coste de cada una de las operaciones, ser necesario introducir a previamente el concepto de factor de carga. Dada una tabla de dispersi n T con o m cubetas y que almacena n elementos, se dene como factor de carga al valor n/m; es decir, al n mero medio de elementos almacenados en cada cubeta de la u tabla. Adem s asumiremos que, dada un clave k, el valor de dispersi n h(k) puede a o ser calculado en un tiempo O(1). Insertar La inserci n de un elemento consiste en obtener la cubeta que le corresponde: o O(1), e insertarlo en la cabeza de la lista: O(1). Por lo tanto, el coste de la inserci n de un elemento en la tabla es O(1). o Buscar El coste temporal de la b squeda de un elemento en la tabla, vendr determiu a nado por el n mero de elementos que ser necesario examinar, hasta encontrar u a o no, el elemento buscado. O lo que es lo mismo, por la longitud de la lista que encabeza la cubeta en la que se dispersa el elemento buscado. El peor caso, por tanto, que puede producirse, ocurrir cuando los n elementos a que contiene la tabla hayan sido dispersados en la misma cubeta. En este caso, una cubeta de la tabla contendr una lista enlazada de n elementos, y el resto de a cubetas estar n vacas. Por lo tanto, en el peor caso, el coste de buscar un elemento a en la tabla, ser igual al coste de buscar un elemento en una lista de n elementos: a O(n). Como se puede observar, el coste de buscar un elemento en la tabla depender de c mo distribuya la funci n de dispersi n h los elementos entre las cubea o o o tas. En general, asumiremos que cualquier elemento tiene la misma probabilidad de dispersarse en cualquiera de las m cubetas; a esta asunci n se le denomina o dispersi n uniforme simple. o

147

En la estimaci n del caso medio deberemos considerar dos casos: a) el elo emento buscado no se encuentra en la tabla; b) el elemento buscado s que es encontrado. Teorema: En una tabla de dispersi n en la que las colisiones se resuelven por o encadenamiento, una b squeda sin exito tiene una complejidad temporal (1+), u en promedio, bajo la asunci n de dispersi n uniforme simple [Cormen]. o o Demostraci n: o Si el elemento no se encuentra en la tabla, habr que examinar todos los elea mentos que contenga la lista de la cubeta en la que se dispersa el elemento buscado. Bajo la asunci n de dispersi n uniforme simple, la longitud media de cada o o lista vendr determinada por el factor de carga = n/m. Por lo tanto, el tiempo a total requerido ser (1 + ) (incluyendo el tiempo para calcular h(k) = O(1)). a Teorema: En una tabla de dispersi n en la que las colisiones se resuelven por o encadenamiento, una b squeda con exito tiene una complejidad temporal (1 + u ), en promedio, bajo la asunci n de dispersi n uniforme simple [Cormen]. o o Demostraci n: o Para la demostraci n, asumiremos que cada vez que se inserta un nuevo eleo mento en la tabla, se coloca al nal de la lista de la cubeta en la que se dispersa (el coste que vamos a estimar ser el mismo si la inserci n se realizara en la cabeza a o de la lista). Para estimar el coste medio, deberemos estimar el coste que supone 1 buscar cada uno de los n elementos que contiene la tabla, y hallar la media n . El coste de la b squeda de un elemento depender de la posici n que ocupa en u a o la lista de la cubeta en la que se dispersa. A su vez, la posici n que ocupa en la o lista, depender del factor de carga que exista en la tabla cuando fue insertado; a por ejemplo, si el elemento buscado fue insertado cuando existan i 1 elementos i1 en la tabla, su posici n en la lista ser : m + 1 (debido a que asumimos que se o a inserta al nal de la lista). Por lo tanto, encontrar el elemento que fue el i elemento insertado en la tabla costar : ci = i1 + 1. De esta forma, cuando en la tabla a m se han insertado n elementos, el n mero medio de elementos que ser necesario u a examinar en una b squeda con exito ser : u a

148

1 n

ci =
i=1

1 n

1 = n = 1 n

i=1 n

i1 +1 m i1 + m
n

1
i=1

i=1 n

i=1

i1 +n m
n

= 1+ = 1+ = 1+

1 n 1 nm

i=1 n

i1 m (i 1)

i=1

1 (n 1)n nm 2 n1 = 1+ 2m n 1 = 1+ 2m 2m 1 = 1+ 2 2m Por lo tanto, el tiempo total requerido para encontrar con exito un elemento en la tabla (incluyendo el tiempo para calcular la cubeta en la que se dispersa h(k) = O(1)) es (2 + /2 1/2m) = (1 + ). Esto signica que el tiempo total que se requiere para buscar en promedio un elemento en la tabla, depender del factor de carga . Por lo tanto, si el n mero de a u cubetas es, al menos, proporcional al n mero de elementos de la tabla, tendremos u que n = O(m) y, en consecuencia, = n/m = O(m)/m = O(1). En denitiva, podemos concluir que el tiempo de b squeda de un elemento en una tabla de u dispersi n es, en promedio, constante. o Borrar La eliminaci n de un elemento de la tabla supone que previamente debe ser o buscado. Por lo tanto, el coste temporal de la operaci n ser esencialmente el o a mismo que el de la b squeda: (1 + ). u Como se ha demostrado, si se utiliza una tabla de dispersi n para representar o un diccionario, todas las operaciones que necesitamos efectuar sobre el (inserci n, o 149

b squeda y borrado) pueden realizarse, en promedio, con un coste temporal conu stante O(1). Funciones de dispersi n o Una funci n de dispersi n ideal, debera satisfacer la asunci n de dispersi n o o o o uniforme simple: cada elemento tiene la misma probabilidad de dispersarse en cualquiera de las m cubetas. En la pr ctica, ser difcil encontrar alguna funci n a a o de dispersi n que cumpla esta asunci n. De cualquier forma, podremos utilizar o o funciones de dispersi n que dispersen de forma aceptable los elementos entre las o cubetas. Asumiremos que el universo de claves es el conjunto de n meros naturales u N = {0, 1, 2, . . . }. Si la clave que identica al elemento no es un n mero natu ural, habr que encontrar una forma de representarla como un n mero natural. a u Por ejemplo, una clave que sea una cadena de caracteres, puede ser representada como un n mero natural, combinando de alg n modo la representaci n ASCII u u o (num rica) de sus caracteres. e El m todo de la divisi n e o En este m todo, para obtener una funci n de dispersi n h, la clave1 k se puede e o o transformar en un valor entre 0 y m 1, tomando el resto de la divisi n de k o dividido por m, donde m es el n mero de cubetas: u h(k) = k mod m Ejemplo: Si la clave k = 100 y la tabla de dispersi n es de talla m = 12; entonces: o h(100) = 100 mod 12 = 4. El elemento con clave 100 se dispersara en la cubeta 4 de la tabla. Obs rvese, por ejemplo, que el elemento con clave 16: h(16) = e 16 mod 12 = 4 se dispersara en la misma cubeta. Aspecto crtico: La elecci n del valor m. Para que la funci n de dispersi n o o o tenga un buen comportamiento, es decir, disperse de forma aceptable los elementos entre las cubetas, ser recomendable escoger un valor de m que sea primo y a no est pr ximo a una potencia de 2. e o Ejemplo: Supongamos que queremos almacenar en la tabla de dispersi n 2000 o cadenas de caracteres, y queremos que el factor de carga no sea mayor que 3. En este caso, el mnimo n mero de cubetas necesario sera 2000/3 = 666,6. Bus u camos un n mero m pr ximo a 666 que sea primo y no est cercano a una potencia u o e de 2: 701. Por lo tanto la funci n de dispersi n sera: h(k) = k mod 701. o o
1

A partir de ahora siempre consideraremos que la clave es un n mero natural: k N. u

150

El m todo de la multiplicaci n e o En este m todo, para obtener una funci n de dispersi n h, la clave k se puede e o o transformar en un valor entre 0 y m 1, en dos pasos: 1. Se multiplica la clave k por una constante en el rango 0 < A < 1 y se extrae del resultado unicamente la parte decimal. Esta operaci n puede expresarse o como: (k A) mod 1 (6.1)

2. Se multiplica el valor obtenido en (6.1) por el valor de m, y se extrae el valor entero m s pr ximo por debajo al n mero obtenido. Por lo tanto, la a o u funci n de dispersi n h quedara denida como: o o h(k) = m (( k A) mod 1)

En las funciones de dispersi n basadas en el m todo de la multiplicaci n, el o e o valor de m no es crtico. Se suele tomar como valor de m una potencia de 2 para facilitar el c mputo de la funci n: m = 2p , para alg n valor entero p. o o u Aunque el m todo sirve para cualquier constante A, en algunos trabajos se e sugiere utilizar el valor de A ( 5 1)/2 = 0, 618033988750. Ejemplos de funciones de dispersi n sobre cadenas de caracteres o Como se ha comentado anteriormente, en el caso de que las claves sean cadenas de caracteres, deber realizarse una conversi n de la cadena en un numero a o natural. Para ello, dada una cadena x de n caracteres, xi simbolizar el codia go ASCII del caracter que ocupa la posici n i en la cadena, 0 i n 1, o (x = x0 x1 x2 . . . xn1 ). A continuaci n, veremos algunos ejemplos de funciones o de dispersi n aplicadas sobre cadenas de caracteres. o 1. Ejemplos de funciones de dispersi n basadas en el m todo de la divisi n. o e o Funci n 1: Se transforma la clave alfab tica en una clave num rica o e e sumando los c digos ASCII de cada caracter. Se caracteriza porque o aprovecha toda la clave alfanum rica para realizar la transformaci n. e o
n1

h(x) = (
i=0

xi ) mod m

Inconvenientes: 151

Si el tama o de T es grande, por ejemplo m = 10007, no se n distribuir n bien las claves, ya que si todas las cadenas son de a longitud menor o igual que, por ejemplo, 10, el m ximo valor a que se podra obtener en una transformaci n de la cadena en un o n mero natural sera: 255 10 = 2550 (suponiendo que la cadeu na de longitud 10, estuviera formada por el mismo caracter cuya representaci n ASCII fuera el valor m ximo: 255). Por lo tanto, o a los valores de la funci n h(x) tomar n valores entre 0 y m xio a a mo 2550. Este hecho provocar que las cubetas desde la posici n a o 2551 hasta 10006 est n vacas. e El orden en que aparecen los caracteres en la cadena no se tiene en cuenta, por lo que, por ejemplo h(cosa)=h(saco). Esto signica que claves que est n formadas por los mismos caracteres e ser n dispersadas en las mismas cubetas. a Funci n 2: Se transforma la clave alfab tica en una clave num rica, o e e utilizando, unicamente, los tres primeros caracteres de la clave, pero considerando que son un valor num rico en una determinada base (256 e en este caso).
2

h(x) = (
i=0

xi 256i ) mod m

Inconvenientes: Al utilizar unicamente los tres primeros caracteres de la cadena, todas las cadenas cuyos primeros tres caracteres coincidan ser n a dispersadas en las mismas cubetas. Por ejemplo, h(clase)=h(clarinete)=h(clan). Funci n 3: Similar a la funci n 2 pero considerando toda la cadena. o o
n1

h(x) = (
i=0

xi 256((n1)i) ) mod m

Inconvenientes: El c lculo de la funci n h es costoso. a o 2. Ejemplos de funciones de dispersi n basadas en el m todo de la multiplio e caci n. o 152

Funcion 4: Se transforma la clave alfab tica en una clave num rica, e e sumando los c digos ASCII de cada caracter considerando que son un o valor num rico en base 2. e
n1

h(x) =

m (((
i=0

xi 2((n1)i) ) A) mod 1)

Evaluaci n emprica o En la gura 6.5 se muestra una comparativa emprica del comportamiento de cada una de las funciones, para un n mero de elementos n = 3975 y un n mero u u de cubetas m = 2003. Para cada funci n, se muestran dos tipos de gr cas: o a 1. En las gr cas de la primera columna se representa, para cada cubeta, el a n mero de elementos que contiene. Puede apreciarse que la funci n 1 y u o 2 distribuyen los elementos decientemente, ya que existen cubetas con una gran concentraci n de elementos. Obs rvese que las funciones 3 y 4 o e consiguen distribuir los elementos de una manera m s uniforme entre las a cubetas: la funci n 3 no contiene m s de 9 elementos por cubeta con una o a desviaci n tpica de 1,43; mientras que la funci n 4, unicamente presenta o o una cubeta con 14 elementos, mientras que el resto no supera los 10 elementos por cubeta (desviaci n tpica: 1,8). o 2. En cada una de las gr cas de la segunda columna se representa, el n mero a u de cubetas (eje y) que tienen ex ctamente un n mero determinado de elea u mentos (eje x). Por ejemplo, puede observarse que tanto la funci n 1 como o la funci n 2, presentan un n mero elevado de cubetas que contienen m s de, o u a por ejemplo, 10 elementos; mientras que para la funci n 3, la gran mayora o de cubetas contienen un n mero menor de 5 elementos y ninguna m s de u a 9, y para la funci n 4, se cumple que la gran mayora de cubetas contienen o un n mero menor de 7 elementos, y s lo una cubeta contiene un n mero u o u m ximo de 14. a A la vista de los resultados obtenidos, se aprecia claramente que la funci n o 3 y 4 presentan un mejor comportamiento. Esto se debe a que ambas funciones aprovechan mejor la informaci n de la cadena (clave). Las funciones 3 y 4 utilizan o todos los caracteres de la cadena y, adem s, consideran la posici n que ocupan; a o sin embargo, la funci n 1, aunque utiliza todos los caracteres de la cadena no tiene o en cuenta su posici n; y la funci n 2 unicamente utiliza la informaci n de los tres o o o primeros caracteres. e En las guras 6.6 y 6.7 se muestra de qu forma afecta, al buen comportamiento de una funci n de dispersi n basada en el m todo de la divisi n, la modicaci n o o e o o 153

del n mero de cubetas m: el comportamiento de la funci n de dispersi n 3 (basau o o da en el m todo de la divisi n) empeora al variar el valor de m (de 2003 a 2000), e o mientras que a la funci n 4 (basada en el m todo de la multiplicaci n) no le afecta o e o esta variaci n en el n mero de cubetas manteniendo el buen comportamiento. o u

154

34 32 30 28 26 24 22 elemento por cubeta 20 18 16 14 12 10 8 6 4 2 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 numero de elementos 1 numero de cubetas

1000 alfa: 1.985; desv.tip: 4.33

100

10

300 alfa: 1.985; desv.tip.: 8.97

1000

250 100 elementos por cubeta 200

numero de cubetas

150

10

100 1 50

0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 0 25 50 75 100 125 150 175 numero de elementos 200 225 250 275 300

10 1000 9 8 7 elementos por cubeta 6 5 4 3 1 2 1 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 0 1 2 3 4 5 numero de elementos 6 7 8 9 numero de cubetas

alfa: 1.985; desv.tip: 1.43

100

10

15 1000 14 13 12 11 10 elementos por cubeta 9 8 7 6 5 4 3 2 1 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 numero de elementos 1 numero de cubetas

alfa: 1.985; desv.tip: 1.8

100

10

Figura 6.5: Distribuci n de los elementos entre las cubetas para cada una de las o funciones de dispersi n 1, 2, 3 y 4, respectivamente, para un n mero de cubetas o u m = 2003 y un n mero de elementos n = 3975. Las gr cas de la izquierda u a muestran, para cada cubeta, el n mero de elementos que contiene. Las gr cas de u a la derecha muestran, en el eje y, el n mero de cubetas (en escala logartmica) que u contienen ex ctamente el n mero de elementos que se representa en el eje x. a u 155

10 9 8 7 elementos por cubeta 6 5 4 3 2 1 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 elementos por cubeta alfa: 1.985; desv.tip: 1.43

50 alfa: 1.987; desv.tip: 7.83 45 40 35 30 25 20 15 10 5 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000

1000 1000

100

100 numero de cubetas

numero de cubetas

10

10

4 5 numero de elementos

10

15

20 25 numero de elementos

30

35

40

45

Figura 6.6: Comparativa en la que se muestra c mo afecta la elecci n del valor o o de m a la funci n 3 basada en el m todo de la divisi n; m = 2003 (izquierda), o e o m = 2000 (derecha).
15 14 13 12 11 10 elementos por cubeta 9 8 7 6 5 4 3 2 1 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 0 0 250 500 750 1000 cubeta 1250 1500 1750 2000 2 4 elementos por cubeta 10 12 alfa: 1.985; desv.tip: 1.8 14 16 alfa: 1.987; desv.tip: 1.8

1000 1000

100

100 numero de cubetas

numero de cubetas

10

10

10

11

12

13

14

10

11

12

13

14

15

numero de elementos

numero de elementos

Figura 6.7: Comparativa en la que se muestra que la elecci n del valor de m no o afecta al comportamiento de la funci n 4 basada en el m todo de la multiplicaci n; o e o m = 2003 (izquierda), m = 2000 (derecha).

156

Ejercicio: Se disponen de las siguientes deniciones de tipos, constantes y variables, para declarar y manipular tablas de dispersi n: o #dene NCUB ... typedef struct snodo { char *pal; struct snodo *sig; } nodo; typedef nodo *Tabla[NCUB]; Tabla T1, T2, T3; Adem s, las siguientes funciones est n disponibles y pueden utilizarse cuando a a se considere oportuno. /* Inicializa una tabla vacia */ Tabla crea_Tabla(); /* Inserta la palabra w en la tabla T */ Tabla inserta(Tabla T, char *pal) /* Devuelve un puntero al nodo que almacena la palabra */ /* pal, o NULL si no la encuentra */ nodo *buscar(Tabla T, char *pal) Escribir una funci n que cree una tabla T 3 con los elementos pertenecientes a o la intersecci n de los conjuntos almacenados en las tablas T 1 y T 2. o Tabla interseccion(Tabla T1, Tabla T2) { int i; nodo *aux; Tabla T3; T3=crea_Tabla(); for (i=0; i<NCUB; i++) { aux = T1[i]; while (aux != NULL) { if (buscar(T2,aux->pal)!=NULL) T3=inserta(T3,aux->pal); aux = aux->sig; } } return(T3); } 157

6.3.

Arboles binarios de busqueda

Denici n: Un arbol binario de b squeda es un arbol binario que puede estar o u vaco, o si no est vaco cumple las siguientes propiedades: a Cada nodo contiene una clave y no existen dos nodos con la misma clave. Para cada nodo n de un arbol binario de b squeda, se cumple que, si su u sub rbol izquierdo no es vaco, todas las claves almacenadas en los nodos a de su sub rbol izquierdo son menores que la clave almacenada en el nodo a n. Para cada nodo n de un arbol binario de b squeda, se cumple que, si su u sub rbol derecho no es vaco, todas las claves almacenadas en los nodos de a su sub rbol derecho son mayores que la clave almacenada en el nodo n. a Los arboles binarios de b squeda, son una estructura de datos que soportan u muchas de las operaciones que se pueden realizar sobre conjuntos din micos; coa mo son: inserci n, borrado, b squeda, mnimo, m ximo, predecesor y sucesor. o u a Un arbol binario de b squeda puede ser utilizado para representar un diccionario u o una cola de prioridad. En la gura 6.8 se muestran dos ejemplos de arboles bi narios de b squeda; mientras que en la gura 6.9 se muestran dos arboles binarios u que no cumplen las propiedades para ser considerados como arboles binarios de b squeda. u
15 7 3 9 11 18
10 4 7

21
8

14

Figura 6.8: Ejemplos de arboles binarios de b squeda. Para cualquier nodo n de u clave x, las claves de los nodos del sub rbol izquierdo de n son menores que x, y a las claves de los nodos del sub rbol derecho de n son mayores que x. a

158

15 7 3 8 16 18 21
3 7 8

15 18 14 19 21

Figura 6.9: Ejemplos de arboles binarios que no son de b squeda. En el primer u arbol, el nodo de clave 8 pertenece al sub rbol izquierdo del nodo de clave 7. En a el segundo arbol, el nodo de clave 14 pertenece al sub rbol derecho del nodo de a clave 15.

6.3.1.

Representaci n de arboles binarios de busqueda o

Los arboles binarios de b squeda suelen representarse mediante variables din miu a cas, de tal forma que cada nodo del arbol es una estructura con los siguientes campos: clave: clave del nodo. hijo izquierdo: puntero a la estructura que representa al nodo que es hijo izquierdo. hijo derecho: puntero a la estructura que representa al nodo que es hijo derecho.

15 7 3 9 11 18 21
/ /

15 7 3 11 9 18
/

21
/ /

/ /

Figura 6.10: Representaci n de un arbol binario de b squeda mediante una estruco u tura enlazada de variables din micas. a

159

La siguiente denici n de tipos en C, nos servira para denir una estructura o de datos que se ajuste a esta representaci n. o typedef ... tipo_baseT; typedef struct snodo { tipo_baseT clave; struct snodo *hizq, *hder; } abb; Una posible variante a este tipo de representaci n, como ya se indic en el o o tema 2 cuando se vio la representaci n de arboles binarios, sera almacenar en o otro campo de la estructura, un puntero al nodo padre (ver gura 6.11).
15 7
/

15 7 3 9 11 18 21 3
/ /

18 11
/

/ /

21

9
/ /

Figura 6.11: Representaci n de un arbol binario de b squeda mediante una estruco u tura enlazada de variables din micas, en la que tambi n se mantiene un puntero al a e nodo padre de cada nodo. La siguiente denici n de tipos en C, nos servira para denir una estructura o de datos que se ajuste a esta representaci n. o typedef ... tipo_baseT; typedef struct snodo { tipo_baseT clave; struct snodo *hizq, *hder, *padre; } abb;

160

6.3.2.

Altura m xima y mnima de un arbol binario de busquea da

Dado cualquier arbol binario de b squeda, su altura ser la mnima posible, u a en el caso de que todos los niveles del arbol contengan el m ximo numero de a nodos posibles, excepto, si cabe, el ultimo nivel. En ese caso, si el arbol contiene n nodos, su altura ser log2 n . Por lo tanto, la altura mnima de un arbol binario a de b squeda es O(log n). u De la misma forma, dado cualquier arbol binario de b squeda, su altura ser la u a m xima posible, en el caso de que todos los niveles del arbol contengan un unico a nodo. En ese caso su altura ser n 1. Por lo tanto, la altura m xima de un arbol a a binario de b squeda es O(n). u

logn

. . .

...

. . .

. .
(b) altura m xima a

(a) altura mnima

Figura 6.12: Altura mnima (a) y m xima (b) de un arbol binario de b squeda. a u Cuando analicemos cada una de las operaciones que se pueden realizar sobre un arbol binario de b squeda, veremos que el coste temporal de cada una de ellas u es O(h), donde h es la altura del arbol. Por lo tanto, si la altura del arbol es mnima, el coste temporal de cada operaci n ser O(log n); por el contrario, si la altura del o a arbol es m xima, cada una de las operaciones tendr un coste temporal de O(n). a a En general, la altura de un arbol binario de b squeda construido aleatoriamente u es O(log n), por lo que cada operaci n podr realizarse con un coste temporal de o a O(log n).

6.3.3.

Recorrido de arboles binarios de busqueda

Tal como se vio en el tema 2, una acci n P puede ser realizada sobre todos o los nodos de un arbol, siguiendo tres tipos de recorrido: recorrido en orden previo (preorden), recorrido en orden sim trico (inorden) y recorrido en orden posterior e 161

(postorden). Por lo tanto, un arbol binario de b squeda podr recorrerse igualu a mente de cualquiera de estas tres formas. Sin embargo, vamos a centrarnos en el recorrido en orden sim trico (inorden), e ya que, debido a las caractersticas de un arbol binario de b squeda, si la acci n u o P fuera imprimir la clave del nodo, las claves de un arbol binario de b squeda u se imprimir n de forma ordenada (de menor a mayor) siguiendo el recorrido a en inorden. Esto es as, debido a que en el recorrido en inorden, la operaci n P o sobre un nodo n se realiza despu s de recorrer en inorden su sub rbol izquierdo e a y antes de recorrer en inorden su sub rbol derecho. Por lo tanto, como siempre se a cumple que, para todo nodo, su clave es mayor que todas las claves del sub rbol a izquierdo, y menor que todas las claves del sub rbol derecho, la clave de cada a uno de los nodos se imprime justamente despu s de imprimir todas las que son e menores y antes de imprimir todas las que son mayores. La siguiente funci n realiza un recorrido en inorden de un arbol binario de o b squeda: u void inorden(abb *T) { if (T != NULL) { inorden(T->hizq); printf("%d ",T->clave); inorden(T->hder); } } Ejercicio Realizar la traza del algoritmo inorden para el arbol de la gura 6.10: La soluci n es: <3, 7, 9, 11, 15, 18, 21> o

6.3.4.

Busqueda de un elemento en un arbol binario de busqueda

La operaci n m s com n que se realiza sobre un arbol binario de b squeda es o a u u rbol. A continuaci n se presenta una funci n buscar una clave almacenada en el a o o que dado un puntero T al nodo raz de un arbol binario de b squeda y una clave u x a buscar, devuelve un puntero al nodo con clave x si se encuentra, o NULL en caso contrario.

162

abb *abb_buscar(abb *T, tipo_baseT x) { while ( (T != NULL) && (x != T->clave) ) if (x < T->clave) T = T->hizq; else T = T->hder; return(T); } La funci n comienza la b squeda desde el nodo raz y va trazando un camino o u descendente a trav s del arbol, de forma que cada vez que se sit a en un nuevo e u nodo n, compara la clave del nuevo nodo con la clave buscada; si las claves son iguales la b squeda naliza con exito, mientras que si son diferentes, si la clave u b scada es menor, la b squeda contin a por el sub rbol izquierdo de n, ya que u u u a la caracterstica de los arboles binarios de b squeda implica que el elemento bus u cado no puede encontrarse en el sub rbol derecho. De forma an loga, si la clave a a buscada es mayor que la clave del nodo n, la b squeda contin a por el sub rbol u u a derecho. Si el elemento buscado no es encontrado, la b squeda naliza cuando se u llega a un sub rbol vaco (T = N U LL). a Ejercicio Realizar una versi n recursiva de la anterior funci n de b squeda de una clave o o u x en un arbol binario de b squeda: u abb *abb_buscar(abb *T, tipo_baseT x) { if (T != NULL) if (x == T->clave) return(T); else if (x < T->clave) return(abb_buscar(T->hizq,x)); else return(abb_buscar(T->hder,x)); return(T); } El n mero de nodos que es necesario examinar hasta encontrar o no el eleu mento buscado, determinar el coste del algoritmo. Debido a que el camino de a b squeda que se va trazando en el arbol, examina un nodo por nivel, como m xiu a mo ser necesario examinar todos los niveles del arbol hasta encontrar o no el a elemento buscado. Por lo tanto, el coste temporal de la busqueda de un ele mento en un arbol binario de busqueda ser O(h), donde h es la altura del a arbol.

163

Ejemplo En la gura 6.13, se muestra un ejemplo del funcionamiento del algoritmo de b squeda para la llamada inicial: nodo = abb buscar(T, x), donde x es un u elemento con clave 11. En este ejemplo, el elemento buscado s que se encuentra en el arbol. Por otra parte, en la gura 6.14 se muestra un ejemplo en el que el elemento buscado no se encuentra en el arbol. En este caso, la llamada inicial es: nodo = abb buscar(T, x) donde x es un elemento con clave 19. Debajo de las guras se muestra qu instrucci n se lleva a cabo dada la situaci n e o o que se muestra en la gura.
T 7 3 11 9 15
T

15 18
/

7 21
/ /

18
/

/ /

/ /

11 9

21
/ /

/ /

/ /

(a) T=T>hizq;

(b) T=T>hder;

15 7 3 11 9 18
/

21
/ /

/ /

/ /

(c) return(T);

Figura 6.13: Ejemplo de c mo se desplaza el puntero T a trav s de la estructura o e del arbol hasta encontrar el elemento buscado (x = 11).

164

T 7 3

15 18
/

15
T

7 21
/ /

18
/

/ /

11 9

/ /

11 9

21
/ /

/ /

/ /

(a) T=T>hder;

(b) T=T>hder;

15 7 3 11 9 18
/

15 7 3 11 9 18
/

21
/ /

21
/ /

/ /

/ /

T=NULL

/ /

/ /

(c) T=T>hizq;

(d) return(T);

Figura 6.14: Ejemplo de c mo se desplaza el puntero T a trav s de la estructura o e del arbol sin encontrar el elemento buscado x = 19.

6.3.5.

Busqueda del elemento mnimo y del elemento m ximo a

En un arbol binario de b squeda, el elemento cuya clave es mnima puede ser u f cilmente encontrado, simplemente realizando un recorrido descendente desde la a raz a trav s de los hijos izquierdos de cada nodo, hasta encontrar un nodo que no e tenga hijo izquierdo. Ese nodo ser el nodo cuya clave es mnima. a La siguiente funci n devuelve un puntero al nodo cuya clave es la clave mnio ma del arbol apuntado por T .

165

abb *minimo(abb *T) { if (T != NULL) while(T->hizq != NULL) T=T->hizq; return(T); } La funci n basa su funcionamiento en el aspecto de que, en los arboles binao rios de b squeda, el hecho de que un nodo tenga sub rbol izquierdo no vaco, imu a plica necesariamente que existen en el arbol nodos con claves menores que la clave de ese nodo . Por lo tanto, en general, para cualquier nodo n que tenga hijo izquierdo, el nodo n no podr ser el elemento de clave mnima del sub rbol de raz n, y el a a elemento de clave mnima habr que buscarlo siempre en su sub rbol izquierdo. a a Por lo tanto, aplicando este razonamiento, si el arbol binario de b squeda no es u vaco y nos situamos inicialmente en el nodo raz, la b squeda del elemento de u clave mnima supone ir descendiendo siempre a trav s de los hijos izquierdo de e cada nodo hasta encontrar un nodo x que carezca de sub rbol izquierdo, lo que a implicar necesariamente que no existen en el arbol claves menores que la clave a del nodo x. Para obtener el nodo cuya clave es m xima, el razonamiento ser sim trico al a a e expuesto en el caso de la b squeda del mnimo; es decir, se realizar un recorrido, u a comenzando desde el nodo raz, descendiendo siempre a trav s de los hijos dere e cho de cada nodo. Cuando se encuentre un nodo que no tenga sub rbol derecho a se habr localizado al nodo de clave m xima. A continuaci n, se presenta una a a o funci n, que devuelve un puntero al nodo cuya clave es la clave m xima del arbol o a apuntado por T . abb *maximo(abb *T) { if (T != NULL) while(T->hder != NULL) T=T->hder; return(T); } El n mero de nodos que es necesario examinar hasta encontrar el elemento de u clave mnima o m xima, determinar el coste de los algoritmos. Debido a que el a a camino de b squeda que se va trazando en el arbol, examina un nodo por cada u nivel, como m ximo ser necesario examinar todos los niveles del arbol hasta a a encontrar el elemento buscado. Por lo tanto, el coste temporal de obtener el mnimo o el m ximo de un arbol binario de busqueda ser O(h), donde h es a a la altura del arbol. En las guras 6.15 y 6.16 se muestran ejemplos del funcionamiento de ambos algoritmos. Debajo de las guras se muestra qu instrucci n se lleva a cabo dada e o la situaci n que se muestra en la gura. o 166

Ejemplo
T 7 3 11 9 15
T

15 18
/

15 18
/

7 21
/ /

7 11 9

18
/

/ /

/ /

11 9

21
/ /

/ /

21
/ /

/ /

/ /

/ /

(a) T=T>hizq;

(b) T=T>hizq;

(c) return(T);

Figura 6.15: Ejemplo de c mo se desplaza el puntero T a trav s de la estructura o e del arbol hasta encontrar el nodo de clave mnima.

T 7 3

15 18
/

15
T

15 18
/

7 21
/ /

7 21
/ /

18
/

/ /

11 9

/ /

11 9

/ /

11 9

21
/ /

/ /

/ /

/ /

(a) T=T>hder;

(b) T=T>hder;

(c) return(T);

Figura 6.16: Ejemplo de c mo se desplaza el puntero T a trav s de la estructura o e del arbol hasta encontrar el nodo de clave m xima. a

6.3.6.

Inserci n de un elemento en un arbol binario de busqueo da

La inserci n de un nuevo elemento en un arbol binario de b squeda debe reo u alizarse de tal forma, que tras la inserci n, se siga cumpliendo que, para todo o nodo del arbol, las claves de los nodos de su sub rbol izquierdo son menores, y a las claves de los nodos de su sub rbol derecho son mayores. Para conseguirlo, la a inserci n se realizar de la siguiente forma: o a

167

1. Si el arbol binario de b squeda est vaco, se inserta el nuevo nodo como u a raz del arbol. 2. Si el arbol binario de b squeda no est vaco, la inserci n se realizar en u a o a dos pasos: a) Se busca la posici n que le corresponde en el arbol binario de b squeo u da al nuevo nodo. Para obtener la posici n correcta donde debe ubio carse el nuevo nodo, se realiza un recorrido desde la raz del arbol siguiendo el mismo criterio que se segua en la operaci n de b sque o u da. b) Una vez encontrada la posici n que le corresponde, el nuevo elemento o es insertado en el arbol enlaz ndolo correctamente con el nodo que a debe ser su padre. A continuaci n se presenta una funci n que, dado un puntero T al nodo raz o o de un arbol binario de b squeda, y un puntero nodo a un elemento, realiza la u inserci n del elemento apuntado por nodo en el arbol apuntado por T , y como o resultado devuelve un puntero al nodo raz del nuevo arbol que se obtiene tras la inserci n. o abb *abb_insertar(abb *T, abb *nodo) { abb *aux, *aux_padre=NULL; aux = T; while (aux != NULL) { aux_padre = aux; if (nodo->clave < aux->clave) aux = aux->hizq; else aux = aux->hder; } if (aux_padre == NULL) return(nodo); else { if (nodo->clave < aux_padre->clave) aux_padre->hizq = nodo; else aux_padre->hder = nodo; return(T); } } La funci n comienza la b squeda de la posici n que le corresponde al nueo u o vo nodo desde el nodo raz, y va trazando un camino descendente a trav s del e arbol, de forma que cada vez que se sit a en un nuevo nodo n, compara la clave u 168

del nodo n con la clave del nuevo elemento a insertar; si la clave del nuevo elemento es menor, el recorrido contin a por el sub rbol izquierdo de n, ya que la u a caracterstica de los arboles binarios de b squeda implica que el nuevo elemen u to debera insertarse en el sub rbol izquierdo. De forma an loga, si la clave del a a nuevo elemento es mayor que la clave del nodo n, el recorrido contin a por el u sub rbol derecho. El recorrido naliza cuando se llega a un sub rbol vaco, moa a mento en el cual se ha encontrado la posici n que le corresponde al nuevo nodo o en el arbol. La inserci n se realiza enlazando el nuevo nodo con el ultimo nodo n o que se visit en el recorrido. Para ello, si la clave del nuevo nodo es menor que la o clave del ultimo nodo n, el nuevo elemento se inserta como hijo izquierdo de n o, en caso contrario, como hijo derecho. La inserci n de un nuevo elemento en el arbol binario de b squeda supone o u buscar previamente la posici n que le corresponde en el arbol. Por lo tanto, el o n mero de nodos que es necesario examinar hasta encontrar la posici n determiu o nar el coste del algoritmo. Al igual que ocurra con el resto de operaciones, el a camino de b squeda que se va trazando en el arbol, examina un nodo por cada u nivel; por lo tanto, como m ximo, ser necesario examinar todos los niveles del a a arbol hasta encontrar la posici n correcta. Esto implica que el coste temporal de o insertar un elemento en un arbol binario de busqueda ser O(h), donde h es a la altura del arbol. A continuaci n se muestra un ejemplo de inserci n en un arbol binario de o o b squeda; se inserta un nodo de clave 10. Debajo de las guras se indica qu inu e strucciones se llevan a cabo dada la situaci n que se muestra en la gura. o

169

Ejemplo
aux 7 3 11 9 15 T 18
/

aux_padre aux 7

15

T 18
/

aux_padre 7
21
/ /

15

aux 18 / 11 9 21
/ / /

21
/ /

/ /

/ /

11 9

/ /

/ /

/ /

/ /

(a) aux padre=aux; aux=aux->hizq;

(b) aux padre=aux; aux=aux->hder;

(c) aux padre=aux; aux=aux->hizq;


15 T 18
/

15 7 3 11 9

T
7

18
/
/ /

11 9

21
/ /

21
/ /

aux_padre
/

/ /

aux
/ /

aux_padre
aux=null

10
/ /

nodo

(d) aux padre=aux; aux=aux->hder;

(e) aux padre->hder=nodo; return(T);

e Figura 6.17: Ejemplo de c mo se desplazan los punteros aux y aux padre a trav s o de la estructura del arbol hasta encontrar la posici n que le corresponde al nuevo o nodo a insertar (10). Una vez localizada la posici n del nuevo nodo, se inserta o enlaz ndolo correctamente al padre (que es apuntado por aux padre). a

6.3.7.

Borrado de un elemento en un arbol binario de busqueda

El borrado de un elemento en un arbol binario de b squeda se realiza teniendo u en cuenta los siguientes casos: 1. Si el elemento a borrar no tiene hijos (es un hoja), se elimina. 170

15 7 3 6 9 16 17 18 21
3 6 7

15 18 16 17 21

2. Si el elemento a borrar es un nodo interior que tiene un hijo, se elimina, y su posici n es ocupada por el hijo que tiene. o
15 7 3 6 16 17 18 21
15 18 6 16 17 21

3. Si el elemento a borrar es un nodo interior que tiene dos hijos, su lugar es ocupado por el nodo de clave mnima del sub rbol derecho (o por el de a clave m xima del sub rbol izquierdo). a a

15 18 6 16 17 21 25

16 18 6 17 21 25

15 18 6 16 17 21 25

6 18 16 17 21 25

171

A continuaci n se presenta una funci n en C que, siguiendo el esquema deo o scrito, borra el nodo de clave x (si se encuentra) del arbol binario de b squeda a u cuya raz apunta T .

172

abb *abb borrar(abb *T, tipo baseT x){ abb *aux, *aux padre=NULL, *p1, *p2=NULL; aux = T; /* buscamos el nodo a borrar (clave x) */ while ( (aux != NULL) && (x != aux->clave) ){ aux padre = aux; if (x < aux->clave) aux = aux->hizq; else aux = aux->hder; } if (aux != NULL){ /* se encuentra el nodo a borrar */ /* Caso 1: El nodo a borrar es una hoja */ if ( (aux->hizq == NULL) && (aux->hder == NULL) ){ /* El rbol slo tena un nodo -> retorna rbol vaco (Fig.6.18) */ a o a if (aux padre == NULL) T = NULL; else{ /* El nodo a borrar tiene padre */ if (aux padre->hizq == aux) /* El nodo era hijo izquierdo (Fig.6.19) */ aux padre->hizq = NULL; else /* El nodo era hijo derecho (Fig.6.20) */ aux padre->hder = NULL; } free(aux); /* Eliminamos el nodo */ } /* Caso 2: El nodo a borrar es un nodo interior con un hijo */ else if (aux->hizq == NULL){ /* Slo tiene hijo derecho */ o if (aux padre == NULL) /* El nodo a borrar es la raz (Fig.6.21) */ T = aux->hder; else{ /* El padre hereda el hijo derecho del nodo borrado */ if (aux padre->hizq == aux) aux padre->hizq = aux->hder; /* (Fig.6.22) */ else aux padre->hder = aux->hder; /* (Fig.6.23) */ } free(aux); /* Eliminamos el nodo */ } else if (aux->hder == NULL){ /* Slo tiene hijo izquierdo */ o if (aux padre == NULL) /* El nodo a borrar es la raz (Fig.6.24) */ T = aux->hizq; else{ /* El padre hereda el hijo izquierdo del nodo borrado */ if (aux padre->hizq == aux) aux padre->hizq = aux->hizq; /* (Fig.6.25) */ else aux padre->hder = aux->hizq; /* (Fig.6.26) */ }

free(aux); /* Eliminamos el nodo */ }/* Caso 3: El nodo a borrar es un nodo interior con dos hijos */ else{ /* buscamos el nodo de clave minima del subarbol derecho */ p1 = aux->hder; while (p1->hizq != NULL){ p2 = p1; p1=p1->hizq;} /* el nodo de clave minima (p1) era el hijo derecho del nodo a borrar */ if (p2 == NULL){aux->clave = p1->clave;aux->hder = p1->hder;} /* (Fig.6.27) * /* el nodo de clave minima (p1) estaba en el subarbol */ /* izquierdo del hijo derecho */ else{aux->clave = p1->clave; p2->hizq = p1->hder;} /* (Fig.6.28) */ free(p1); /* Eliminamos el nodo de clave mnima */ } } return(T); }

173

A continuaci n se representan cada una de las situaciones que pueden suceder o en el borrado de un nodo en un arbol binario de b squeda. Tambi n, se muestra el u e arbol resultante despu s de realizar cada borrado. Debajo de las guras se indican e las instrucciones que se llevan a cabo, dada la situaci n que se muestra en la gura. o En las guras 6.18, 6.19 y 6.20 se muestran los distintos casos que pueden darse al borrar un nodo que es hoja (Caso 1).

15

aux

aux_padre=null
(i) T=NULL; free(aux); return(T);

Figura 6.18: Borrado x = 15. Caso: hoja que no tiene padre (aux padre = N U LL). En este caso, el nodo a borrar es el unico nodo del arbol, por lo que el borrado devuelve el arbol vaco (T = N U LL).

T 7 3

15 18
/

T 7
21
/

15 18
/

/ /

11 9

/ /

11
/

21
/

aux_padre aux

20
/ /

/ /

20
/ /

(a) aux padre->hizq=NULL; free(aux);

(b) return(T);

Figura 6.19: Borrado x = 9. Caso: hoja que es hijo izquierdo de un nodo. Para realizar el borrado se actualiza a N U LL el puntero al hijo izquierdo del padre (apuntado por aux padre), y se libera la memoria del nodo.

174

T 7 3

15

aux_padre 18
/

T 7

15 18 11 9

/ /

/ /

11 9

21
/ /

/ /

aux
/ /
/ /

(a) aux padre->hder = NULL; free(aux);

(b) return(T);

Figura 6.20: Borrado x = 21. Caso: hoja que es hijo derecho de un nodo. Para realizar el borrado se actualiza a N U LL el puntero al hijo derecho del padre (apuntado por aux padre), y se libera la memoria del nodo. En las guras 6.21, 6.22 y 6.23, 6.24, 6.25 y 6.26 se muestran los distintos casos que pueden darse al borrar un nodo interno que tiene un unico hijo (Caso 2). En las guras 6.21, 6.22 y 6.23, el nodo a borrar tiene un unico hijo derecho.

15

aux 18
/

aux_padre=null

T
21
/ /

18
/

21
/ /
(b) return(T);

(a) T=aux->hder; free(aux);

Figura 6.21: Borrado x = 15. Caso: nodo interno que es raz del arbol y tiene un unico hijo derecho. Para realizar el borrado se actualiza el puntero T para que apunte al hijo derecho, y se realiza el borrado liberando la memoria del nodo (apuntado por aux).

175

T aux 7
/

15 18
/

aux_padre

T
21
/ /

15 18
/

11 9

11 9

21
/ /

/ /

/ /

(a) aux padre->hizq=aux->hder; free(aux);

(b) return(T);

Figura 6.22: Borrado x = 7. Caso: nodo interno que es hijo izquierdo de un nodo y tiene
un unico hijo derecho. Antes de borrar el nodo, el hijo debe ser enlazado correctamente con el padre del nodo a borrar; para ello, se actualiza el puntero hijo izquierdo del padre (apuntado por aux padre) para que apunte al hijo derecho del nodo a borrar (apuntado por aux); nalmente, se realiza el borrado liberando la memoria del nodo.

T 7
/

15

aux_padre 18 aux 21
/
/

T 7
/

15 18 16 25

3 5

16
/ /

3 5

/ /

/ /

25
/ /

/ /

/ /

(a) aux padre->hder=aux->hder; free(aux);

(b) return(T);

Figura 6.23: Borrado x = 21. Caso: nodo interno que es hijo derecho de un nodo y tiene
un unico hijo derecho. Antes de borrar el nodo, el hijo debe ser enlazado correctamente con el padre del nodo a borrar; para ello, se actualiza el puntero hijo derecho del padre (apuntado por aux padre) para que apunte al hijo derecho del nodo a borrar (apuntado por aux); nalmente, se realiza el borrado liberando la memoria del nodo.

En las guras 6.24, 6.25 y 6.26, el nodo a borrar tiene un unico hijo izquierdo.

176

T 7 3

15
/

aux aux_padre=null

T 3

7 11 9

/ /

11 9

/ /

/ /

/ /

(a) T=aux->hizq; free(aux);

(b) return(T);

Figura 6.24: Borrado x = 15. Caso: nodo interno que es raz del arbol y tiene un unico hijo izquierdo. Para realizar el borrado se actualiza el puntero T para que apunte al hijo izquierdo, y se realiza el borrado liberando la memoria del nodo (apuntado por aux).

T aux_padre aux 1 3 7

15 18
/

T 7

15 18
/

11 9

21
/ /

1
/ /

11 9

21
/ /

/ /

/ /

/ /

(a) aux padre->hizq=aux->hizq; free(aux);

(b) return(T);

Figura 6.25: Borrado x = 3. Caso: nodo interno que es hijo izquierdo de un nodo y tiene
un unico hijo izquierdo. Antes de borrar el nodo, el hijo debe ser enlazado correctamente con el padre del nodo a borrar; para ello, se actualiza el puntero hijo izquierdo del padre (apuntado por aux padre) para que apunte al hijo izquierdo del nodo a borrar (apuntado por aux); nalmente, se realiza el borrado liberando la memoria del nodo.

177

T 7
/

15 19 17
/

aux_padre aux
/

T 7
/

15 17
/

3 5

3 5

18
/ /

18
/ /

/ /

/ /

(a) aux padre->hder=aux->hizq; free(aux);

(b) return(T);

Figura 6.26: Borrado x = 19. Caso: nodo interno que es hijo derecho de un nodo y tiene
un unico hijo izquierdo. Antes de borrar el nodo, el hijo debe ser enlazado correctamente con el padre del nodo a borrar; para ello, se actualiza el puntero hijo derecho del padre (apuntado por aux padre) para que apunte al hijo izquierdo del nodo a borrar (apuntado por aux); nalmente, se realiza el borrado liberando la memoria del nodo.

En las guras 6.27 y 6.28 se muestran los casos que pueden darse al borrar un nodo interno que tiene dos hijos (Caso 3).
T 7
/

15 18 17

aux_padre aux p1 25
/

T 7
/

15 25 17 31
/

3 5

p2=null 31
/

/ /

3 5

/ /

/ /

26
/ /

26
/ /

/ /

(a) aux->clave=p1->clave;aux->hder=p1->hder;free(p1);

(b) return(T);

Figura 6.27: Borrado x = 18. Caso: nodo interno que tiene dos hijos y su hijo derecho es
el nodo de clave mnima de su sub rbol derecho. La clave del nodo a borrar se sustituye por a la clave mnima de su sub rbol derecho. Esta modicaci n implica que el nodo de clave mnima a o ha pasado a ocupar la posici n de su padre, se enlazar su hijo derecho ( nico hijo), como hijo o a u derecho del nuevo nodo que ocupa. Finalmente, se libera el nodo que ocupaba, la clave minima del sub rbol derecho del nodo a borrar. a

178

T 7
/

10

aux aux_padre=null 18

T 7
/

12 18 16
/

p2
/

16
/

25
/

p1

12 15
/

31
/

25
/

26
/ /

15
/

31
/

13
/ /

13
/ /

26
/ /

(a) aux->clave=p1->clave; p2->hizq=p1->hder; free(p1);

(b) return(T);

Figura 6.28: Borrado x = 10. Caso: nodo interno que tiene dos hijos y el nodo de clave
mnima de su sub rbol derecho no es su hijo derecho. La clave del nodo a borrar se sustituye a por la clave mnima de su sub rbol derecho; como el padre del nodo de clave mnima no ha pasado a a ocupar la clave mnima, su hijo derecho ( nico hijo) se enlaza con el nodo que era su padre antes u del cambio. As, el hijo derecho del nodo de clave mnima pasa a ser hijo izquierdo del nodo padre (apuntado por p2). Finalmente, se libera el nodo que ocupaba la clave minima del sub rbol a derecho del nodo a borrar.

El coste temporal de borrar un elemento en un arbol binario de busqueda rbol. ser O(h), donde h es la altura del a a Ejercicio: Las siguientes deniciones de tipos y variables sirven para representar arboles binarios de b squeda: u typedef ... tipo_baseT; typedef struct snodo { tipo_baseT clave; struct snodo *hizq, *hder; } abb; abb *T; Escribir una funci n recursiva que imprima por pantalla las claves almaceo nadas en un arbol T que sean menores que una clave dada k. En el caso peor, el coste del algoritmo debe ser O(n), siendo n el n mero de nodos del arbol. Sugu erencia: Una posible soluci n la puede proporcionar una modicaci n adecuada o o de alg n algoritmo ya conocido de recorrido de arboles. u

179

void menores(abb *T, tipo_baseT k) { if (T != NULL) { if (T->clave < k) { printf("%d ",T->clave); menores(T->hizq,k); menores(T->hder,k); } else menores(T->hizq,k); } }

6.4.

Montculos (Heaps). Colas de prioridad.

Antes de denir la estructura de datos montculo, recordaremos brevemente qu es un arbol binario completo: un arbol binario completo es un arbol binario e en el que todos los niveles tienen el m ximo n mero posible de nodos excepto, a u puede ser, el ultimo. En ese caso, las hojas del ultimo nivel est n tan a la izquierda a 2 como sea posible . Un montculo o heap es un conjunto de n elementos representados en un arbol binario completo en el que se cumple la siguiente propiedad: para todo nodo i, excepto el nodo raz, la clave del nodo i es menor o igual que la clave del nodo padre de i. A esta propiedad le denominaremos propiedad de montculo. La representaci n de un montculo se realiza utilizando la representaci n veco o torial de los arboles binarios completos; es decir: En la posici n 1 del vector se encuentra el nodo raz del arbol. o Dado un nodo que ocupa la posici n i en el vector: o En la posici n 2i se encuentra el nodo que es su hijo izquierdo. o En la posici n 2i + 1 se encuentra el nodo que es su hijo derecho. o En la posici n i/2 se encuentra el nodo padre si i > 1. o Desde la posici n n/2 + 1 hasta la posici n n, donde n es el n mero de o o u nodos del arbol binario completo, se encuentran las claves de los nodos que son hojas.
2

En el tema de arboles se introdujo el concepto de arbol binario completo

180

Por lo tanto, en un montculo representado mediante un vector A se cumplir que: a A[ i/2 ] A[i], 1 < i n.
1 16 2 14 4 8 8 2 4 9 10 1 7 5 6 9 3 10 7
1 2 3 4 5 6 7 8 9 10

16 14 10 8 7

3 2 4

Figura 6.29: Representaci n de un montculo como arbol binario completo y como o vector. Dado que, posiblemente, el vector A que representa un montculo tendr un a tama o superior al n mero de elementos almacenados en el montculo, ser necen u a ndice que indique el n mero de nodos que almacena el montcusario mantener un u lo; a este ndice le denominaremos talla Heap.
1 2 3 4 5 6 7 8 9 10

16 14 10 8 7

3 2 4

...

tamanyo

talla_Heap

Debido a la propiedad que satisfacen todos los nodos de un montculo, en un montculo se cumple que: El nodo de clave m xima se encuentra en la raz del arbol. a Cada rama del arbol est ordenada de mayor a menor desde la raz hasta las a hojas. Adem s, debido a que un montculo se representa en un arbol binario coma pleto, su altura ser (log n), donde n es el n mero de elementos que contiene el a u montculo.

6.4.1.

Manteniendo la propiedad de montculo

Supongamos que tenemos un arbol binario completo donde existe un nodo i que tiene un sub rbol izquierdo y un sub rbol derecho que son montculos. a a Supongamos tambi n, que la clave del nodo i es menor que alguna de las claves e 181

de los nodos que son sus hijos (A[i] < A[2i] y/o A[i] < A[2i + 1]) (ver gura 6.30). En este caso, el sub rbol que parte del nodo i no cumplira la propiedad de a montculo. Una funci n muy importante en el manejo de montculos es aqu lla o e que, dado un nodo que viola la propiedad de montculo, transforma el sub rbol a que parte de ese nodo en un montculo, manteniendo as la propiedad de montcu lo.
1 16 2 4 4 14 8 2 8 9 10 1 7 5 6 9 3 10 7 3

Figura 6.30: El nodo 2 no cumple la propiedad de montculo. Obs rvese que los e sub rboles izquierdo y derecho del nodo 2 s que son montculos. a La funci n, denominada heapify, transforma el sub rbol que parte de un nodo o a i en un montculo. La funci n asume que los sub rboles izquierdo y derecho del o a nodo i son montculos. A continuaci n se presenta una versi n recursiva de la o o funci n heapify. La funci n recibe un vector M que representa un arbol binario o o completo, y un ndice i que indica el nodo raz del sub rbol que se desea que sea a un montculo.

182

void heapify(tipo_baseT *M, int i) { tipo_baseT aux; int hizq, hder, mayor; hizq = 2*i; hder = 2*i+1; if ((hizq<=talla_Heap) && (M[hizq]>M[i])) mayor = hizq; else mayor = i; if ((hder<=talla_Heap) && (M[hder]>M[mayor])) mayor = hder; if (mayor != i) { aux = M[i]; M[i] = M[mayor]; M[mayor] = aux; heapify(M,mayor); } } La funci n heapify compara inicialmente la clave del nodo i (M [i]) con la de o su hijo izquierdo (M [2i]), si lo tiene, y almacena en la variable mayor el ndice del nodo que tiene la clave mayor de los dos; a continuaci n compara la clave del o nodo obtenido (M [mayor]) con la clave del hijo derecho del nodo i (M [2i+1]), si lo tiene, almacenando nuevamente en la variable mayor el ndice del nodo mayor. Una vez ha obtenido qu nodo de los tres (nodo i, hijo izquierdo, hijo derecho) e tena la clave mayor, si el nodo de clave mayor era el nodo i la funci n naliza ya o que, para el sub rbol que parte del nodo i, se cumplir la propiedad de montculo. a a En caso contrario, uno de los dos hijos es el nodo mayor, en ese caso, la claves del nodo i y del nodo mayor se intercambian, consiguiendo que el nodo i y sus hijos cumplan la propiedad de montculo. Debido a que el cambio puede suponer que ahora la propiedad de montculo no se cumpla en el sub rbol que parta del nodo a de clave mayor, se deber aplicar recursivamente el algoritmo heapify sobre ese a nodo. Ejemplo del funcionamiento de heapify A continuaci n se muestra un ejemplo del funcionamiento de heapify para o una llamada inicial heapify(M,2), donde M es el vector que se muestra en la gura y talla Heap = 10.

183

1 16 2 4 i 4 hizq 14 8 2 8 5 hder 7 9 10 1 6 9 3 10 7 M
1 2 3 4 5 6 7 8 9 10

3
tamanyo

16 4 10 14 7 i hizq hder

3 2 8

...

talla_Heap

(a) El nodo 2 no cumple la propiedad de montculo ya que no es mayor que sus hijos.
1 16 2 4 i 4 mayor 14 8 2 8 9 10 1 5 7 6 9 3 10 7 M
1 2 3 4 5 6 7 8 9 10

3
tamanyo

16 4 10 14 7 i mayor

3 2 8

...

talla_Heap

(b) El nodo de clave mayor es el hijo izquierdo del nodo i por lo que se intercambian sus claves. A continuaci n se realiza la llamada recursiva heapify(M,4) o
1 16 2 14 4 4 i 8 9 10 2 1 8 hizq hder 7 5 6 9 3 10 7 M
1 2 3 4 5 6 7 8 9 10

3
tamanyo

16 14 10 4 7 i

9 3 2 8 1

...

hizq hder talla_Heap

(c) La modicaci n de la clave del nodo 4 en la llamada anterior, ha provocado que ahora o sea este nodo el que no cumple la propiedad de montculo.

184

1 16 2 14 4 i 4 8 2 9 10 1 8 mayor 5 7 6 9 3 10 7 M
1 2 3 4 5 6 7 8 9 10

3
tamanyo

16 14 10 4 i

3 2 8 mayor

...

talla_Heap

(d) El nodo de clave mayor es el hijo derecho del nodo i por lo que se intercambian sus claves. A continuaci n se realiza la llamada recursiva heapify(M,9). o
1 16 2 14 4 8 2 5 8 9 10 4 i 1 mayor 7 6 9 3 10 7 M
1 2 3 4 5 6 7 8 9 10

3
tamanyo

16 14 10 8 7

3 2 4 i mayor

...

talla_Heap

(e) Por ultimo, debido a que el nodo 9 no tiene hijos, el nodo de clave mayor es el nodo i, por lo que las llamadas recursivas nalizan obteniendo el arbol resultante. Obs rvese que, e ahora, el sub rbol que tiene como raz el nodo 2, nodo sobre el que se aplic inicialmente a o la acci n heapify, s que es un montculo. o

Coste temporal de heapify El coste temporal del algoritmo vendr determinado por el coste que supone a obtener la clave del nodo mayor, entre el nodo i, su hijo izquierdo y su hijo derecho, m s el coste que supone aplicar la funci n heapify sobre un sub rbol cuya a o a raz es uno de los hijos del nodo i. Por lo tanto, el caso mejor ocurrir cuando la clave del nodo i sea mayor que a las claves de sus hijos. En este caso, el coste temporal ser unicamente el coste de a obtener el nodo de clave mayor: O(1). El caso peor se producir cuando la acci n heapify se tenga que realizar sobre a o el m ximo n mero posible de nodos. Este caso ocurrir cuando la llamada inicial a u a a la funci n heapify se realice sobre el nodo raz del arbol, y para cada nodo sobre o el que se aplique heapify, se cumpla que la clave del nodo es menor que alguna de las claves de sus hijos. En este caso, dado que la acci n heapify se aplica sobre o 185

un nodo en cada nivel del arbol, el coste vendr determinado por O(h), donde h a es la altura del arbol. Dado que la altura de un arbol binario completo es log2 n , donde n es el n mero de nodos que contienen el arbol, el coste de la funci n u o heapify en el caso peor es O(log n). En general, para un nodo i, el coste de aplicar heapify sobre ese nodo, en el caso peor, ser O(h) donde h es la altura del nodo i. a

6.4.2.

Construir un montculo

Problema: Dado un conjunto de n elementos representados en un arbol binario completo mediante un vector M , se desea transformar el vector para que sea un montculo. Estrategia: Dado que los nodos que son hojas ya cumplen la propiedad de montcu lo, tras aplicar la funci n heapify sobre cada uno del resto de nodos, comenzando o por el nodo de mayor ndice que no es hoja (M [ n/2 ]) y recorriendo decrecien temente hasta el nodo raz del arbol (M [1]), se conseguir que todos los nodos a cumplan la propiedad de montculo, por lo que el arbol binario completo resul tante ser un montculo. El orden en que se aplica sobre cada uno de los nodos la a funci n heapify, garantiza que cuando se aplica la acci n heapify sobre un nodo o o siempre se cumple que sus sub rboles son montculos. a A continuaci n se presenta una funci n en C que, dado un vector M que cono o tiene n elementos M [1, . . . , n], transforma el vector en un montculo siguiendo la estrategia indicada. void build_Heap(tipo_baseT *M, int n) { int i; talla_Heap = n; for (i=n/2; i>0; i--) heapify(M,i); }

186

Ejemplo del funcionamiento de build Heap A continuaci n se muestra un ejemplo del funcionamiento de la funci n build Heap o o al aplicarlo sobre el vector M que se muestra en la gura; la llamada inicial es: build Heap(M,10). Los nodos sombreados representan los nodos del arbol sobre los que se ha aplicado la acci n heapify; a excepci n de las hojas sobre las que no o o es necesario aplicar heapify ya que inicialmente son montculos.
1 4 2 1 4 2 8 14 1 4 2 1 9 10 7 4 2 5 16 6 9 7 10 8 14 9 8 10 7 16 5 6 9 10 3 7 3

3 3

(a) build Heap(M,10)

187

1 4 2 1 4 2 8 14 1 4 2 1 5 mayor 16 i 9 10 7 4 2 5 6 16 9 i=mayor 7 10 8 14 9 8 10 7 talla_Heap 6 9 10 3 7 3

1 4 2 1 4 2 8 14 1 4 2 1 9 10 7 4 2 5 16 6 9 7 10 8 14 9 8 10 7 talla_Heap 16 5 6 9 10 3 7 3

3 3

3 3

(b) Heapify(M,5): En este caso, el nodo 5 ya cumple la propiedad de montculo.

(c) Heapify(M,5): La acci n de heapify soo bre el nodo 5 no realiza ninguna modicaci n o sobre el arbol.
1 4

1 4 2 1 4 2 i 8 mayor 14 1 4 2 1 9 10 7 4 2 i 5 16 6 9 7 10 8 9 10 14 8 7 mayor talla_Heap


M 1 4

3 3 5 16 6 9 10
8 2 2 1

2 1 3 5 14 i 9 10 7 4 14 5 16 6 9 7 10 16 6 9 4

7 10

3 3

3 3

8 2

9 8

10 7 talla_Heap

(d) Heapify(M,4): El nodo 4 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.
1 4 2 1 4 14 8 2 1 4 2 1 9 10 7 4 14 5 6 16 9 7 8 10 2 mayor 9 8 10 7 talla_Heap 16 5 6 9 10 7 mayor i 3 3

(e) Heapify(M,4): La acci n de heapify soo bre el nodo 4 transforma el sub rbol de raz en a el nodo 4 en un montculo.
1 4 2 1 4 14 8 2 1 4 2 1 9 10 7 8 3 4 10 14 5 6 16 9 7 3 8 2 9 8 10 7 talla_Heap 16 5 6 9 3 3 10 7

3 3 i

(f) Heapify(M,3): El nodo 3 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.

(g) Heapify(M,3): La acci n de heapify soo bre el nodo 3 transforma el sub rbol de raz en el a nodo 3 en un montculo.

188

1 4 2 i 1 4 14 8 2 1 4 2 1 i 9 10 7 5 6 16 9 mayor 7 3 8 2 9 8 10 7 talla_Heap 1 4 6 5 9 16 mayor 8 3 10 7 3 8 2 2 16 4 14 9 10 7 4 14 5 1 1 2 16 5 4

1 3 10 6 9 3 7

3 4 10 14

3 10

6 9

7 3

8 2

9 8

10 7 talla_Heap

(h) Heapify(M,2): El nodo 2 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.
1 4 2 16 4 14 8 2 1 4 2 16 i 1 9 10 7 mayor 4 14 5 1 i 6 9 7 3 8 2 9 8 talla_Heap 10 7 mayor 5 6 9 3 3 10 7

(i) heapify(M,2): La modicaci n de la o clave del nodo 5 provoca una llamada recursiva a Heapify(M,5).
1 4 2 16 4 14 8 2 M 1 4 2 16 9 10 1 4 14 5 7 6 9 7 3 8 2 9 8 10 1 talla_Heap 7 5 6 9 3 3 10 7

3 10

3 10

(j) heapify(M,5): El nodo 5 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.

(k) heapify(M,5): La acci n heapify soo bre el nodo 5 transforma el sub rbol de raz a en este nodo nuevamente en un montculo; al ser la ultima de las modicaciones iniciadas en heapify(M,2) se cumple tambi n que el e sub rbol de raz en el nodo 2 es un montculo. a

189

1 i 4 2 mayor 16 4 14 8 2 M 9 10 1 4 14 5 7 6 9 7 3 8 2 9 8 10 1 talla_Heap 1 M 16 7 5 6 9 3 3 10 7 4 14 8 2 2 4 9 10 8 1 3 10 4 14 5 7 7 2 4 5 16

1 3 10 6 9 3 7

1 2 3 4 16 10 i mayor

6 9

7 3

8 2

9 8

10 1 talla_Heap

(l) heapify(M,1): El nodo 1 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.
1 16 2 4 i 4 mayor 14 8 2 1 M 16 2 4 i 9 10 8 1 3 10 4 5 14 7 mayor 6 9 7 3 8 2 9 8 10 1 talla_Heap 5 7 6 9 3 3 10 7

(m) heapify(M,1): La modicaci n de la o clave del nodo 2 provoca una llamada recursiva a Heapify(M,2).
1 16 2 14 4 4 8 2 1 M 16 2 14 9 10 8 1 3 10 4 4 5 7 6 9 7 3 8 2 9 8 10 1 talla_Heap 7 5 6 9 3 3 10 7

(n) Heapify(M,2): El nodo 2 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.
1 16 2 14 4 i 4 8 2 1 M 16 5 7 6 9 3 3 10 7

( ) heapify(M,2): La modicaci n de la n o clave del nodo 4 provoca una llamada recursiva a Heapify(M,4).
1 16 2 14 4 8 8 2 9 4 8 7 10 5 7 6 9 7 3 8 2 9 4 10 1 talla_Heap 5 6 9 3 3 10 7

10 9 8 1 mayor 3 4 5 2 14 10 4 7 i

4 3 10

6 9

7 3

8 2

talla_Heap 9 10 8 1 mayor

1 M 16

2 14

(o) Heapify(M,4): El nodo 4 no cumple la propiedad de montculo. Se intercambia su clave con la clave mayor de sus hijos.

(p) heapify(M,4): La acci n heapify soo bre el nodo 4 transforma el sub rbol de raz a en este nodo nuevamente en un montculo; al ser la ultima de las modicaciones iniciadas en heapify(M,1) se cumple tambi n que el e a 190 sub rbol de raz en el nodo 1 es un montculo, es decir, se ha transformado el vector inicial en un montculo.

Coste temporal de build Heap Sabiendo que el coste temporal de heapify es O(log n), una forma de obtener f cilmente una cota superior para el coste temporal del algoritmo build Heap, a sera asumir que cada llamada a la funci n heapify tiene un coste O(log n). De o esta forma, podramos decir que, como m ximo, el coste temporal de transformar a un vector de talla n en un montculo es O(n log n). Sin embargo, a n siendo esta u cota superior correcta, no es ex ctamente la m s cercana a la cota real. a a Dado que aplicar la acci n heapify sobre un nodo tiene un coste temporal de o a O(h), donde h es la altura del nodo, el coste temporal de build heap depender de la altura a la que se encuentren cada uno de los nodos sobre los que se aplica heapify durante la construcci n del montculo. Por lo tanto, para cada altura h, o tendremos un coste de Nh O(h), donde Nh es el n mero de nodos que tienen altura u h en el arbol. Para determinar el coste temporal, ser necesario establecer una relaci n entre a o la altura de un nodo y el n mero m ximo de nodos que pueden tener esa altura. u a Dado que en un arbol binario completo que contiene n nodos se cumple que su altura es log2 n , y que para cada nivel i el n mero m ximo de nodos es 2i1 ; se u a puede establecer una relaci n entre la altura de un nodo y el n mero m ximo de o u a nodos que puede contener el arbol a esa altura, de la siguiente forma:

Altura (h) [log(n)] [log(n)]1 [log(n)]2

Nivel (i) 1 2

Num. maximo de nodos 2 2 2 2 2


0

1 0

. . .

...

. . .

[log(n)] [log(n)]+1

[log(n)]1

[log(n)]

Observando la gura puede verse que para cada altura h se cumple que el n mero de nodos Nh es: u Nh 2 . 191
log2 n h

log2 n

2h

Utilizando esta relaci n podemos obtener una cota superior del coste tempoo ral del algoritmo build Heap. Para ello, para cada altura del arbol, evaluaremos el coste que supone aplicar heapify sobre cada uno de sus nodos. La siguiente expresi n realiza este c lculo: o a
log2 n

T (n) =
h=0 log2 n

Nh O(h) 2
log2 n

h=0 log2 n

2h 2log2 n h 2h 1 2

h=0 log2 n

= n
h=0 h=0

Dado que

hxh = x/(1 x)2 , si 0 < x < 1, entonces:


log2 n

n
h=0

1 2

1/2 n (1 1/2)2

= 2n O(n) Como se ha visto el coste temporal del algoritmo build Heap es O(n). Por lo tanto, podemos concluir que construir un montculo partiendo de un vector tiene un coste temporal lineal.

6.4.3.

Algoritmo de ordenaci n Heapsort o

El algoritmo heapsort ordena de manera no decreciente los elementos almacenados en un vector. Dado un vector M de n elementos M [1, . . . , n], el algoritmo realiza la siguiente estrategia: 1. utiliza la funci n build Heap para transformar el vector M en un montculo; o 2. El algoritmo heapsort explota el hecho de que la clave mayor de un montcu lo siempre se encuentra en el nodo raz del montculo. Por ello, intercambia la clave del nodo raz (valor m ximo) con la clave del ultimo nodo almace a nado en el montculo. De esta forma, el elemento mayor del montculo se ubica en la posici n que debe ocupar tras la ordenaci n del vector; o o 192

M
1 2

... ...

n2 n1 n

tallaH
n2 n1 n

<=

3. dado que uno de los elementos ya ha sido colocado en la posici n que le o corresponde (al nal del montculo), se reduce la talla del montculo en 1, consiguiendo, as, excluir este elemento de las siguientes acciones a realizar en la ordenaci n; o
1 2

...

n2 n1

tallaH

4. debido a que, en el intercambio, el valor que ha sido colocado en la raz puede violar la propiedad de montculo, se aplica heapify sobre el nodo raz para que el vector M vuelva a ser un montculo;
heapify
1 2 n2 n1 n

M tallaH

5. tras restablecer la propiedad de montculo en el vector M , se repiten los pasos (2), (3) y (4) hasta conseguir la ordenaci n total del vector. o

193

M heapify
1 2

... ... ... ...


3

n1

tallaH
n2 n1 n

M
1 2

tallaH
n2 n1 n

M heapify
1 2

tallaH

n3 n2 n1 n

...
1 2

tallaH

M tallaH heapify
1 2 3

... ... ... ... ...

M tallaH
1 2 3

M tallaH heapify
1 2 3

M tallaH
1

A continuaci n, se presenta una funci n que, dado un vector M que contiene n o o elementos M [1, . . . , n], ordena el vector M siguiendo la estrategia del algoritmo heapsort:

194

void heapsort(tipo_baseT *M, int n) { int i; tipo_baseT aux; build_Heap(M,n); for (i=n; i>1; i--) { aux = M[i]; M[i] = M[1]; M[1] = aux; talla_Heap--; heapify(M,1); } } Ejemplo del funcionamiento del algoritmo heapsort
1 16 2 14 4 8 8 2 9 7 i 10 1 5 6 9 3 8 2 3 10 7 4 8 9 9 10 4 16 tallaH 7 2 14 5 6 9 3
8 2

1 1 3 10 7
4 4 9 7 2 8 5 14

1 3 10 6 9 3 7

i 1 2 3 4 5 6 7 8 9 10 16 14 10 8 7 9 3 2 4 1 tallaH

1 2 3 4 5 6 7 8 1 14 10 8 7 9 3 2

1 2 3 4 5 6 7 8 14 8 10 4 7 9 3 2

9 10 1 16 tallaH

(a) M[1] M[i]


1 14 2 8 4 4 8 2 9 i 7 5 6 9 3 3 10 7

(b) heapify(M,1)
1 1 2 8 4 4 8 2 7 5 6 9 3 3 10 7 4

(c) n iteraci n i = 10 o
1 10 2 8 5 4 8 2 1 2 3 4 5 6 7 8 9 10 10 8 9 4 7 1 3 2 14 16 tallaH 7 6 1 3 9 7 3

1 2 3 4 5 6 7 8 14 8 10 4 7 9 3 2

i 9 10 1 16 tallaH

1 2 3 4 5 6 7 8 9 10 1 8 10 4 7 9 3 2 14 16 tallaH

(d) M[1] M[i]

(e) heapify(M,1)

(f) n iteraci n i = 9 o

195

1 10 2 8 4 4 8 i 2 i 1 2 3 4 5 6 7 8 9 10 10 8 9 4 7 1 3 2 14 16 tallaH 7 5 6 1 3 9 7 4 4 7 3 2 8 5 2

1 9 3 9 6 1 3 7 4 4 7 2 8 5

1 3 3 6 1 2 7

1 2 3 4 5 6 7 8 9 10 2 8 9 4 7 1 3 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 9 8 3 4 7 1 2 10 14 16 tallaH

(g) M[1] M[i]


1 9 2 8 4 4 7 5 6 1 3 i 2 7 4 4 3

(h) heapify(M,1)
1 2 2 8 5 7 6 1 3 3

(i) n iteraci n i = 8 o
1 8 2 7 4 4 2 5 6 1 3 3

i 1 2 3 4 5 6 7 8 9 10 9 8 3 4 7 1 2 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 2 8 3 4 7 1 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 8 7 3 4 2 1 9 10 14 16 tallaH

(j) M[1] M[i]


1 8 2 7 4 4 2 5 6 1 i 3 3

(k) heapify(M,1)
1 1 2 7 4 4 2 5 3 4 1 3

(l) n iteraci n i = 7 o
1 7 2 4 5 2 3 3

i 1 2 3 4 5 6 7 8 9 10 8 7 3 4 2 1 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 1 7 3 4 2 8 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 7 4 3 1 2 8 9 10 14 16 tallaH

(m) M[1] M[i]


1 7 2 4 4 1 i 5 2 3 4 1 3

(n) heapify(M,1)
1 2 2 4 3 4 1 3

( ) n iteraci n i = 6 n o
1 4 2 2 3 3

i 1 2 3 4 5 6 7 8 9 10 7 4 3 1 2 8 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 2 4 3 1 7 8 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 4 2 3 1 7 8 9 10 14 16 tallaH

(o) M[1] M[i]

(p) heapify(M,1)

(q) n iteraci n i = 5 o

196

1 4 2 2 4 1 i 1 2 3 4 5 6 7 8 9 10 4 2 3 1 7 8 9 10 14 16 tallaH i 3 3 2 2 1

1 3 3 3 2 2

1 3 1

1 2 3 4 5 6 7 8 9 10 1 2 3 4 7 8 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 3 2 1 4 7 8 9 10 14 16 tallaH

(r) M[1] M[i]


1 3 3 2 1 i i 1 2 3 4 5 6 7 8 9 10 3 2 1 4 7 8 9 10 14 16 tallaH 2

(s) heapify(M,1)
1 1 2 2 1 2 3 4 5 6 7 8 9 10 1 2 3 4 7 8 9 10 14 16 tallaH

(t) n iteraci n i = 4 o
1 2 2 1 1 2 3 4 5 6 7 8 9 10 2 1 3 4 7 8 9 10 14 16 tallaH

(u) M[1] M[i]


1 2 2 1 i i 1 2 3 4 5 6 7 8 9 10 2 1 3 4 7 8 9 10 14 16 tallaH

(v) heapify(M,1)
1 1

(w) n iteraci n i = 3 o

1 2 3 4 5 6 7 8 9 10 1 2 3 4 7 8 9 10 14 16 tallaH

1 2 3 4 5 6 7 8 9 10 1 2 3 4 7 8 9 10 14 16

(x) M[1] M[i]

(y) heapify(M,1)

Coste temporal de heapsort Debido a que el coste temporal de construir un montculo es O(n), y que cada una de las n 1 llamadas que realiza el algoritmo a la funci n heapify tienen un o coste temporal O(log n), podemos concluir que el algoritmo heapsort tiene un coste temporal O(n log n). Unicamente en el caso de que todos los elementos del vector fueran iguales el coste temporal del algoritmo heapsort sera O(n).

197

6.4.4.

Colas de prioridad

Una de las aplicaciones m s usuales de un montculo es su utilizaci n como a o una cola de prioridad. Una cola de prioridad es una estructura de datos que mantiene un conjunto S de elementos, cada uno de los cuales tiene asociado un valor llamado clave (prioridad). Una cola de prioridad soporta las siguientes operaciones: Insert(S,x): inserta el elemento x en el conjunto S. Extract Max(S): borra y devuelve el elemento de S con la clave mayor. Maximum(S): devuelve el elemento de S con la clave mayor. Una de las aplicaciones de las colas de prioridad es la planicaci n de proceo sos en un sistema compartido. La cola de prioridad mantiene la informaci n de o los procesos con sus prioridades asociadas. Cuando un proceso naliza o es interrumpido, el proceso de mayor prioridad se selecciona para entrar en ejecuci n o utilizando la operaci n Extract-Max. Nuevos procesos pueden ser a adidos a la o n cola en cualquier momento utilizando la operaci n Insertar. o Insertar un elemento en un montculo La inserci n de un nuevo elemento en un montculo debe hacerse de tal forma o que el nuevo conjunto contin e siendo un montculo. Para conseguirlo, el algoritu mo de inserci n realiza la siguiente estrategia: o 1. expande el tama o del montculo en 1 (talla Heap + 1), e inserta el nuevo n elemento en esa posici n del vector. De esta forma, el nuevo elemento se o a ade como una hoja que ocupa la posici n libre m s a la izquierda posible n o a del ultimo nivel del arbol; 2. a continuaci n, compara la clave del nuevo elemento con la clave de su noo do padre; si la clave del nuevo elemento es mayor, la inserci n ha provocado o que el nodo padre no cumpla la propiedad de montculo, por lo que se inter cambian las claves. Debido a que el intercambio de claves puede provocar que nuevamente no se cumpla la propiedad de montculo, esta vez para el nodo que tras el intercambio ha pasado a ser padre del nuevo elemento, se repite el intercambio de claves hasta que se cumpla que la clave del nodo padre del nuevo elemento es mayor o igual, o hasta que el nuevo elemento ocupa la raz del montculo.

198

A continuaci n se presenta una funci n que, dado un montculo M inserta o o un nuevo elemento de clave x. Antes de realizarse la llamada a la funci n, deo bera comprobarse si el tama o fsico del vector permite la inserci n de un nuevo n o elemento. void insert(tipo_baseT *M, tipo_baseT x) { int i; talla_Heap++; i = talla_Heap; while ( (i > 1) && (M[i/2] < x) ) { M[i] = M[i/2]; i = i/2; } M[i] = x; } Ejemplo del funcionamiento del algoritmo insert A continuaci n se muestra un ejemplo de c mo se realiza la inserci n del o o o elemento de clave 15 en el arbol de la gura. Debajo de cada gura se indican las instrucciones que se han llevado a cabo para obtener el arbol que se muestra.

199

1 16 2 14 4 8 8 2 9 7 10 5 6 9 3
8 2 16

1 16 3 10 5 8 9 10 1 7 i 6 9 3 8 2 7 4 8 2 14 i 9 10 1 5

1 3 10 6 9 3 7

3 10 7
4

2 14

1 2 3 4 5 6 7 8 9 10 16 14 10 8 7 9 3 2 4 1 tallaH

i 1 2 3 4 5 6 7 8 9 10 11 16 14 10 8 7 9 3 2 4 1 tallaH

1 2 3 4 5 6 7 8 9 10 11 16 14 10 8 7 9 3 2 4 1 7 i tallaH

(a) arbol inicial


1 16 2 i 4 8 8 2 9 10 1 14 5

(b) talla Heap++;i=talla Heap;(c) M[i]=M[i/2];i=i/2;


1 16 3 10 6 9 3 8 2 7 4 8 9 10 1 14 2 i 15 5 6 9 3 3 10 7

1 2 3 4 5 6 7 8 9 10 11 16 14 10 8 14 9 3 2 4 1 7 i tallaH

1 2 3 4 5 6 7 8 9 10 11 16 15 10 8 14 9 3 2 4 1 7 i tallaH

(d) M[i]=M[i/2];i=i/2;

(e) M[i]=x;

Coste temporal del algoritmo El coste temporal de la inserci n depende del n mero de comparaciones que o u es necesario realizar, hasta encontrar la posici n que le corresponde al nuevo eleo mento en el montculo. Por lo tanto, en un montculo que contenga n elementos, el caso mejor ocurrir cuando el elemento se inserte inicialmente en la posici n que a o le corresponde, en cuyo caso unicamente ser necesario realizar una comparaa ci n: O(1). El caso peor se producir cuando el elemento insertado sea mayor o a que cualquier elemento del montculo; en este caso ser necesario ubicar el nue a vo elemento como raz del montculo, por lo que el n mero de comparaciones u necesarias ser : O(log n). a

200

Extraer el m ximo de un montculo a La extracci n del m ximo de un montculo supone obtener la clave almacenao a da en la raz del montculo (valor m ximo) y, a continuaci n, eliminar esta clave a o del montculo de tal forma que el nuevo conjunto siga siendo un montculo. La extracci n y borrado del m ximo de un montculo se realiza de la siguiente o a forma; dado un vector M que representa un montculo: 1. se obtiene el valor m ximo almacenado en el montculo, que se corresponde a con la clave almacenada en el nodo raz del mismo (M [1]). 2. se borra la clave obtenida sustituy ndola por el valor almacenado en la ultie ma posici n del montculo (M [1]=M [talla Heap]); y, a continuaci n, se o o reduce el tama o del montculo en 1 (talla Heap1). n 3. Debido a que el intercambio puede provocar que el nodo raz no cumpla la propiedad de montculo, se aplica la acci n heapify sobre el nodo raz para o restablecer la propiedad de montculo. A continuaci n se presenta el algoritmo Extract Max: o tipo_baseT extract_max(tipo_baseT *M) { tipo_baseT max; if (talla_Heap == 0) { fprintf(stderr,"Monticulo vacio"); exit(-1); } max = M[1]; M[1] = M[talla_Heap]; talla_Heap--; heapify(M,1); return(max); }

201

Ejemplo del funcionamiento del algoritmo Extract Max


1 16 2 14 4 8 8 2 9 7 10 5 6 9 3 8 2 3 10 7 4 8 9 7 10 2 14 5 6 9 3 16 3 10 7 1

1 2 3 4 5 6 7 8 9 10 16 14 10 8 7 9 3 2 4 1 tallaH

1 2 3 4 5 6 7 8 9 10 16 14 10 8 7 9 3 2 4 1 tallaH

(a) max=M[1];
1 1 2 14 4 8 8 2 9 7 5 6 9 3 3 10 7

(b) M[1] M[talla Heap]


1 14 2 8 4 4 8 2 9 7 5 6 9 3 3 10 7

1 2 3 4 5 6 7 8 9 1 14 10 8 7 9 3 2 4 tallaH

1 2 3 4 5 6 7 8 9 14 8 10 4 7 9 3 2 1 tallaH

(c) talla Heap--; heapify(M,1)

(d) return(max);

Coste temporal del algoritmo El coste temporal del algoritmo Extract Max, para un montculo que con tiene n elementos, es O(log n), ya que depende del tiempo que supone realizar la acci n heapify sobre el nodo raz del montculo: O(log n). o En resumen, un montculo que representa un conjunto de n elementos, so porta las operaciones de una cola de prioridad con un coste temporal O(log n).

202

6.5.

Estructura de datos para conjuntos disjuntos: MF-set

Dado un conjunto C, la clase de equivalencia de un elemento a C es el subconjunto de C que contiene todos los elementos relacionados con a. Obs rvese e que las clases de equivalencia forman una partici n de C: todo miembro de C o aparece en ex ctamente una clase de equivalencia. Para saber si un elemento a a tiene una relaci n de equivalencia con otro elemento b, unicamente necesitamos o vericar si a y b est n en la misma clase de equivalencia. a Un MF-set (Merge-Find set) es una estructura en la que el n mero n de eleu mentos es jo, no se pueden borrar ni a adir elementos, y los elementos se organ nizan en una colecci n S ={S1 ,S2 ,. . . ,Sk } de subconjuntos disjuntos o clases de o equivalencia. Cada subconjunto se identica mediante un representante, que debe ser uno de los miembros del subconjunto. En algunas aplicaciones se mantiene la norma de que el representante de cada subconjunto es siempre el elemento menor del subconjunto (asumiendo que puede establecerse una relaci n de orden total entre o los elementos); en otras, no importa el elemento que se elija como representante. En cualquier caso debe cumplirse que siempre que se consulte el valor del representante de un subconjunto, si el subconjunto no ha sido modicado entre consulta y consulta, el resultado sea el mismo.
C
5 7 3 10 12 8

4 1 2 6

9 11

Figura 6.31: Subconjuntos disjuntos o clases de equivalencia en las que se agrupan los elementos del conjunto C. Cada subconjunto se identica mediante alguno de sus miembros: en la gura cada representante aparece en negrita. Sobre un MF-Set unicamente pueden realizarse dos operaciones: Union(x,y) (Merge): Sean Sx el subconjunto que contiene a x, y Sy el subconjunto que contiene a y, se crea un nuevo subconjunto resultado de la uni n de Sx con Sy . El representante del nuevo subconjunto creado es alo guno de sus miembros, aunque normalmente se escoge como representante al elemento que representaba a Sx o al que representaba a Sy . Debido a que 203

en todo momento se debe cumplir que la colecci n de subconjuntos sean o disjuntos, se eliminan los subconjuntos Sx y Sy . Buscar(x) (Find): devuelve el nombre del subconjunto o clase de equivalencia a la que pertenece el elemento x. El nombre del subconjunto equivale al elemento que representa al subconjunto. Este tipo de representaci n se utiliza en numerosas aplicaciones como, por o ejemplo: inferencia de gram ticas, equivalencias de aut matas nitos, c lculo del a o a arbol de expansi n de coste mnimo en un grafo no dirigido, compiladores que o procesan declaraciones (o tipos) de equivalencia, etc.

6.5.1.

Representaci n de MF-sets o

Existen distintas maneras de representaci n para MF-sets, pero nos centraremos o en analizar la m s eciente. a Cada uno de los subconjuntos disjuntos se representa mediante un arbol, donde cada nodo contiene la informaci n de un elemento, y el nodo raz del arbol es el o representante del subconjunto. Para representar cada arbol, utilizaremos un tipo de representaci n denominada representaci n de arboles mediante apuntadores al o o padre. En este tipo de representaci n, para cada nodo del arbol, s lo se mantiene o o un puntero al nodo padre. Adem s, dado un arbol que representa al subconjunto a Si , el nodo que se apunte a s mismo ser el nodo raz del arbol y, por lo tanto, el a representante del subconjunto Si . Por lo tanto, dado que cada subconjunto disjunto se representa mediante un arbol, un MF-set ser una colecci n de arboles, o lo que es lo mismo, un bosque. a o Debido a que el n mero n de elementos del conjunto es jo, podemos identiu car cada elemento mediante un n mero desde 1 hasta n. Este conjunto es f cilu a mente representable en un vector M , en el que en cada posici n i se almacena el o ndice del elemento que es padre del elemento i. Obs rvese que si se cumple que e M [i] = i, el elemento i ser la raz de un arbol y, por lo tanto, el representante de a un subconjunto (ver gura 6.32).

6.5.2.

Operaciones sobre MF-sets

A continuaci n analizaremos c mo pueden llevarse a cabo cada una de las o o operaciones sobre MF-sets utilizando la representaci n indicada en el apartado o anterior. Operaci n Uni n Merge o o Union(x,y): hacer que la raz de un arbol apunte al nodo raz del otro. 204

12

10

11

1 2 3 M 1 1 2

4 5 6 7 8 9 10 11 12 4 4 4 9 10 8 10 9 12

Figura 6.32: Ejemplo de representaci n de MF-Set. Cada uno de los conjuntos diso juntos mostrados en la gura 6.31 se representa mediante un arbol. Cada posici n o i del vector contiene el ndice del elemento que es padre de i. Suponiendo que x e y son races (representantes), la operaci n unicamente o implica modicar el puntero al padre de uno de los representantes, por lo tanto el coste ser O(1) (ver gura 6.33). a

12

10

11

1 2 3 M 1 1 2

4 5 6 7 8 9 10 11 12 10 4 4 9 10 8 10 9 12

Figura 6.33: MF-set resultante tras realizar la uni n de los subconjuntos disjuntos o 5 y 8 que se muestran en la gura 6.32. Obs rvese que la uni n se realiza modie o cando unicamente un valor del vector: el padre del representante de la clase de equivalencia del 5 pasa a ser el representante de la clase de equivalencia del 8. 205

Operaci n Buscar Find o Buscar(x): utilizando el puntero al padre recorrer el arbol desde el nodo x hasta encontrar la raz del arbol. Los nodos visitados en el camino hasta la raz consti tuyen el camino de busqueda. El coste temporal ser proporcional a la profundidad a la que se encuentre el a nodo. El caso peor ocurrir cuando los n elementos est n en un unico conjunto y a e el arbol que lo representa sea una lista enlazada de n nodos; en este caso el coste es O(n).

10

11

1 2 3 M 1 1 2

4 5 6 7 8 9 10 11 12 10 4 4 9 10 8 10 9 12

Figura 6.34: Ejemplo de b squeda del elemento 6: el subconjunto al que pertenece u el elemento nos lo dice el representante, que se corresponde con la raz del arbol. La b squeda consiste en trazar un camino a trav s del arbol, comenzando desde u e el nodo sobre el que se aplica la operaci n Buscar hasta llegar a la raz. o An lisis del coste temporal a Inicialmente, en un MF-Set, existen n subconjuntos disjuntos que contienen, cada uno, un elemento. Esta representaci n indica que, inicialmente, no existen o relaciones de equivalencia entre los elementos del conjunto. Dada esta situaci n inicial, la peor secuencia de operaciones que puede proo ducirse es: a) realizar el m ximo n mero posible de operaciones Uni n: n1, con a u o nico conjunto de n elementos, y b) realizar m operaciones lo que se obtendr un u a Buscar. Dada esta situaci n, analizaremos el coste temporal. o 206

Dado que cada operaci n Uni n tiene un coste temporal O(1), n 1 operao o ciones Uni n tendr n un coste temporal O(n). Debido a que tras realizar todas las o a operaciones de uni n, tendremos un unico arbol que contendr los n elementos, o a el coste temporal de realizar m operaciones Buscar ser O(mn). a El coste obtenido viene determinado por el hecho de que, tal como se realiza la operaci n Uni n, tras realizar k operaciones de Uni n podra obtenerse un arbol o o o de altura k. Es decir, un arbol en el que todos sus nodos formen una lista enlazada. Utilizando dos t cnicas heursticas podemos mejorar el coste temporal de las e operaciones a base de reducir la altura del arbol. Uni n por altura o rango o Estrategia: la uni n de dos conjuntos se realiza de tal forma que la raz del arbol o con menos altura apunta a la raz del arbol con m s altura. De esta forma, la altura a del arbol que se obtiene tras unir un arbol de altura h1 con un arbol de altura h2 , ser max(h1 , h2 ) si h1 = h2 , o bien h1 + 1 si h1 = h2 (ver gura 6.35). a Para llevar a cabo este heurstico ser necesario mantener, para cada nodo ni , a la altura a la que se encuentra el nodo ni . Puede demostrarse que si se realiza la uni n por rango, la altura de un arbol o de n elementos nunca ser superior a log n . Por lo tanto, el coste de realizar la a operaci n Buscar, en el peor caso, utilizando en la operaci n Uni n el heurstico o o o uni n por rango es de O(log n) y, por lo tanto, el coste de realizar m operaciones o de b squeda ser O(m log n). u a

207

10
6 10

4
8

5
9

11

11

(a)

(b) uni n por altura o

Figura 6.35: En ambas guras se muestra un arbol que representa la uni n de o o los subconjuntos 12 y 8 de la gura 6.32. En la gura (a), la uni n de ambos subconjuntos se ha realizado enlazando arbitrariamente una raz de un arbol con la raz del otro, lo que, desaconsejablemente, ha causado que el arbol resultante crezca en altura. En la gura (b), al aplicar el heurstico uni n por rango o altura, o el arbol resultante de la uni n sigue teniendo la misma altura que el arbol de mayor o altura de los que se unieron; por lo tanto se ha evitado que el arbol obtenido tras realizar la uni n crezca en altura. o Compresi n de caminos o Estrategia: Cuando se busca un elemento se aprovecha la b squeda, de tal manu era, que todos los nodos que forman parte del camino de b squeda se enlazan u directamente con la raz del arbol; de esta forma se consigue reducir consider ablemente la altura del arbol (ver gura 6.36). Combinando ambos heursticos, el tiempo requerido para realizar m opera ciones de b squeda, en el peor caso, es O(m(m, n)), donde (m, n) es una u inversa de la funci n de Ackerman, la cual crece muy lentamente. Normalmente, o en cualquier aplicaci n que se utilice un MF-Set, (m, n) 4. o En denitiva, puede decirse que, en la pr ctica, aplicando ambos heursticos, a realizar m operaciones de b squeda tiene un coste temporal casi lineal en m. u

208

10

8
10

11

11

(a)

(b)

Figura 6.36: En la gura (b) se muestra como quedara el arbol de la gura (a), tras realizar sobre el una operaci n b squeda del elemento 11 y aplicar el heurstico o u compresi n de caminos. o

6.6.

Otras Estructuras de Datos para Conjuntos

En la siguiente secci n se pretende ofrecer una visi n muy general de otras o o estructuras de datos cl sicas y de gran inter s pr ctico. a e a Aunque no se realizar una descripci n completa de las estructuras, s que se a o pretende mostrar en cierta manera un peque o compendio de estructuras de datos n para completar la lista de las estructuras vistas a lo largo de este tema. Cada estructura tiene unas aplicaciones especcas, las cuales comentaremos brevemente en cada apartado.

6.6.1.

Tries

Un trie es un tipo de arbol de b squeda. El t rmino trie proviene de abreviar la u e palabra inglesa Retrieval, que se reere a la operaci n de acceder a la informaci n o o guardada en la memoria del computador. Tradicionalmente, los tries se han utilizado para guardar diccionarios de manera que las palabras que tienen prejos comunes (secuencias iniciales de smbolos iguales) utilizan la misma memoria para guardar dichos prejos. Esta estructura ahorra espacio respecto a un almacenamiento simple de las cadenas debido a la compactaci n de los prejos comunes. o

209

Si suponemos un conjunto de palabras formado por las palabras: {pool, prize, preview, prepare, produce, progress}, y las almacenamos en un trie, obtendremos:

Cuando buscamos una palabra determinada, la b squeda comienza en el nodo u raz. Despu s desde el principio al nal de la palabra, se toma caracter a caracter e para determinar el siguiente nodo al que ir. Se elige la arista etiquetada con el mismo caracter. Cada paso de esta b squeda consume un caracter de la palabra y u desciende un nivel en el arbol. Si se agota toda la palabra y se ha alcanzado un nodo hoja, entonces habremos encontrado la informaci n correspondiente a esa o palabra. Si nos quedamos parados en un nodo, tanto porque no existe ninguna arista etiquetada con el caracter actual, tanto porque se ha agotado la palabra en un nodo interno, entonces esto indica que la palabra no es reconocida por el trie. El tiempo necesario para llegar desde el nodo raz al nodo hoja (proceso de b squeda de una palabra) no depende del tama o del trie, sino que es proporcional u n a la longitud de la palabra, lo cual convierte al trie en una estructura de datos muy eciente. Un trie puede entenderse como un tipo de aut mata nito determinista (AFD), o donde cada nodo se corresponde con un estado del AFD y cada arista del arbol se corresponde con una transici n del AFD. o En general un AFD se representa con una matriz de transici n, en la que las o las se corresponden con los estados y las columnas se corresponden con las etiquetas o smbolos para realizar una transici n. En cada posici n de la matriz se o o almacena el siguiente estado al que transicionar para un estado cuando la entrada es equivalente a la etiqueta correspondiente. Esta representaci n mediante o 210

una matriz bidimensional puede resultar muy eciente respecto al coste temporal, pero un poco extra a vista desde el coste espacial, puesto que la mayora de non dos tendr n pocas aristas, dejando la mayora de las posiciones de la matriz vacas. a Existen representaciones m s ecientes espacialmente, pero nos las veremos aqu. a

6.6.2.

Arboles Balanceados

Los arboles balanceados son otras estructuras de datos muy utilizadas para almacenar elementos de manera que se permita una b squeda eciente de estos u una vez construidas las estructuras. Un arbol balanceado es un arbol donde ninguna hoja est m s alejada de la a a raz que cualquier otra hoja. Se pueden denir varios esquemas de balanceo, con lo que se permite una denici n diferente de m s lejos y diferentes algoritmos para mantener el arbol o a balanceado conforme a las operaciones de actualizaci n del arbol. o En la tres siguientes secciones veremos algunos de los tipos de arboles balanceados m s importantes seg n el criterio de balanceo elegido. a u Arboles AVL Un arbol AVL es un arbol binario de b squeda balanceado. Su nombre viene u de sus inventores Adelsson, Velskii y Landis. No est n balanceados perfectamente a como veremos en alg n ejemplo. u Un arbol AVL es un arbol binario de b squeda que cumple las siguientes u propiedades: Las alturas de los sub rboles de cada nodo dieren como mucho en 1. a Cada sub rbol es un arbol AVL. a Como hemos comentado anteriormente, debe tenerse cuidado con esta deni ci n ya que permite arboles aparentemente no balanceados. En la gura 6.37 podeo mos ver algunos ejemplos de arboles AVL. Las operaciones de b squeda de un elemento, inserci n y borrado tienen un u o coste O(log n), siendo n el n mero de elementos. u Arboles 2-3 Un arbol 2-3 se dene como un arbol vaco (cuando tiene 0 nodos) o un nodo simple (cuando tiene un unico nodo) o un arbol con m ltiples nodos que cumplen u las siguientes propiedades:

211

12

11 5

18

17

11

17

12

18

Figura 6.37: Ejemplos de arboles AVL. Cada nodo interior tiene 2 o tres hijos. Cada camino desde la raz a una hoja tiene la misma longitud. Diferenciamos los nodos internos de las hojas creando unos nodos internos con la siguiente informaci n: o p1 Donde cada campo es: p1 : puntero al primer hijo. p2 : puntero al segundo hijo. p3 : puntero al tercer hijo (si existe). k1 : clave m s peque a que sea un descendiente del segundo hijo. a n k2 : clave m s peque a que sea un descendiente del tercer hijo. a n Los nodos hoja tienen solo la informaci n referente a la clave correspondiente. o En la gura 6.38 puede observarse un ejemplo de arbol 2-3. Los valores almacenados en los nodos internos se usan para guiar el proceso de b squeda. Para buscar un elemento con clave x, empezamos en la raz y u suponemos que k1 y k2 son los dos valores almacenados aqu. A partir de aqu re alizamos las siguientes comprobaciones: Si x < k1 , seguir la b squeda por el primer hijo. u k1 p2 k2 p3

212

Figura 6.38: Ejemplo de arbol 2-3. Si x k1 y el nodo tiene solo 2 hijos, seguir la b squeda por el segundo u hijo. Si x k1 y el nodo tiene 3 hijos, seguir la b squeda por el segundo hijo si u x < k2 y por el tercer hijo si x k2 . Aplicar el mismo esquema a cada uno de los nodos que vayan formando parte del camino de b squeda. El proceso acaba cuando se encuentra una hoja con la u clave x (elemento encontrado) o se llega a una hoja con una clave diferente a x (elemento no perteneciente al arbol 2-3). B- rboles a Los B- rboles o arboles B son una generalizaci n de los arboles 2-3. Esta esa o tructura de datos es eciente para almacenamiento externo de datos y suele aplicarse de manera est ndard para la organizaci n de los ndices en sistemas de bases a o de datos. Adem s, proporciona la minimizaci n de los accesos a disco para las a o aplicaciones que manejan bases de datos. Un arbol B de orden n es un arbol de b squeda n-ario con las siguientes u propiedades: La raz es una hoja o tiene por lo menos dos hijos. Cada nodo, excepto el raz y las hojas. tiene entre
n 2

y n hijos.

Cada camino desde la raz a una hoja tiene la misma longitud. Cada nodo interno tiene hasta (n 1) valores de claves y hasta n punteros a sus hijos.

213

Como se ha comentado en el punto anterior, los elementos se almacenan tpicamente repartidos entre los nodos internos y las hojas. Aunque en al gunas variantes de implementaci n s lo se almacenan en las hojas, como o o veremos un poco m s adelante. a Si se almacenan varias claves en una hoja, ser de forma ordenada. a Un B- rbol puede verse como un ndice jer rquico en el cual la raz es el a a primer nivel de indizado. Cada nodo interno es de la forma: p1 donde: pi es un puntero al i- simo hijo, 1 i n. e ki son los valores de las claves, las cuales guardan un orden k1 < k2 < . . . < kn1 de manera que: todas las claves en el sub rbol apuntado por p1 son menores que a k1 . Para 2 i n 1, todas las claves en el sub rbol apuntado por a pi son mayores o iguales que ki1 y menores que ki . Todas las claves en el sub rbol apuntado por pn son mayores o a iguales que kn1 . En la gura 6.39 puede observarse un ejemplo de arbol B. k1 p2 k2 ...... kn1 pn

Figura 6.39: Ejemplo de arbol B. Un arbol B+ es un arbol B en el que se considera que las claves almacenadas en los nodos internos no son utiles como claves (s lo se utilizan para operaciones de o 214

b squeda). Por lo tanto, todas esas claves de los nodos internos est n duplicadas u a en las hojas. Esto tiene la ventaja de que todas las hojas est n enlazadas secuena cialmente y se podra acceder a toda la informaci n de los elementos guardados o en el arbol sin necesidad de visitar nodos internos. En la gura 6.40 se muestra un ejemplo de arbol B+. Esta modicaci n de los B- rboles consigue una mejor utio a lizaci n del espacio en el arbol (a nivel de implementaci n) y mejora la eciencia o o de determinados m todos de b squeda. e u

Figura 6.40: Ejemplo de arbol B+.

215

6.7.
6.7.1.

Ejercicios
Tablas de dispersi n o

Ejercicio 1: -Dadas las siguientes deniciones de tipos, constantes y variables, para declarar y manipular tablas de dispersi n: o #dene NCUBETAS ... typedef struct snodo { char *pal; struct snodo *sig; } nodo; typedef nodo * Tabla[NCUBETAS]; Tabla T1, T2, T3; int ncubetas1, ncubetas2, ncubetas3; Donde se considera que ncubetas1, ncubetas2 y ncubetas3 son variables que indican el n mero de cubetas de T1, T2 y T3, respectivamente. Adem s, u a las siguientes funciones est n disponibles y pueden utilizarse cuando se considere a oportuno: /* Inicializa una tabla vacia */ void crea_Tabla(Tabla T); /* Inserta la palabra pal en la tabla T */ void inserta(Tabla T, char *pal); /* Devuelve un puntero al nodo que almacena la palabra */ /* pal, o NULL si no la encuentra */ nodo *buscar(Tabla T, char *pal); Se pide: a) Escribir una funci n que cree una tabla T 3 con los elementos pertenecientes a o la uni n de los conjuntos almacenados en las tablas T 1 y T 2. Estudia cu l o a es el coste temporal del algoritmo. b) Escribir una funci n que cree una tabla T 3 con los elementos pertenecientes o a la diferencia T 2 - T 1; es decir, aquellos elementos que est n en T 2 y no a est n en T 1. Estudia cu l es el coste temporal del algoritmo. a a

216

Soluci n: o a) Funci n que realiza la uni n de dos tablas hash: o o void union(Tabla T1, Tabla T2, Tabla T3) { int i; nodo *aux; crea_Tabla(T3); /* Metemos en T3 los elementos de T1 */ for (i=0;i<ncubetas1;i++){ aux = T1[i]; while (aux != NULL){ inserta(T3,aux->pal); aux = aux->sig; } } /* Metemos en T3 los elementos de T2 */ for (i=0;i<ncubetas2;i++){ aux = T2[i]; while (aux != NULL){ inserta(T3,aux->pal); aux = aux->sig; } } } El coste del algoritmo es O(n1 + n2 ), siendo n1 el n mero de elementos u almacenados en T1 y n2 el n mero de elementos almacenados en T2. Esto u es debido a que habr que recorrer todos los elementos de T1 e insertarlos a en T3 y recorrer todos los elementos de T2 e insertarlos en T3 igualmente. Como la operaci n de inserci n se considera que tiene un coste constante, o o entonces el coste de esta funci n viene dado por el n mero de elementos o u que habr que insertar en T3, que son los de T1 y T2. a

217

b) Funci n que realiza la diferencia de dos tablas hash: o void diferencia(Tabla T1, Tabla T2, Tabla T3) { int i; nodo *aux; crea_Tabla(T3); for (i=0;i<ncubetas2;i++){ aux = T2[i]; while (aux != NULL){ if (buscar(T1,aux->pal) == NULL) inserta(T3,aux->pal); aux = aux->sig; } } } El coste del algoritmo es O(n2 ), siendo n2 el n mero de elementos almaceu nados en T2. Esto es debido a que habr que recorrer todos los elementos de a T2, para cada uno de ellos buscar si est en T1 y si no lo est , entonces ina a sertarlo en T3. Como las operaciones de b squeda e inserci n se considera u o que tienen un coste constante, entonces el coste de esta funci n vendr dado o a por el n mero de elementos que hay que recorrer en T2. u

218

Ejercicio 2: -Dadas las siguientes deniciones de tipos y constantes para declarar y manipular tablas de dispersi n que almacenan n meros enteros: o u #dene NCUBETAS ... typedef struct snodo { int numero; struct snodo *sig; } nodo; typedef nodo * Tabla[NCUBETAS]; Se pide escribir una funci n: o int minimo(Tabla T, int ncubetas) que obtenga el mnimo de un conjunto de n meros enteros representado como u una tabla de dispersi n. Analiza cu l es el coste temporal del algoritmo. o a

219

Soluci n: o Funci n para calcular el mnimo de una tabla hash de n meros enteros: o u int minimo(Tabla T, int ncubetas) { int i,j; nodo *aux; int min; /* Buscamos el primer elemento que haya en la tabla */ /* y lo ponemos como el minimo. */ i=0; while ((T[i]==NULL) && (i<ncubetas)) i++; min = T[i]->numero; /* Buscamos en el resto de la tabla si hay otro */ /* elemento menor. */ for (j=i;j<ncubetas;j++){ aux = T[j]; while (aux != NULL){ if (aux->numero < min) min = aux->numero; aux = aux->sig; } } return(min); } El coste del algoritmo es O(n), siendo n el n mero de elementos del conjunto, u esto es, la cantidad de enteros que est n almacenados en la tabla de dispersi n. a o Esto es debido a que habr que recorrer todos los elementos para saber cu l es el a a mnimo.

220

6.7.2.

Arboles binarios de busqueda

Ejercicio 3: - Dadas las siguientes deniciones de tipos y variables para representar arboles binarios de b squeda: u typedef ... tipo_baseT; typedef struct snodo { tipo_baseT clave; struct snodo *hizq, *hder; } abb; abb *T; Se pide: a) Escribe una versi n recursiva del algoritmo visto en clase de teora que obtiene o el m ximo de un arbol binario de b squeda. a u abb *maximo(abb *T) b) Escribe un algoritmo recursivo que, dado un arbol binario indique si es de b squeda o no. u int es_abb(abb *T)

Soluci n: o a) El esquema para la funci n que obtiene el m ximo de un arbol binario de o a b squeda de manera recursiva es sencillo. Para buscar el m ximo siempre u a debamos buscar por el hijo derecho del nodo actual en el caso de que tu viera, si no tena hijo derecho es porque el era el m ximo. El esquema re a cursivo consistir en si un nodo tiene hijo derecho, el problema se reduce a a buscar el m ximo de su sub rbol derecho, en el caso de que no tenga hijo a a derecho, el es el m ximo: a abb *maximo(abb *T) { abb *max=NULL; if (T!=NULL) if (T->hder!=NULL) max = maximo(T->hder); else max = T; return(max); } 221

b) La funci n debe devolver 0 (falso) si el arbol binario no es de b squeda y 1 o u (verdadero) en caso contrario. Para comprobar si un arbol binario es de b queda podemos comprobar si para cada nodo del arbol, se cumple la u propiedad de arbol binario de b squeda a nivel local, esto es, si para el u nodo actual que estemos consultando, se cumple que su hijo izquierdo tiene una clave menor que la suya y su hijo derecho tiene una clave mayor que la suya, entonces solo quedar por comprobar si su sub rbol izquierdo es o a a no un arbol binario de b squeda y si su sub rbol derecho es o no un arbol u a binario de b squeda. Aqu es donde tenemos el esquema recursivo del probu lema. Si nos jamos, veremos que realmente lo que hay que hacer es un recorrido del arbol, donde la acci n a realizar para cada nodo es comprobar si su clave o es mayor que la de su hijo izquierdo y menor que la de su hijo derecho. La funci n que indica si un arbol binario es de b squeda o no es la siguiente: o u int es_abb(abb *T) { int es=1; /* Comprobamos si el hijo izq cumple condicion */ /* de abb y si el subarbol izq es abb. */ if (T->hizq!=NULL) { if (T->clave < T->hizq->clave) return(0); else es = es_abb(T->hizq); } if (es) /* Comprobamos si el hijo der cumple */ /* condicion de abb y si el subarbol der */ /* es abb. */ if (T->hder!=NULL) { if (T->clave > T->hder->clave) return(0); else es = es_abb(T->hder); } return(es); }

222

Ejercicio 4: -Sea A una arbol binario de b squeda con n elementos y todos sus niveles comu pletos. Por ejemplo, sea el arbol de enteros, con n = 7 de esta gura:
25

10

40

18

30

61

Escribir un algoritmo Divide y Vencer s en lenguaje C, a int select_kmenor(arbol *A, int k, int n) que encuentre con un coste O(log n) el elemento k- simo en la secuencia ore denada de los elementos de A sin ordenar dichos elementos (esto es, debe buscar el k- simo menor elemento), con 1 k n. Por ejemplo, en el arbol de la gura e anterior, si k = 5, debe devolverse el elemento 30. NOTA: Para intentar hallar la funci n correspondiente que resuelva este problema, o hay que pensar c mo quedaran los elementos del arbol si estuvieran ordenados o en un vector. Como los arboles binarios especicados deben cumplir que son completos y por la propiedad de ser arbol binario de b squeda, el elemento que estuviera almau cenado en la raz del arbol quedara justo en la posici n central de la secuencia de o elementos ya ordenados. Adem s esa posici n sera la posici n n/2 . Adem s, a o o a todos los elementos de su sub rbol izquierdo estar n antes que la posici n n/2 a a o de la secuencia ordenada y todos los elementos de su sub rbol derecho estar n en a a las posiciones posteriores a n/2 . As pues, como estamos buscando el elemento que ocupe la posici n k en la secuencia nal, si k es menor que n/2 entonces o el elemento a buscar estar en el sub rbol izquierdo del raz y podemos centrar la a a b squeda en ese sub rbol, pero si k es mayor que n/2 entonces el elemento a u a buscar estar en el sub rbol derecho del raz y debemos centrar la b squeda en ese a a u sub rbol; en el caso en que k = n/2 entonces ya hemos encontrado el elemento a buscado. Cuando comencemos la b squeda en el sub rbol correspondiente (el izquierdo u a o el derecho) debemos aplicar la misma estrategia, por ello, podemos escribir la funci n siguiendo un esquema recursivo o iterativo, seg n se desee. o u OJO!: crees que habr que cambiar la k seg n busquemos por el sub rbol a u a izquierdo o el derecho? 223

Soluci n: o Los par metros de la funci n ser n el arbol binario de b squeda, la posici n a o a u o k que buscamos y el n mero de nodos que forman el arbol binario de b squeda u u actual en el que estamos buscando. Un aspecto a tener en cuenta es que si buscamos por el sub rbol derecho ena tonces la k debe cambiar, esto es debido a que ahora la posici n que buscamos o dentro de esa subsecuencia no se corresponde con la posici n k puesto que el o primer elemento de la secuencia ordenada de los elementos del sub rbol derecho a no est en la posici n 0. a o Soluci n recursiva: o int select_kmenor(arbol *A, int k, int n) { int n_subarb; n_subarb = (n-1)/2; if (k <= n_subarb) return(select_kmenor(A->hizq, k, n_subarb)); else if (k > n_subarb + 1) return(select_kmenor(A->hder,k-(n_subarb+1),n_subarb)); else return(A->clave); }

224

Soluci n iterativa: o int select_kmenor(arbol *A, int k, int n) { int n_subarb; arbol *aux; aux = A; do { n_subarb = (n-1)/2; if (k <= n_subarb) aux = aux->hizq; else if (k > n_subarb + 1){ aux = aux->hder; k = k - (n_subarb + 1); } n = n_subarb; }while( k != n + 1 ); return(aux->clave); }

225

Ejercicio 5: -Dado un arbol binario de b squeda abb *T y la operaci n de inserci n de un u o o nuevo nodo vista en clase. Se pide reimplementar dicha operaci n de manera reo cursiva. Una idea es basarse en la implementaci n recursiva de la b squeda en un abb o u y en la estrategia para realizar la inserci n en un abb. o Soluci n: o abb *abb_insertar(abb *T, abb *nodo) { if (T==NULL) return(nodo); else { if (nodo->clave < T->clave) T->hizq=abb_insertar(T->hizq,nodo); else T->hder=abb_insertar(T->hder,nodo); return(T); } }

226

6.7.3.

Montculos (Heaps)

Ejercicio 6: -Escribe un algoritmo recursivo que, dada una clave x, obtenga, de la forma m s a eciente posible, la posici n que ocupa en un montculo (la posici n del veco o tor que representa el montculo). Si la clave no se encuentra, el algoritmo debe devolver el valor -1. Suponer que existe una variable global talla_Heap que indica el n mero de nodos que forman el Heap. u Cu l sera el coste temporal del algoritmo en el peor caso? a Soluci n: o Puede aparentar que en un montculo no exista ninguna propiedad que pue da permitirnos facilitar la b squeda de un elemento, por lo que tendramos que u aplicar un recorrido tradicional de arboles (preorden, postorden o inorden) para llevar a cabo la b squeda. Sin embargo, s que podemos optimizar un poco el u proceso de b squeda utilizando la propiedad de montculo que dice que para un u determinado nodo la clave de este ha de ser mayor o igual que la de sus dos posibles hijos. La idea es que si llegamos a un nodo cuya clave es menor que la clave x que estamos buscando, entonces no seguiremos buscando por sus sub rboles, a ya que estos contendr n nodos cuyas claves es segura que van a ser menores que a x y por tanto no podr n ser iguales. Estaremos evitando as el tener que hacer una a exploraci n completa del arbol. o La funci n que realiza la b squeda es: o u

227

int busca(int *M, int act, int x) { int pos; if (M[act] == x) return(act); else if (M[act] < x) return(-1); else { pos = -1 /* Si tiene hijo izquierdo, buscamos */ /* por subarbol izquierdo. */ if ((2*act)<=talla_Heap) pos = busca(M,2*act,x); /* Si tiene hijo derecho, buscamos */ /* por subarbol derecho. */ if ((pos == -1) && (((2*act)+1)<=talla_Heap)) pos = busca(M,(2*act)+1,x); return(pos); } } La llamada inicial sera pos=busca(M,n,1,x). El peor caso ser cuando estemos buscando una clave x que sea menor que a todas las claves que est n almacenadas en el montculo y adem s no se encuene a tre en el. En este caso habr que explorar todas las ramas del montculo puesto a que nunca se cumplir la condici n de que x sea menor que ninguna clave del a o montculo, adem s no podremos saber que x no est en el montculo hasta que no a a hayamos recorrido todos los nodos. As pues, para el peor caso el coste ser O(n), a siendo n el n mero de nodos del montculo. u

228

Ejercicio 7: -Realiza una traza de la llamada heapify(M,3) para el vector:


1 2 3 4 5 6 7 8 9 10 11 12 13 14

27 17

16 13 10

12

Soluci n: o El nodo 3 no cumple la propiedad de montculo:


1

27

17

16

13

hizq

10

hder

10

11

12

13

14

12

10

11

12

13

14

27 17 3

16 13 10

1 5

7 12 4

9 0
talla_Heap

hizq hder

El nodo de clave mayor es el hijo izquierdo de i, se intercambian las claves y se hace una llamada recursiva a heapify(M,6):
1

27

17

16

13

hizq

10

hder

10

11

12

13

14

12

10

11

12

13

14

27 17 3 16 13 10
i

1 5

7 12 4

9 0
talla_Heap

hizq hder

229

El nodo 6 no cumple la propiedad de montculo:


1

27

17

10

16

13

3 i

10

11

12

13

14

12

4
hizq

8
hder
11 12

10

13

14

27 17 10 16 13 3
i

7 12 4

9 0
talla_Heap

hizq hder

El nodo de clave mayor es el hijo derecho de i, se intercambian las claves y se hace una llamada recursiva a heapify(M,13):
1

27

17

10

16

13

3 i

10

11

12

13

14

12

4
hizq

8
hder
11 12

10

13

14

27 17 10 16 13 3
i

7 12 4

9 0
talla_Heap

hizq hder

El nodo 13 cumple la propiedad de montculo, termina la llamada a heapify(M,13), termina la llamada a heapify(M,6) y por ultimo termina la llamada a heapify(M,3). Obs rvese que ahora el sub rbol que comienza en el noe a do 3 s es un montculo:

230

27

17

10

16

13

10

11

12

13

14

12

10

11

12

13

14

27 17 10 16 13 9

7 12 4

3 0
talla_Heap i

Ejercicio 8: -Cu l es el resultado de realizar una llamada a la funci n heapify(M,i) para a o i > talla_Heap/2? Y si i < (talla_Heap/2)+1 y la clave del nodo i es mayor que la de sus hijos? Soluci n: o Para el caso en que i > talla_Heap/2 la funci n heapify no hace nada o puesto que detecta que el nodo i es una hoja y por tanto no puede incumplir la propiedad de montculo. Si i < (talla_Heap/2)+1 y la clave del nodo i es mayor que la de sus hijos la funci n heapify tambi n deja inalterado el montculo puesto que detecta o e que el nodo i, a pesar de no ser una hoja, s que cumple la propiedad de montculo.

231

6.7.4.

MF-sets

Ejercicio 9: -Dado el siguiente MF-set:


1 2 3 4 5 6 7 8 9 10

M Se pide:

a) Dibuja cada uno de los arboles que representan los subconjuntos disjuntos que contiene el MF-set. b) Muestra c mo se van transformando los arboles, despu s de ejecutar cada una o e de las operaciones de la secuencia indicada. Utiliza los heursticos uni n o por rango y compresi n de caminos. En la uni n, en caso de que la altura o o de los arboles a unir sea id ntica, la raz del primer arbol debe pasar a ser e hijo de la raz del segundo arbol. 1. Union(3,6) 2. Buscar(4) 3. Union(1,6) 4. Buscar(10) Soluci n: o a) El MF-set se representa de esta forma:

10

7
1 2 3 4 5

9
6 7 8 9 10

1 b) 1. Union(3,6)

232

10

7
1 2 3 4 5

9
6

4
7 8 9 10

1 2. Buscar(4)

10

7
1 2 3 4

9
5 6 7 8 9 10

1 3. Union(1,6)

7
1 2 3 4

9
5

3
6 7

10
8 9 10

1 4. Buscar(10)

10

7
1 2 3 4

9
5 6

3
7

4
8 9 10

6 233

Tema 7 Grafos
7.1. Deniciones

Los grafos son modelos que permiten representar relaciones entre elementos de un conjunto. A continuaci n veremos una serie de deniciones b sicas: o a Un grafo es un par (V ,E), donde V es un conjunto de elementos denominados v rtices o nodos, sobre los que se ha denido una relaci n; y E es un conjunto e o de pares (u,v), u,v V , denominados aristas o arcos, que indica que u est relaa cionado con v de acuerdo a la relaci n denida sobre V . o Un grafo dirigido es aqu l en el que la relaci n denida sobre V no es sim trie o e ca. Cada arista en E es un par ordenado de v rtices (u,v); es decir, la arista (u,v) e y la arista (v,u) son aristas distintas. Un grafo no dirigido es aqu l en el que la relaci n denida sobre V s es e o sim trica. Cada arista en E es un par no ordenado de v rtices {u,v} donde u,v e e V y u=v; es decir, la arista (u,v) y (v,u) son la misma arista, y no puede existir una arista de un v rtice sobre s mismo. e
1 2 3
1 2 3

(a) Grafo dirigido G(V , E). V = {1,2,3,4,5,6} E = {(1,2),(1,4),(2,5),(3,5),(3,6),(4,2),(5,4),(6,6)}

(b) Grafo no dirigido G(V , E). V = {1,2,3,4,5} E = {{1,2},{1,5},{2,3},{2,4},{2,5},{3,4},{4,5}}

Figura 7.1: Ejemplo de grafos.

234

Un camino desde un v rtice u V a un v rtice v V , es una secuencia de e e v rtices v1 , v2 , . . . , vk tal que u = v1 , v = vk , y (vi1 ,vi ) E, para i = 2,. . . ,k. e
1 2 3

Figura 7.2: Ejemplo de camino desde el v rtice 3 al v rtice 2: <3,5,4,2>. e e La longitud de un camino es el n mero de arcos del camino. El camino que u se muestra en la gura 7.2 es de longitud 3. Un camino simple es un camino en el que todos sus v rtices, excepto, tal vez, e el primero y el ultimo, son distintos. El camino que se muestra en la gura 7.2 es un camino simple; sin embargo, el camino <3,5,4,2,5> no es simple. Un ciclo es un camino simple v1 , v2 , . . . , vk tal que v1 = vk .
1 2 3

Figura 7.3: El camino <2,5,4,2> es un ciclo de longitud 3. Un bucle es un ciclo de longitud 1.


1 2 3

Figura 7.4: Ejemplo de bucle. Un grafo acclico es un grafo sin ciclos.


1 2 3

Figura 7.5: Ejemplo de grafo acclico. 235

Se dice que un nodo v es adyacente al nodo u si existe una arista (u,v) E que va desde u hasta v. Por ejemplo, en el grafo que se muestra en la gura 7.5, los v rtices 2 y 4 son adyacentes al v rtice 1. e e En un grafo no dirigido, se dice que un arco (u,v) E incide en los nodos u, v; mientras que en un grafo dirigido un arco (u,v) E incide en el nodo v, y parte del nodo u. Por ejemplo, en el grafo dirigido de la gura 7.1, la arista (1,2) incide en el v rtice 2 y parte del v rtice 1, mientras que en el grafo no dirigido e e que se muestra en la misma gura, la arista {1,2} incide en ambos v rtices: 1 y 2. e Se denomina grado de un nodo al n mero de arcos que inciden en el. En los u grafos dirigidos se puede distinguir entre grado de salida de un nodo (n mero de u arcos que parten de el), y grado de entrada (n mero de arcos que inciden en el). u En este caso, el grado del v rtice ser la suma de los grados de entrada y de salida. e a El grado de un grafo es el m ximo grado de sus v rtices . Por ejemplo, el grafo a e e dirigido de la gura 7.1 es de grado 3, y el v rtice 2 como tiene grado de salida 1 y grado de entrada 2, es de grado 3. Un grafo G = (V , E ) es un subgrafo de G = (V , E) si V V y E E. Un subgrafo inducido por V V es un subgrafo G = (V ,E ) tal que E = {(u,v) E | u,v V }.

4
(a)

4
(b)

Figura 7.6: Ejemplo de subgrafos del grafo dirigido de la gura 7.1: en la gura (a) se muestra un subgrafo G = (V ,E ), tal que V = {1,2,4,5} y E = {(1,2),(1,4),(2,5),(5,4)}; en la gura (b) se muestra el subgrafo G = (V ,E ) inducido por V = {1,2,4,5} Se dice que un v rtice v es alcanzable desde un v rtice u, si existe un camino e e de u a v. Un grafo no dirigido es conexo si existe un camino desde cualquier v rtice e a cualquier otro. Un grafo dirigido con esta propiedad se denomina fuertemente conexo. Si un grafo dirigido no es fuertemente conexo, pero el grafo subyacente (sin sentido en los arcos) es conexo, se dice que el grafo es d bilmente conexo. e El grafo no dirigido de la gura 7.1 es conexo; mientras que el grafo dirigido de la

236

misma gura es d bilmente conexo. A continuaci n se muestra un grafo dirigido e o fuertemente conexo:
1 2

Figura 7.7: Ejemplo de grafo fuertemente conexo. En un grafo no dirigido, las componentes conexas son las clases de equivalencia de v rtices seg n la relaci n ser alcanzable desde. Por lo tanto, todos los e u o v rtices que formen parte de una componente conexa ser n alcanzables entre s. e a Un grafo no dirigido es no conexo si est formado por varias componentes a conexas. Por lo tanto, un grafo no dirigido es conexo, si tiene una unica compo nente conexa. El grafo no dirigido que se muestra en la gura 7.1 tiene una unica componente conexa ya que todos los v rtices son alcanzables entre s; por lo tanto e es conexo. En un grafo dirigido, las componentes fuertemente conexas, son las clases de equivalencia de v rtices seg n la relaci n ser mutuamente alcanzable. Por e u o lo tanto, para todo par de v rtices u,v que pertenezcan a la misma componente e fuertemente conexa, existir un camino desde u hasta v y otro desde v hasta u. a Un grafo dirigido es no fuertemente conexo si est formado por varias coma ponentes fuertemente conexas. Por lo tanto, un grafo dirigido es fuertemente conexo si tiene una unica componente fuertemente conexa. El grafo que se muestra en la gura 7.7 tiene una unica componente fuertemente conexa por lo que es fuertemente conexo. El grafo dirigido que se muestra en la gura 7.1 tiene cuatro componentes fuertemente conexas, como se representa en la gura7.8, por lo que es un grafo dirigido no fuertemente conexo.
1 2 3

Figura 7.8: Componentes fuertemente conexas del grafo dirigido de la gura 7.1. Un grafo ponderado o etiquetado es aquel grafo en el que cada arco, o cada v rtice, o los dos, tienen asociada una etiqueta, que puede ser un nombre, un peso, e etc. 237

1
8

10 12 7 9

2
1

3
15

Figura 7.9: Ejemplo de grafo ponderado: cada arista tiene asociada un peso.

7.2.

Introducci n a la teora de grafos o

Por qu trabajar con grafos? Existen una multitud de problemas que pueden e reducirse a una representaci n basada en grafos, a partir de esta representaci n el o o problema se hace abordable desde un punto de vista formal y es susceptible de ser resuelto mediante un algoritmo y por tanto tratable mediante un computador. Vamos a ver un problema cl sico que motiv el nacimiento de la teora de grafos: a o el problema de los puentes de K nigsberg. o En siglos pasados, K nigsberg fue una rica ciudad de la zona oriental de Pruo sia; hoy da su nombre es Kaliningrad y pertenece a Rusia. Se encuentra a orillas del mar B ltico y a unos 50 Km de la frontera con Polonia. K nigsberg es cruzada a o por un ro, el Pregel, que forma una isla en el centro del ro. En el siglo XVIII, el ro estaba atravesado por siete puentes (ya no, pues la ciudad fue parcialmente destruida durante la segunda Guerra Mundial), situados como en la gura 7.10, que pueden verse resaltados en color en la gura 7.11. y que permitan enlazar distintos barrios. K nigsberg fue la ciudad natal de Kant, famoso l sofo alem n. o o a La disposici n topogr ca de K nigsberg di lugar, precisamente en la epoca de o a o o Kant a un juego que concentr la atenci n de los matem ticos del momento. El o o a juego consiste en lo siguiente: como es habitual en los pueblos alemanes, tambi n e en K nigsberg sus habitantes solan pasear los domingos por las calles, pero era o posible planicar tal paseo de forma que saliendo de casa se pudiera regresar a ella, tras haber atravesado cada uno de los puentes una vez, pero s lo una? o

238

Figura 7.10: Mapa de la ciudad de K nigsberg en tiempos de Euler. o

Figura 7.11: Mapa de la ciudad de K nigsberg con el ro y los puentes coloreados. o La resoluci n del problema puede obtenerse mediante fuerza bruta, probano do todas las combinaciones posibles de paseos, pero fue Leonhard Euler (17071783), matem tico suizo quien analiz el problema y le di una soluci n formal. a o o o Tras prolongados estudios, Euler dio una respuesta segura: no es posible planicar el recorrido para lograr atravesar cada uno de los puentes y una sola vez! Sus investigaciones sentaron las bases de una nueva rama de las matem ticas y de la a 239

geometra que recibe el nombre de teora de grafos. Desde entonces, esta teora ha tenido aplicaci n pr ctica no s lo en las matem ticas, sino tambi n en otras o a o a e ramas. Ya en el siglo XIX los grafos se emplearon en la teora de los circuitos el ctricos y en las teoras de los diagramas moleculares. Actualmente, adem s de e a constituir un instrumento de an lisis en las matem ticas puras, la teora de grafos a a se emplea para solucionar m ltiples problemas de orden pr ctico: de transporte, u a por ejemplo, y, en general, en problemas de programaci n. o Para resolver el problema, Euler se comport como un cientco moderno: o trat de traducir el problema a una f rmula m s general y simplicada. Para ello o o a traz sobre un papel un esquema de K nigsberg semejante al de la gura 7.12, o o que posteriormente tradujo a un esquema todava m s simplicado como el de la a gura 7.13.

Figura 7.12: Mapa esquem tico de la ciudad de K nigsberg. a o


A C

Figura 7.13: Grafo que representa la ciudad de K nigsberg y sus puentes. o Represent las islas y las orillas del ro con puntos y transform los puentes en o o otras tantas lneas que enlazaban los diferentes puntos. As el problema quedaba reducido a este otro: es posible, partiendo de un punto cualquiera A, B, C, D, dibujar la gura volviendo siempre al mismo punto de partida, sin repasar ninguna lnea ya trazada y sin levantar el l piz del papel? a Para lograr una comprensi n clara de la soluci n propuesta por Euler, tenemos o o que aclarar un nuevo concepto: el de la practicabilidad. Un grafo es practicable (en el sentido de un camino) cuando se pasa una sola vez por cada arista, mientras que por los v rtices se puede pasar cuantas veces sea necesario. Corresponde a Euler e el m rito de haber descubierto las siguientes reglas: e

240

1. Si una grafo est compuesto de v rtices solo de grado par, se puede ena e tonces recorrer en una sola pasada, partiendo de un determinado v rtice y e regresando al mismo. 2. Si un grafo contiene s lo dos v rtices de grado impar, tambi n puede recoro e e rerse en una sola pasada, pero sin volver al punto de partida. 3. Si un grafo contiene un n mero de v rtices de grado impar superior a 2, u e entonces el problema no tiene soluci n: no se puede recorrer en una sola o pasada. Volvemos ahora sobre nuestro problema de los puentes de K nigsberg y analo izamos cual es el grado de cada v rtice: e VERTICE A B C D GRADO 3 3 5 3

Hay 4 v rtices de grado impar y ninguno de grado par, por consiguiente, el e problema no tiene soluci n! o

7.3.

Representaci n de grafos o

Tpicamente un grafo G = (V , E) suele representarse utilizando dos posibles representaciones: representaci n mediante listas de adyacencia o representaci n o o mediante una matriz de adyacencia. En ambas representaciones, para poder referenciar los v rtices de un grafo, a e cada uno de ellos, se le asigna un n mero que lo identica. u

7.3.1.

Listas de adyacencia

Esta es la representaci n m s escogida a la hora de representar un grafo. o a Un grafo G = (V , E) se representa mediante un vector de tama o igual al n n mero de v rtices que contiene el grafo |V |, de tal forma, que cada posici n i del u e o vector, contiene un puntero a una lista enlazada de elementos, denominada lista de adyacencia; cada elemento de la lista de adyacencia representa a cada uno de los v rtices adyacentes al v rtice i; es decir, todos los v rtices v tales que existe e e e un arco (i,v) E. Los v rtices en cada lista de adyacencia son, normalmente, almacenados en e un orden arbitrario. 241

1 2

2 1 2 2 1

5 5 4 5 2 3 4 3 4

2 3

3 4 5

1 2 3

2 5 5 2 4 6

4 5

Figura 7.14: Ejemplo de representaci n de un grafo no dirigido, y de otro dirigido, o mediante listas de adyacencia. Si G = (V , E) es un grafo dirigido, la suma de las longitudes de todas las listas de adyacencia ser |E|; ya que cada arista del grafo (u,v) E implica tener a un elemento en una lista de adyacencia (v en la lista de adyacencia de u). Si G es un grafo no dirigido, la suma de las longitudes de todas las listas de adyacencia ser 2|E|; ya que cada arista del grafo (u,v) E implica tener dos elementos a en dos listas de adyacencia (u en la lista de adyacencia de v, y v en la lista de adyacencia de u). Por lo tanto, el tama o m ximo de memoria requerido (coste espacial) para n a representar un grafo, sea dirigido o no lo sea, es O(|V | + |E|); donde |V | es el n mero de v rtices del grafo (tama o del vector), y |E| es el n mero de aristas del u e n u grafo. Esta representaci n ser apropiada para grafos en los que |E| sea considero a ablemente menor que |V |2 . Esta representaci n es f cilmente extensible a su utilizaci n con grafos pono a o derados, es decir, grafos en los que cada arista tiene asociada un peso. El peso w de una arista (u,v) E se puede almacenar f cilmente en el elemento que representa a a v en la lista de adyacencia de u. Una potencial desventaja de la representaci n mediante listas de adyacencia o es, que si se quiere comprobar si una arista (u,v) E es necesario buscar el v rtice e v en la lista de adyacencia de u. El coste de esta operaci n ser O(Grado(G)) o a O(|V |).

242

1 2 3
1
8 10 12 7 9

2 10 5 7 5 1 2 12 4 9 6 9

4 8

6 15

2
1

3
15

4 5
9

Figura 7.15: Ejemplo de representaci n de un grafo ponderado mediante listas de o adyacencia. Un posible denici n de tipos en C para esta representaci n (considerando o o que el grafo puede ser ponderado) sera: #dene MAXVERT ... typedef struct vertice { int nodo, peso; struct vertice *sig; } vert_ady; typedef struct { int talla; vert_ady *ady[MAXVERT]; } grafo;

7.3.2.

Matriz de adyacencia

Una posible soluci n para evitar el inconveniente que presentan las listas de o adyacencia es utilizar una matriz de adyacencia para la representaci n del grafo, o asumiendo, eso s, la necesidad de disponer de m s memoria para este tipo de a representaci n. o La representaci n de un grafo G = (V , E) mediante una matriz de adyacencia, o consiste en tener una matriz A de dimensiones |V | |V |, tal que, para cada valor aij de la matriz:

243

aij =

1 0

si (i, j) E en cualquier otro caso


1 0 1 0 0 1 2 1 0 1 1 1 3 0 1 0 1 0 4 0 1 1 0 1 5 1 1 0 1 0

2 3

1 2 3 4 5
1 0 0 0 0 0 0

1 2 3 4 5 6

2 1 0 0 1 0 0

3 0 0 0 0 0 0

4 1 0 0 0 1 0

5 0 1 1 0 0 0

6 0 0 1 0 0 1

Figura 7.16: Ejemplo de representaci n de un grafo no dirigido, y de otro dirigido, o mediante una matriz de adyacencia. Independientemente del n mero de arcos del grafo, el coste espacial es O(|V |2 ). u En el caso de que el grafo fuese ponderado, para cada arco (i,j) E, su peso se almacena en la posici n (i,j) de la matriz (ver gura 7.17): o aij = w(i, j) 0o si (i, j) E en cualquier otro caso

1
8

10 12 7 9

2
1

3
15

1 2 3 4 5 6

1 0 0 0 0 0 0

2 10 0 0 12 0 0

3 0 0 0 0 0 0

4 8

5 0

0 7 0 1 15 0 0 0 9 0 0 0 0 9

6 0 0

Figura 7.17: Ejemplo de representaci n de un grafo ponderado mediante una mao triz de adyacencia. 244

Este tipo de representaci n es util cuando los grafos tienen un n mero de o u v rtices razonablemente peque o, o cuando se trata de grafos densos, es decir, e n donde el n mero de aristas |E| es cercano a las dimensiones de la matriz |V ||V |. u En este tipo de representaci n se puede comprobar si una arista (u,v) E con o un coste temporal O(1), con tan solo consultar la posici n A[u][v] de la matriz. o Un posible denici n de tipos en C para esta representaci n sera: o o #dene MAXVERT ... typedef struct { int talla; int A[MAXVERT][MAXVERT]; } grafo;

7.4.

Recorrido de grafos

Para resolver con eciencia muchos problemas relacionados con grafos, es necesario recorrer los v rtices y los arcos del grafo de manera sistem tica. Exise a ten dos formas tpicas de recorrer un grafo: recorrido primero en profundidad y recorrido primero en anchura.

7.4.1.

Recorrido primero en profundidad

El recorrido primero en profundidad de un grafo, es una generalizaci n del o recorrido en orden previo de un arbol. Iniciando en alg n v rtice v el recorrido, u e se procesa v y, a continuaci n, recursivamente, se recorren en profundidad todos o los v rtices adyacentes a el que queden por recorrer. Esta t cnica se conoce como e e recorrido primero en profundidad porque recorre el grafo en la direcci n hacia o adelante (m s profunda) mientras sea posible. a La estrategia consiste en ir recorriendo el grafo, partiendo de un v rtice detere minado v, de forma que cuando se visita un nuevo v rtice, se exploran cada uno e de los caminos que parten de ese v rtice, de tal manera que hasta que no se ha e nalizado de explorar uno de los caminos no comienza a explorarse el siguiente. Es importante resaltar que un camino deja de explorarse en el momento que lleva a un v rtice que ya ha sido visitado en el recorrido con anterioridad. Una vez que e se han visitado todos los v rtices alcanzables desde v, el recorrido del grafo puede e quedar incompleto si existan v rtices en el grafo que no eran alcanzables desde e v; en este caso, se selecciona alguno de ellos como nuevo v rtice de partida, y se e repite el mismo proceso hasta que todos los v rtices del grafo han sido visitados. e

245

Dado un grafo G = (V , E), el funcionamiento del algoritmo se ajusta al siguiente esquema recursivo: 1. Inicialmente se marcan todos los v rtices del grafo G como no visitados; e 2. Se escoge un v rtice u V como punto de partida; e 3. u se marca como visitado; 4. para cada v rtice v adyacente a u, (u,v) E, si v no ha sido visitado, se e repiten recursivamente los pasos (3) y (4) para el v rtice v. Este proceso e naliza cuando se visitan todos los nodos alcanzables desde el v rtice ese cogido como de partida en el paso 2. Debido a que el recorrido del grafo puede quedar incompleto si desde el v rtice de partida no fueran alcanze ables todos los nodos del grafo, se vuelve al paso (2) escogiendo un nuevo v rtice v, que no haya sido visitado, como de partida, y se repite el mismo e proceso hasta que se han recorrido todos los v rtices del grafo. e A continuaci n se presenta el algoritmo (en pseudo-c digo) que, dado un grafo o o G = (V , E), realiza un recorrido del grafo G siguiendo la estrategia primero en profundidad. El algoritmo utiliza un vector denominado color de talla |V | para indicar si un v rtice u V ha sido visitado (color[u]=AMARILLO) o no e (color[u]=BLANCO). Algoritmo Recorrido en profundidad(G){ para cada v rtice u V e color[u] = BLANCO n para para cada v rtice u V e si (color[u] = BLANCO) Visita nodo(u) n para } Algoritmo Visita nodo(u){ color[u] = AMARILLO para cada v rtice v V adyacente a u e si (color[v] = BLANCO) Visita nodo(v) n para }

246

El algoritmo Recorrido en profundidad comienza marcando todos los v rtices e como no visitado (u V color[u]=BLANCO). A continuaci n, para cada v rtice o e u que quede sin visitar (color[u]=BLANCO), se comienza el recorrido llamando a la funci n Visita nodo(u). Inicialmente, como todos los v rtices est n sin visitar, o e a se iniciar el recorrido arbitrariamente por uno de ellos. Hay que tener en cuenta a que cuando se nalice el recorrido iniciado desde un v rtice u, se habr n visitado e a todos los nodos que eran alcanzables desde u, por lo que la siguiente llamada a a e Visita nodo desde Recorrido en profundidad se realizar sobre un v rtice que no era alcanzable desde u (si existiera alguno en el grafo). e Cada llamada a la funci n Visita nodo se realiza sobre un v rtice no visitado. o Debido a que lo primero que realiza la funci n es marcar el v rtice u, sobre el o e que se aplica, como visitado (color[u]=AMARILLO), nunca m s se volver a a a o aplicar la funci n Visita nodo sobre el nodo u. A continuaci n, se recorren en o profundidad todos los v rtices adyacentes a u que no han sido visitados llamando e recursivamente a la funci n Visita nodo. o Ejemplo de funcionamiento del algoritmo En la gura 7.18 se muestra un ejemplo del funcionamiento del algoritmo para el siguiente grafo:

1 2
2 5 7 1 4 6

4 5 5 6 7 7

3 4 5 6

Obs rvese en el ejemplo que el recorrido a trav s del grafo depende del orden e e en que aparecen en las listas de adyacencia los v rtices. Es decir, cuando se aplica e la funci n Visita nodo sobre un v rtice u, la misma funci n se aplica recursivao e o mente sobre los v rtices adyacentes a u no visitados siguiendo el orden en que e aparecen en la lista de adyacencia de u.

247

5 7
u

5 7
u

5 7

(a) Recorrido en profundidad(G)


2 5 7
u

(b) Visita nodo(1)

(c) color[1]=AMARILLO

5 7

5 7
u

(d) Visita nodo(4)


2 5 7
u

(e) color[4]=AMARILLO
2 5

(f) Visita nodo(6)


2 5

7 1 4 6 1 4 6

(g) color[6]=AMARILLO
2 5 7 1 4 6

(h) Visita nodo(7)


2 5 7 1 4 6

(i) color[7]=AMARILLO
u

5 7

(j) Visita nodo(3)


u

(k) color[3]=AMARILLO (l) Visita nodo(5)


u u

5 7

5 7

5 7

(m) color[5]=AMARILLO

(n) Visita nodo(2) ( ) color[2]=AMARILLO n

Figura 7.18: Ejemplo de c mo se recorre un grafo siguiendo la estrategia primero o en profundidad. 248

Coste temporal del algoritmo Para evaluar el coste temporal del algoritmo consideraremos que el grafo G = (V , E) se representa mediante listas de adyacencia. Obs rvese que el algoritmo Visita nodo se aplica ex ctamente una vez sobre e a cada v rtice del grafo. Esto es debido a que el algoritmo Visita nodo se aplica e unicamente sobre v rtices u V no visitados (color[u]=BLANCO), y lo primero e que hace el propio algoritmo es marcar como visitado al v rtice sobre el que se e aplica (color[u]=AMARILLO), por lo que nunca m s volver a realizarse una a a llamada al algoritmo Visita nodo sobre u. Debido a que el coste temporal del algoritmo Visita nodo depende del n mero de v rtices adyacentes que tiene el u e nodo u sobre el que se aplica (bucle para), es decir, de la longitud de la lista de adyacencia del v rtice u; el coste temporal de realizar todas las llamadas al e a algoritmo Visita nodo ser : |ady(v)| = (|E|)
vV

Si, adem s, a adimos el coste temporal asociado a los bucles que se realizan a n en Recorrido en profundidad: O(|V |), podemos concluir que el coste temporal del algoritmo de recorrido en profundidad es O(|V | + |E|).

7.4.2.

Recorrido primero en anchura

El recorrido primero en anchura de un grafo, es una generalizaci n del recoro rido por niveles de un arbol. Iniciando en alg n v rtice u el recorrido, se visita u e u y, a continuaci n, se visitan cada uno de los v rtices adyacentes a u. El proceo e so se repite para cada uno de los nodos adyacentes a u, siguiendo el orden en que fueron visitados. El coste temporal del algoritmo es el mismo que para el recorrido en profundidad: O(|V | + |E|).
2 5 7
u

249

7.4.3.

Ordenaci n topol gica o o

Una aplicaci n inmediata del recorrido en profundidad es su utilizaci n para o o obtener una ordenaci n topol gica de los v rtices de un grafo dirigido y acclico. o o e Sea G = (V , E) un grafo dirigido y acclico; la ordenaci n topol gica es una o o permutaci n v1 , v2 , v3 , . . . , v|V | de los v rtices del grafo, tal que si (vi ,vj ) E, o e vi = vj , entonces vi aparece antes que vj en la permutaci n. o La ordenaci n no es posible si el grafo es cclico. Adem s, la permutaci n o a o v lida, obtenida como resultado de la ordenaci n topol gica, no es unica; es decir, a o o podran existir distintas permutaciones de los v rtices que dieran como resultado e una ordenaci n topol gica de los mismos. o o Una ordenaci n topol gica de un grafo puede ser vista como una ordenaci n o o o de los v rtices a lo largo de una lnea horizontal, de forma que todos los arcos van e de izquierda a derecha. Los grafos dirigidos y acclicos se utilizan en numerosas aplicaciones en las que se necesita representar el orden de ejecuci n de diferentes o tareas relacionadas entre s. A continuaci n se presenta el algoritmo que, dado un grafo G = (V , E) dirigio do y acclico, obtiene una ordenaci n topol gica de sus v rtices. El algoritmo es o o e pr cticamente el mismo que el utilizado en el recorrido primero en profundidad, a unicamente vara en la utilizaci n de una pila P en la que se va almacenando el o orden topol gico de los v rtices del grafo. o e Algoritmo Ordenaci n topol gica(G){ o o para cada v rtice u V e color[u] = BLANCO n para P = para cada v rtice u V e si (color[u] = BLANCO) Visita nodo(u) n para devolver(P ) } Algoritmo Visita nodo(u){ color[u] = AMARILLO para cada v rtice v V adyacente a u e si (color[v] = BLANCO) Visita nodo(v) n para apilar(P ,u) } 250

El algoritmo explota el hecho de que, en el recorrido primero en profundidad, cuando naliza el recorrido partiendo desde un nodo u (Visita nodo(u)), se habr n a visitado todos aquellos v rtices que son alcanzables desde u; es decir, todos aquee llos v rtices que deben suceder a u en el orden topol gico. De esta forma, si justo e o antes de nalizar la funci n Visita nodo apilamos el v rtice sobre el que se acaba o e de aplicar la funci n, el v rtice en cuesti n se apilar justamente despu s de haber o e o a e apilado todos aquellos v rtices que eran alcanzables desde el. Por lo tanto, cuando e o a e el algoritmo Ordenaci n topol gica nalice, en la pila P estar n los v rtices del o grafo ordenados topol gicamente. o Debido a que el tiempo de inserci n de cada v rtice en la pila tiene un coste o e constante O(1), obtener el orden topol gico de los v rtices de un grafo dirigido o e y acclico tiene un coste temporal equivalente a recorrer el grafo siguiendo la estrategia primero en profundidad: O(|V | + |E|). Ejemplo de funcionamiento del algoritmo A continuaci n se muestra c mo el algoritmo obtiene el orden topol gico de o o o los v rtices de un grafo dirigido y acclico. Debajo de cada gura se muestra e qu acci n se lleva a cabo, dada la situaci n que se muestra en la gura. Adem s, e o o a en todo momento, se muestra el contenido de la pila P y en qu instante se insertan e cada uno de los v rtices en la pila. e
2 5 7
u

5 7
u

5 7

(a) Orden topologico(G)

(b) Visita nodo(1) P = {}

(c) Visita nodo(4) P = {}

5 7
u

2
u

7 1 4 6 1 4 6

(d) Visita nodo(6) P = {}

(e) Visita nodo(7) P = {}

(f) apilar(P,7) P = {7}

251

5 7

5 7

5 7

(g) Visita nodo(3) P = {7}

(h) Visita nodo(5) P = {7}

(i) apilar(P,5) P = {5,7}


u

5 7

5 7
u

5 7

(j) apilar(P,3) P = {3,5,7}


u

(k) apilar(P,6) P = {6,3,5,7}

(l) Visita nodo(2) P = {6,3,5,7}

5 7
u

5 7
u

5 7

(m) apilar(P,2) P = {2,6,3,5,7}

(n) apilar(P,4) P = {4,2,6,3,5,7}

( ) n apilar(P,1) P = {1,4,2,6,3,5,7}

Tal como se indic anteriormente, una ordenaci n topol gica de un grafo o o o puede ser vista como una ordenaci n de los v rtices a lo largo de una lnea horo e izontal, de forma que todos los arcos van de izquierda a derecha. En la siguiente gura se muestra c mo puede representarse el grafo, utilizado en el ejemplo, siguo iendo el orden topol gico de sus vertices. o

252

1
2 5 7 1 4 6

7.5.
7.5.1.

Caminos de mnimo peso: algoritmo de Dijkstra


Caminos de mnimo peso

Un problema com n que se presenta en numerosas aplicaciones es la b squeda u u de caminos de mnimo peso en grafos dirigidos y ponderados. Veamos qu es un e camino de mnimo peso: Dado un grafo G = (V , E) dirigido y ponderado, donde cada arista (u,v) tiene asociada un peso w(u,v), se dene el peso de un camino p =< v0 , v1 , . . . , vk > como la suma de los pesos de las aristas que lo forman:
k

w(p) =
i=1

w(vi1 , vi )

Ahora podemos denir el camino de mnimo peso desde un v rtice u a v, e como el camino que tenga un peso menor entre todos los caminos de u a v, o si no existe camino de u a v. En lo sucesivo, para referirnos al peso de un camino de u a v hablaremos de la longitud de un camino de u a v, y para referirnos a un camino de mnimo peso de u a v diremos camino m s corto de u a v. a Dado el grafo dirigido y ponderado que se muestra en la gura, se indican cu les son las longitudes de todos los caminos desde el v rtice 1 al v rtice 2. a e e Como puede observarse el camino < 1, 3, 2 > es el camino de menor longitud.
10

1
100 20

50 30

2
5

5
10

50

253

Camino
1
50

Longitud (peso o coste)


2

50
2

30

35
2

30

50

20

100 120

100

20

10

10

20

40

Por ejemplo, sup ngase que utilizamos un grafo dirigido y ponderado para reo presentar las comunicaciones entre aeropuertos: cada v rtice representa una ciue dad, y cada arista (u,v) una ruta a rea de la ciudad u a la ciudad v; el peso asociado e a la arista podra ser el tiempo que se requiere para volar de u a v. La obtenci n o del camino m s corto entre un par de ciudades nos indicara cu l es la ruta m s a a a r pida entre ellas. a Dentro del c lculo de caminos de menor longitud en grafos dirigidos y pona derados existen distintas variantes: Obtenci n de los caminos m s cortos desde un v rtice origen a todos los o a e dem s. a Obtenci n de los caminos m s cortos desde todos los v rtices a uno destino. o a e Obtenci n del camino m s corto de un v rtice u a un v rtice v. o a e e Obtenci n de los caminos m s cortos entre todos los pares de v rtices. o a e El algoritmo de Dijkstra, que veremos a continuaci n, obtiene los caminos m s o a cortos desde un v rtice origen a todos los dem s. El resto de variantes indicadas e a pueden resolverse aplicando igualmente el algoritmo de Dijkstra, si bien, la ultima de las variantes puede resolverse de manera m s eciente [Cormen]. a

7.5.2.

Algoritmo de Dijkstra

Problema: Sea G = (V , E) un grafo dirigido y ponderado con pesos no negativos en las aristas; dado un v rtice origen s V , se desea obtener cada uno de los e caminos m s cortos de s al resto de v rtices v V . a e 254

El algoritmo de Dijkstra resuelve ecientemente este problema, si bien, hay que hacer hincapi en que si existen aristas ponderadas con pesos negativos en e el grafo, la soluci n obtenida podra ser erronea. Otros algoritmos, como el de o Bellman-Ford [Cormen], permiten pesos negativos en las aristas, siempre y cuando, no existan ciclos de peso negativo alcanzables desde el v rtice origen s V . e Los algoritmos de b squeda de caminos m s cortos explotan la propiedad de u a que el camino m s corto entre dos v rtices contiene, a su vez, caminos m s cortos a e a entre los v rtices que forman el camino. e El algoritmo de Dijkstra mantiene los siguientes conjuntos: Un conjunto de v rtices S que contiene los v rtices para los que la distancia e e m s corta desde el origen ya es conocida. Inicialmente S = . a Un conjunto de v rtices Q = V S en el que se mantiene, para cada e v rtice, la distancia m s corta desde el origen pasando a trav s de v rtices e a e e que pertenecen a S; es decir, para cada v rtice u Q se mantiene la distane cia m s corta desde el origen utilizando uno de los caminos m s cortos ya a a calculados. A esta distancia la denominaremos distancia provisional. Para mantener las distancias provisionales utilizaremos un vector D de talla |V |, en el cual, para cada posici n i, D[i] indicar la distancia provisional desde o a el v rtice origen s al v rtice i. Inicialmente, D[u] = u V {s} y e e D[s]= 0. La estrategia que sigue el algoritmo es la siguiente: 1. Se extrae del conjunto Q el v rtice u cuya distancia provisional D[u] es e la menor. Se puede armar que esta distancia es la menor posible entre el v rtice origen s y u. La raz n de esta aseveraci n radica en que, dee o o bido a que los pesos de las aristas son no negativos, no ser posible encona trar otro camino m s corto desde s hasta u pasando a trav s de alg n otro a e u v rtice perteneciente a Q, ya que sus distancias provisionales eran maye ores o iguales que la distancia provisional a u. Al mismo tiempo, la distancia provisional se corresponda con el camino m s corto posible pasando a a trav s de alg n camino m s corto ya calculado; o lo que es lo mismo, con e u a el camino m s corto posible utilizando v rtices de S. Por este motivo, ya no a e es posible encontrar un camino m s corto desde s hasta u utilizando alg n a u otro v rtice del grafo, ya que V = S Q, por lo que podemos concluir que e hemos hallado el camino m s corto de s a u. a 2. A continuaci n, se inserta el nuevo v rtice u, para el que se ha calculado el o e camino m s corto desde s, en el conjunto S (S = S {u}). Al a adir u a n al conjunto S ser posible acceder a los v rtices v adyacentes a u a trav s a e e 255

del nuevo camino m s corto calculado desde s hasta u. Por lo tanto, podra a ocurrir que las distancias provisionales de los v rtices v Q adyacentes a u e fueran mejoradas utilizando el nuevo camino, por lo que, si esto ocurre, se actualiza la distancia provisional de estos v rtices al nuevo valor calculado. e 3. Los pasos 1 y 2 se repiten hasta que el conjunto Q queda vaco, momento en el cual se habr n estimado los caminos m s cortos desde el v rtice origen s a a e al resto de v rtices del grafo. En el vector D se tendr , para cada v rtice, la e a e distancia m s corta desde el origen. a A continuaci n se presenta el algoritmo de Dijkstra (en pseudo-c digo). El o o algoritmo utiliza un vector P de talla |V | que permitir recuperar la secuencia a de v rtices que forman cada uno de los caminos mnimos calculados; en cada e posici n i del vector P , se almacena el ndice del v rtice que precede al v rtice i o e e en el camino m s corto desde el origen s hasta i. a Dado un grafo G = (V , E) dirigido y ponderado, una funci n de ponderaci n o o w y un v rtice origen s: e Algoritmo Dijkstra(G, w, s){ para cada v rtice v V hacer e D[v] = P [v] = N U LO n para D[s] = 0 S= Q=V mientras Q = hacer u = extract min(Q) / seg n D / u S = S {u} para cada v rtice v V adyacente a u hacer e si D[v] > D[u] + w(u,v) entonces D[v] = D[u] + w(u,v) P [v] = u n si n para n mientras }

256

Ejemplo del funcionamiento del algoritmo A continuaci n se muestra un ejemplo de c mo el algoritmo de Dijsktra obo o tiene los caminos m s cortos desde el v rtice origen 1 a todos los dem s, en el a e a siguiente grafo. Las aristas con trazo discontnuo indican caminos provisionales desde el origen a v rtices; las aristas con trazo grueso indican caminos mnimos e ya calculados; el resto de aristas se dibujan con trazo no. Debajo de cada gura se muestra el estado de las estructuras que mantiene el algoritmo dada la situaci n o mostrada en la gura.
s 5

1
10

20 40

2
10 5

6
10

3
5

20

S {}

Q {1,2,3,4,5,6}

u D P

1 0 N U LO
s 5

2 N U LO

3 N U LO

4 N U LO

5 N U LO

6 N U LO

1
10

20 40

2
10 5

6
10

3
5

20

S {1}

Q u {2,3,4,5,6} 1

D P

1 0 N U LO
s

2 N U LO

3 4 40 1 N U LO

5 6 10 5 1 1

1
10

20 40

2
10 5

6
10

3
5

20

S {1,6}

Q u {2,3,4,5} 6

D P

1 0 N U LO 257

2 25 6

3 4 40 1 N U LO

5 6 10 5 1 1

s 5

1
10

20 40

2
10 5

6
10

3
5

20

S {1,6,5}

Q u {2,3,4} 5

D P
s

1 0 N U LO

2 25 6

3 40 1

4 5 6 30 10 5 5 1 1

1
10

20 40

2
10 5

6
10

3
5

20

S {1,6,5,2}

Q {3,4}

u 2 D P
s 5

1 0 N U LO

2 25 6

3 40 1

4 5 6 30 10 5 5 1 1

1
10

20 40

2
10 5

6
10

3
5

20

S {1,6,5,2,4}

Q {3}

u 4

D P
s

1 0 N U LO

2 25 6

3 35 4

4 5 6 30 10 5 5 1 1

1
10

20 40

2
10 5

6
10

3
5

20

S {1,6,5,2,4,3}

Q {}

u 3

D P

1 0 N U LO 258

2 25 6

3 35 4

4 5 6 30 10 5 5 1 1

Coste temporal del algoritmo


Para evaluar el coste temporal del algoritmo asumiremos que la representaci n o del grafo se realiza mediante listas de adyacencia. El coste temporal del algoritmo viene determinado por el coste que supone realizar todas las iteraciones del bucle mientras. Debido a que el bucle mientras se repite hasta que el conjunto Q queda vaco, si tenemos en cuenta que inicial mente el conjunto Q contiene todos los v rtices del grafo, y que, en cada iteraci n e o del bucle se extrae un elemento de Q, podemos concluir que el bucle mientras se a realiza |V | veces. As pues, la operaci n extract min se llevar a cabo |V | veces. o Por otro lado, el bucle para, que contiene el bucle mientras, se realizar iguala mente |V | veces (una vez para cada v rtice del grafo). El bucle para se repite e tantas veces como v rtices adyacentes tenga el v rtice extrado de Q, por lo que e e el n mero total de iteraciones de este bucle coincidir con el n mero de v rtices u a u e adyacentes que tengan los v rtices del grafo, o lo que es lo mismo, con el n mero e u de arcos del grafo: |E|. Para obtener nalmente el coste temporal del algoritmo, nos falta estimar el a coste temporal asociado a la operaci n extract min. Este coste depender de la foro ma en que se obtenga el v rtice de Q cuya distancia desde el origen sea la mnima e respecto a todos los v rtices de Q. Como una primera aproximaci n considere o aremos que los elementos del conjunto Q se organizan en un vector sin ning n u a tipo de ordenaci n, por lo que la operaci n extract min tendr un coste O(|V |). o o En este caso, y debido a que tal como dijimos anteriormente, la operaci n exo tract min se realiza |V | veces, el coste temporal total de extract min es O(|V |2 ). Si a este coste le sumamos el coste que supone realizar el bucle para, podemos concluir que el coste temporal del algoritmo de Dijkstra, para esta primera aproximaci n, es O(|V |2 + |E|) = O(|V |2 ) o a La operaci n extract min podra realizarse de forma m s eciente, si los elo ementos del conjunto Q estuvieran organizados mediante un montculo1 en una cola de prioridad. En este caso, la operaci n extract min tendra un coste tempoo ral O(log |V |) y, como se realiza |V | veces, el coste temporal total de extract min sera O(|V | log |V |). Sin embargo, construir el montculo tendra un coste O(|V |). Por otro lado, actualizar una distancia provisional (en el bucle para) supondra modicar la prioridad (distancia provisional) del v rtice en el montculo y, consee cuentemente, reorganizar el montculo con un coste temporal O(log |V |); por lo tanto, el coste de cada iteraci n del bucle para sera, en este caso, O(log |V |). o Debido a que el bucle para se realiza |E| veces, el coste total de este bucle ser O(|E| log |V |). a En denitiva, si organiz ramos los v rtices del conjunto Q en una cola de prioa e
El mnimo estara en la raz y, para todo nodo excepto el raz, su clave sera mayor o igual que la del padre.
1

259

ridad, el coste temporal del algoritmo sera: O(|V | + |V | log |V | + |E| log |V |) = O((|V | + |E|) log |V |).

7.6.
7.6.1.

Arbol de expansi n de coste mnimo o


Arbol de expansi n o

A continuaci n introduciremos algunos conceptos necesarios para la exposio ci n del problema que se pretende resolver. o Un grafo no dirigido, acclico y conexo se denomina arbol libre. Un arbol libre cumple las siguientes propiedades: 1. Un arbol libre con n 1 nodos contiene ex ctamente n 1 arcos. a 2. Si se a ade una nueva arista a un arbol libre, se crea un ciclo. n 3. Cualquier par de v rtices est n conectados por un unico camino. e a
1 2 5 3 6

Figura 7.19: Ejemplo de arbol libre Un arbol de expansi n de un grafo no dirigido G = (V , E), es un arbol libre o T = (V ,E ), tal que V = V y E E; es decir T contiene todos los v rtices de e G, y las aristas de T son aristas de G. Dado un grafo no dirigido y ponderado mediante una funci n de ponderaci n o o w, el coste de un arbol de expansi n T es la suma de los costes de todos los o arcos del arbol. w(T ) =
(u,v)T

w(u, v)

Un arbol de expansi n de un grafo ser de coste mnimo, si se cumple que o a su coste es el menor posible respecto al coste de cada uno de los posibles arboles de expansi n que contiene el grafo. o 260

1 2 5
(a)

1 2 5
(b)

3 6

3 6

Figura 7.20: En la gura (b) se muestra un arbol de expansi n del grafo que se o muestra en la gura (a). Obs rvese que el arbol de expansi n es un arbol libre que e o contiene todos los v rtices y un subconjunto de aristas del grafo mostrado en (a). e
1
6 5 1 5 6 6

1
5 1 1 5 4

1 2
3 5

2
3

4
2

2
6

3
4

4 6

3
4 2

4 6

(a)

(b) arbol de expansi n de o coste 22

(c) arbol de expansi n de o coste mnimo: 15

Figura 7.21: En la gura (b) se muestra un arbol de expansi n del grafo ponderado o que se muestra en la gura (a), y en la gura (c) se muestra un arbol de expansi n o de coste mnimo para el grafo de la gura (a). As podemos plantear el problema de obtener el arbol de expansi n de coste o mnimo para un grafo dado de esta forma: Problema: Sea G = (V , E) un grafo no dirigido, conexo y ponderado mediante pesos asociados a los arcos; se desea obtener un nuevo grafo G = (V ,E ) donde E E tal que G sea un arbol de expansi n de coste mnimo de G. o Existen dos algoritmos cl sicos que resuelven este problema: el algoritmo de a Kruskal y el algoritmo de Prim.

7.6.2.

Algoritmo de Kruskal

El algoritmo de Kruskal resuelve ecientemente el problema de obtenci n de o un arbol de expansi n de coste mnimo para un grafo. Veamos en qu consiste: o e 261

El algoritmo mantiene las siguientes estructuras: Un conjunto A en el que se mantiene, en todo momento, aquellas aristas que ya han sido seleccionadas como pertenecientes al arbol de expansi n o de coste mnimo. Por lo tanto, cuando el algoritmo nalice, el conjunto A contendr el subconjunto de aristas E E que forman parte del arbol de a expansi n de coste mnimo. o Un MF-set que se utiliza para saber qu v rtices est n unidos entre s en e e a el bosque (conjunto de arboles) que se va obteniendo durante el proceso de construcci n del arbol de expansi n de coste mnimo. Hay que tener o o en cuenta que, a medida que se van a adiendo aristas que deben formar n parte del arbol de expansi n de coste mnimo al conjunto A, se ir n uniendo o a v rtices entre s formando arboles, por lo que, en un momento dado del proe ceso de construcci n, existir n distintos arboles (bosque) que, poco a poco, o a ir n enlaz ndose entre s hasta obtener un unico arbol que se correspona a der con el arbol de expansi n de coste mnimo. Cada subconjunto disjunto a o del MF-set contiene aquellos v rtices que ya est n unidos entre s en el arbol e a de expansi n de coste mnimo que se est obteniendo; por lo tanto, si dos o a v rtices u, v pertenecen al mismo subconjunto disjunto indicar que existe e a un camino que los une en el arbol (dentro del bosque) en que se encuentren. La estrategia que sigue el algoritmo es la siguiente: 1. Inicialmente el conjunto A est vaco A = ; es decir, al principio, no a existe ninguna arista que pertenezca al arbol de expansi n de coste mnimo. o Por lo tanto, inicialmente, el MF-set estar formado por |V | subconjuntos a disjuntos que contendr n, cada uno de ellos, uno de los v rtices del grafo a e G; esto es as, ya que, como todava no existen aristas que pertenezcan al arbol de expansi n de coste mnimo, no pueden existir v rtices conectados o e entre s. 2. De entre todas las aristas (u,v) E, se selecciona, como candidata para formar parte del arbol de expansi n de coste mnimo, aqu lla que no se ha o e seleccionado todava y tenga asociada un menor peso; es decir, la que, si se a ade, provoque un incremento menor del peso del arbol de expansi n de n o coste mnimo. Si el v rtice u y v no est n conectados entre s en el arbol de e a expansi n de coste mnimo (Buscar(u) = Buscar(v)), al a adir esta nueva o n arista al arbol de expansi n de coste mnimo, se unir n con el menor coste o a posible, todos los v rtices conectados con v con todos los v rtices conectae e dos con u (Union(Buscar(u),Buscar(v)), por lo que la arista (u,v) se a ade n al conjunto A para formar parte del arbol de expansi n de coste mnimo. Si, o 262

por el contrario, el v rtice u y v ya estaban conectados entre s, no se realiza e ninguna acci n ya que si se a adiera la arista al arbol de expansi n de coste o n o mnimo, este dejara de cumplir las propiedades de los arboles libres. 3. El paso (2) se repite una vez para cada arista (u,v) E. A continuaci n se presenta el algoritmo. Dado un grafo G = (V , E) no dirigio do, conexo y ponderado: Algoritmo Kruskal(G){ A= para cada v rtice v V hacer e Crear Subconjunto(v) n para ordenar las aristas pertenecientes a E, seg n su peso, en orden no decreciente u para cada arista (u,v) E, siguiendo el orden no decreciente hacer si Buscar(u) = Buscar(v) entonces A = A {(u,v)} Union(Buscar(u),Buscar(v)) n si n para devuelve(A) } Ejemplo de funcionamiento del algoritmo A continuaci n se muestra un ejemplo de c mo el algoritmo de Kruskal obo o tiene el arbol de expansi n de coste mnimo del grafo de la siguiente gura. Para o cada iteraci n que realiza el algoritmo se muestra el estado del conjunto A y del o MF-set; as como tambi n el bosque que se va obteniendo durante el proceso e de construcci n del arbol de expansi n de coste mnimo. Tambi n, para cada ito o e eraci n, se muestra qu arista ha sido la seleccionada para formar parte del arbol, o e y, dependiendo de la arista escogida, las acciones que lleva a cabo el algoritmo.
10 45 30 40

2
35 55 25

50

5 4
20

15

263

MF-set (componentes conexas)

Arbol de coste mnimo


1 5 2 3 6

{}

{{1},{2},{3},{4},{5},{6}}

10

Buscar(1) = Buscar(2) A = A {(1,2)}; Union(Buscar(1),Buscar(2))

10

2 3 6

{(1,2)}

{{1,2},{3},{4},{5},{6}}

3 6
15

Buscar(6) = Buscar(3) A = A {(6,3)}; Union(Buscar(6),Buscar(3))

10

2 3
15

{(1,2),(6,3)}

{{1,2},{6,3},{4},{5}}

20

Buscar(4) = Buscar(6) A = A {(4,6)}; Union(Buscar(4),Buscar(6))

10

2 3
15

{(1,2),(6,3),(4,6)}

{{1,2},{4,6,3},{5}}

20

264

MF-set (componentes conexas)


2
25

Arbol de coste mnimo

Buscar(2) = Buscar(6) A = A {(2,6)}; Union(Buscar(2),Buscar(6))

10

2
25 15

{(1,2),(6,3),(4,6),(2,6)}

{{1,2,4,6,3},{5}}

20

1
30

Buscar(1) = Buscar(4)

35

Buscar(5) = Buscar(3) A = A {(5,3)}; Union(Buscar(5),Buscar(3))

10

2
35 25

{(1,2),(6,3),(4,6),(2,6),(5,3)}

{{5,1,2,4,6,3}}

15 20

Coste temporal del algoritmo


Para analizar el coste temporal del algoritmo asumiremos que en las operaciones sobre el MF-set Uni n y Buscar, se aplican los heursticos uni n por rango o o y compresi n de caminos, respectivamente. Por lo tanto, tal como se estudi en el o o tema 3, en ese caso, estas operaciones tendr n un coste pr cticamente constante. a a Teniendo en cuenta que estas operaciones se realizan para cada arista del grafo, el

265

coste temporal total de realizar las operaciones sobre el MF-set ser O(|E|). Por a otra parte, la creaci n de un subconjunto disjunto (operaci n Crear Subconjunto) o o tiene un coste constante, por lo que, la construcci n inicial de |V | subconjuntos o disjuntos tendr un coste O(|V |). a Otra operaci n que realiza el algoritmo es la de ir seleccionando las aristas en o orden no decreciente. Si la lista de aristas se organizara mediante un montculo2 en una cola de prioridad, el coste temporal de extraer la arista de mnimo peso sera O(log |E|). Debido a que se extraen todas las aristas de la cola de prioridad, se realizaran |E| operaciones de extracci n, por lo que el coste temporal total o sera O(|E| log |E|). Adem s, habra que a adir el coste temporal de construir el a n montculo O(|E|). En denitiva, podemos concluir que el coste temporal del algoritmo de Kruskal es O(|V | + |E| + |E| log |E| + |E|) = O(|E| log |E|).

7.6.3.

Algoritmo de Prim

El algoritmo de Prim resuelve el mismo problema que el de Kruskal (obtener el arbol de expansi n de coste mnimo), pero aplicando otra estrategia. o El algoritmo de Prim utiliza las siguientes estructuras durante la ejecuci n del o algoritmo: Un conjunto A en el que se guardan aquellas aristas que ya forman parte del arbol de expansi n de coste mnimo. o Un conjunto S inicialmente vaco, y al que se ir n a adiendo los v rtices a n e de V conforme se vayan recorriendo para formar el arbol de expansi n de o coste mnimo. La estrategia del algoritmo de Prim es: empezando por cualquier v rtice, cone struir incrementalmente un arbol de recubrimiento, seleccionando en cada paso una arista (u,v) A tal que: Si se a ade (u,v) al conjunto de aristas A obtenido hasta el momento no se n cree ning n ciclo. u Produzca el menor incremento de peso posible. Los arcos del arbol de expansi n parcial forman un unico arbol. o El arbol toma como raz un v rtice cualquiera del grafo, u V y a partir de e este se extiende por todos los v rtices del grafo G = (V, E). e
El mnimo estara en la raz y, para todo nodo excepto el raz, su clave (peso de la arista) sera mayor o igual que la del padre.
2

266

En cada paso se selecciona el arco de menor peso que une un v rtice pree sente en S con uno de V que no lo est . e A continuaci n se presenta el algoritmo. Dado un grafo G = (V , E) no dirigio do, conexo y ponderado: Algoritmo Prim(G){ A= u = elemento arbitrario de V S = {u} mientras S = V hacer (u,v) = argmin(x,y)S(V S) peso(x, y) A = A {(u,v)} S = S {v} n mientras devuelve(A) } Donde la operaci n de b squeda de la arista factible de menor peso, que hemos o u denotado como (u,v) = argmin(x,y)S(V S) peso(x, y) se calculara de la sigu iente forma: m = + para x S hacer para y V S hacer si peso(x, y) < m entonces m = peso(x, y) (u,v) = (x,y) n si n para n para Ejemplo del funcionamiento del algoritmo A continuaci n se muestra un ejemplo de c mo el algoritmo de Prim obtiene o o el arbol de expansi n de coste mnimo del grafo de la siguiente gura (es el miso mo ejemplo que el presentado para el algoritmo de Kruskal). Para cada iteraci n o 267

que realiza el algoritmo se muestra el estado del conjunto A y del conjunto S. Tambi n, para cada iteraci n, se muestra qu arista ha sido la seleccionada para e o e formar parte del arbol, y, dependiendo de la arista escogida, las acciones que lleva a cabo el algoritmo.

1
45 30

10 40

2
35 55 25

50

5 4
20

15

Arbol de coste mnimo


1 5 2 3 6

{}

{}

A = ; u = elemento arbitrario de V (Ej: u=1); S = {1};


1

1 5

2 3 6

{}

{1}

10

argmin (u,v) = (1,2); A = A {(1,2)}; S = S {2};

268

Arbol de coste mnimo


1
10

2 3 6

{(1,2)}

{1,2}

2
25

argmin (u,v) = (2,6); A = A {(2,6)}; S = S {6};

10

2
25

{(1,2),(2,6)}

{1,2,6}

3 6
15

argmin (u,v) = (6,3); A = A {(6,3)}; S = S {3};

10

2
25 15

{(1,2),(2,6),(6,3)}

{1,2,6,3}

20

argmin (u,v) = (6,4); A = A {(6,4)}; S = S {4};

269

Arbol de coste mnimo


1
10

2
25 15

{(1,2),(2,6),(6,3),(6,4)}

{1,2,6,3,4}

20

35

argmin (u,v) = (3,5); A = A {(3,5)}; S = S {5};

10

2
35 25

{(1,2),(2,6),(6,3),(6,4),(3,5)}

{1,2,6,3,4,5}

15 20

Coste temporal del algoritmo


Analizando en primer lugar el coste del c lculo de (u,v) = argmin(x,y)S(V S) peso(x, y) a vemos que por los dos bucles para anidados que tiene esta operaci n y que recoro ren los subconjuntos de v rtices que se van creando, su coste temporal es O(|V |2 ). e El algoritmo de Prim tiene un bucle principal mientras que realizar |V | 1 a iteraciones, debido a que en la primera iteraci n el conjunto S solo tiene un v rtice o e y a cada iteraci n s lo se a ade un nuevo v rtice de V . La operaci n principal que o o n e o engloba este bucle es la de c lculo de la arista factible de menor peso, que como a ya hemos analizado tiene un coste O(|V |2 ). Por todo esto el algoritmo de Prim tiene un coste O(|V |3 ) Sin embargo existen algunas optimizaciones para el algoritmo del c lculo de a (u,v) = argmin(x,y)S(V S) peso(x, y) que consiguen rebajarlo a un coste O(|V |) y por tanto se puede construir una versi n optimizada del algoritmo de Prim con o 2 un coste O(|V | ), aunque no veremos aqu dicha optimizaci n. o

270

7.7.

Ejercicios

Ejercicio 1: -Haz una traza del algoritmo de recorrido de un grafo primero en profundidad para el grafo que se muestra a continuaci n. Para hacer la traza se recomienda o previamente, representar la estructura del grafo mediante listas de adyacencia. El recorrido se inicia desde el v rtice 1. e

2 4
Soluci n: o

1 5

En primer lugar representamos el grafo mediante listas de adyacencia; el orden de los nodos que se ha elegido para las listas es arbitrario, aunque una vez se haya escogido un orden, debe respetarse para la traza del recorrido en profundidad.
1 2

2 1 1 2 1

4 5

5 4

2 4

1 5

3 4 5

1 2

5 4

El v rtice de inicio se ha especicado que es el 1, con lo que el recorrido en e profundidad se realizara de la siguiente manera:

271

2 4

1 5

2 4

1 5

Recorrido en profundidad(G)
u u

Visita nodo(1)

2 4

1 5

2 4

1 5

color[1]=AMARILLO
u

Visita nodo(2)

2 4

1 5

2 4

1
u

3 5

color[2]=AMARILLO

Visita nodo(5)

2 4

1
u

3 5

2
u

1 4 5

color[5]=AMARILLO

Visita nodo(4)
u

2
u

1 4 5

2 4

1 5

color[4]=AMARILLO
u

Visita nodo(3)

2 4

1 5

color[3]=AMARILLO

272

Ejercicio 2: -Haz una traza del algoritmo de Dijkstra para el siguiente grafo, tomando como v rtice origen el 1. Una vez obtenida la soluci n, recupera la secuencia de v rtices e o e que forman cada uno de los caminos m s cortos, utilizando, para ello, el vector P a que usa el algoritmo para almacenar los predecesores.

3
8

10 12 7 9

2
10

4
Soluci n: o

En el problema se ha especicado que el v rtice de inicio es el 1. As pues, la e traza de Dijkstra para el grafo de la gura es:
3
8 10 12 7 9

2
10

S {}

Q {1,2,3,4,5}

u D P

1 0 N U LO

2 N U LO

3 N U LO

4 N U LO

5 N U LO

3
8

10 12 7 9

2
10

S {1}

Q u {2,3,4,5} 1

D P

1 0 N U LO

2 N U LO

3 N U LO

4 N U LO

5 10 1

273

3
8

10 12 7 9

2
10

S {1,5}

Q u {2,3,4} 5

D P

1 0 N U LO

2 N U LO

3 N U LO

4 19 5

5 10 1

3
8

10 12 7 9

2
10

S {1,5,4}

Q u {2,3} 4

D P

1 0 N U LO

2 3 31 4 N U LO

4 19 5

5 10 1

3
8

10 12 7 9

2
10

S {1,5,4,2}

Q u {3} 2

D P

1 0 N U LO

2 3 31 4 N U LO

4 19 5

5 10 1

274

3
8

10 12 7 9

2
10

S {1,5,4,2,3}

Q u {} 3

D P

1 0 N U LO

2 3 31 4 N U LO

4 19 5

5 10 1

Lo que ocurrira ahora sera que el algoritmo acabara, pero con una soluci n o un tanto extra a. La operaci n de extraer el mnimo de Q nos devolvera el v rtice n o e 3, puesto que no queda otro. Ahora el bucle: mientras Q = hacer del algoritmo de Dijkstra terminara ya que el conjunto Q se queda vaco. Sin embargo, el resultado nos dice que si comenzamos por el v rtice 1, el e v rtice 3 es inalcanzable y por eso el camino de coste mnimo de 1 a 3 es innito, e o podemos decir que no existe camino de 1 a 3 ya que P [3] = N U LO.

275

Ejercicio 3: -Utilizando el algoritmo de Kruskal: a) c mo se podra determinar si un grafo es conexo? o b) c mo se podran obtener los v rtices que forman cada una de las componente o e conexas? No hace falta que escribas los algoritmos modicados, s lo comenta la estrategia o a seguir y la modicaci n que se hara al algoritmo. o Soluci n: o a) Si aplicamos el algoritmo de Kruskal, al acabar este obtendremos como un subproducto un MF-SET que indica las componentes conexas que hay en el grafo, esto es, indica qu v rtices est n unidos entre s al terminar el e e a algoritmo. Si el MF-SET tiene un solo subconjunto, esto es, una sola componente conexa, entonces el grafo es conexo. b) A partir del mismo MF-SET comentado en el apartado a). Este MF-SET contiene los v rtices que est n conectados entre s en el grafo y los tiene e a agrupados en subconjuntos. Cada subconjunto disjunto del MF-SET se corresponde con una componente conexa, y los elementos de ese subconjunto son los v rtices de esa componente conexa. e

276

Ejercicio 4: -Dados dos grafos G1 = (V1 , E1 ) y G2 = (V2 , E2 ), con V1 = V2 , se desea obtener un nuevo grafo G = (V, E) que sea la diferencia sim trica de G1 y G2 , tal que e se cumpla que V = V1 = V2 , y el conjunto de aristas E contenga las aristas de E1 que no est n en E2 , junto con las de E2 que no est n en E1 . e e Escribe una funci n en lenguaje C con la cabecera: o grafo *diferencia_simetrica(grafo *G1, grafo *G2) que construya el grafo resultante G, suponiendo que los grafos est n represena tados mediante matrices de adyacencia con la denicion de tipos vista en clase: #dene MAXVERT 1000 typedef struct{ int talla; int A[MAXVERT][MAXVERT]; } grafo; Analiza tambi n el coste temporal del algoritmo. e Soluci n: o La funci n tendr que crear una nueva estructura grafo (reserva de memoria), o a e indicar que el nuevo grafo creado tiene el mismo n mero de v rtices que G1 o u e G2 , puesto que V = V1 = V2 . La estrategia a seguir para obtener las aristas correspondientes al grafo resultado G aprovecha que la representaci n de los grafos o se realiza mediante una matriz de adyacencias para obtener dichas aristas con un solo recorrido conjunto de las matrices de adyacencia de G1 y G2 . La idea es que en G habr una arista del v rtice i al j (es decir, G > A[i][j] = a e 1) si: en G1 hay una arista del v rtice i al j y no la hay en G2 (es decir, G1 > e A[i][j] == 1 y G2 > A[i][j] == 0), o bien en G2 hay una arista del v rtice i al j y no la hay en G1 (es decir, G1 > e A[i][j] == 0 y G2 > A[i][j] == 1). Como se puede observar, los dos casos en los que habr una arista en G del a v rtice i al j ser cuando las posiciones (i, j) de las matrices de adyacencias de e a G1 y G2 tengan un contenido diferente, con lo que los dos casos se pueden reducir a comparar el contenido de las posiciones correspondientes en las matrices. 277

grafo *diferencia_simetrica(grafo *G1, grafo *G2) { grafo *G; int i,j; int t; G = (grafo *) malloc(sizeof(grafo)); /* Sabemos que el numero de vertices de G1 y G2 es */ /* el mismo --> sus matrices tienen la misma talla. */ t = G1->talla; /* Recorremos las matrices que representan las aristas */ /* de G1 y G2 y pondremos como aristas de G aquellas */ /* de G1 y G2 que sean diferentes. */ for (i=1;i<=t;i++) for (j=1;j<=t;j++) if (G1->A[i][j] != G2->A[i][j]) G->A[i][j] = 1; else G->A[i][j] = 0; return(G); } Como se ve claramente, el coste del algoritmo ser O(|V |2 ), esto es, el cuadraa do del n mero de v rtices del grafo resultado (que es el mismo n mero de v rtices u e u e que tienen el grafo G1 y G2 ) y que viene dado por los dos bucles for anidados, cada uno de los cuales se ejecuta |V | veces.

278

Tema 8 Algoritmos Voraces


8.1. Introducci n o

Los objetivos b sicos de este tema son: estudiar la t cnica de dise o de ala e n goritmos voraces y estudiar algunos problemas cl sicos, como el problema de la a mochila con fraccionamiento. Normalmente esta t cnica de dise o de algoritmos se aplica a problemas de e n optimizaci n, esto es, a la b squeda del valor optimo (m ximo o mnimo) de o u a una cierta funci n objetivo en un dominio determinado. M s adelante veremos o a algunos ejemplos como el problema del Cajero autom tico y el problema de la a mochila con fraccionamiento. La soluci n a un problema mediante un algoritmo voraz se puede presentar o como una secuencia de decisiones: Las decisiones se toman en base a una medida local de optimizaci n. Esta o medida local de optimizaci n puede entenderse como un criterio de manejo o de datos. Las decisiones que se toman para buscar una soluci n determinada son iro reversibles. Un algoritmo voraz no siempre encuentra la soluci n optima, pero en ocao siones permite encontrar una soluci n aproximada con un coste computacional o bajo. La estrategia voraz que ha de seguir un algoritmo voraz consiste en que en cada paso del algoritmo se debe realizar una selecci n entre las opciones existentes. La o estrategia voraz aconseja realizar la elecci n que es mejor en ese momento. Tal o estrategia no garantiza, de forma general, que se encuentre la soluci n optima al o problema. 279

8.1.1.

Ejemplo: Cajero autom tico a

Vamos a ver c mo se puede resolver un sencillo problema mediante una eso trategia voraz. Supongamos que tenemos que programar el control de un cajero autom tico. a El problema a resolver es suministrar la cantidad de dinero solicitada en billetes usando s lo los tipos de billetes especicados y de manera que el n mero o u total de billetes sea mnimo. Por ejemplo supongamos que se solicita una cantidad M = 1100 Euros, disponiendo de billetes de cantidad {100, 200, 500}. Algunas de las posibles soluciones que podemos dar para este problema seran: 11 100 (11 billetes). 5 200 + 1 100 (6 billetes). 2 500 + 1 100 (3 billetes). Una posible estrategia voraz consistira en ir seleccionando siempre el billete de mayor valor, siempre y cuando queden billetes de ese tipo y al a adir uno m s n a de esos billetes no nos pasemos de la cantidad total de dinero solicitada. Respecto a esta estrategia podemos observar que: a veces no hay soluci n, por ejemplo, si M = 300 y no hay billetes de 100. o a veces la soluci n no se encuentra, por ejemplo, si M = 1100 y no hay bilo letes de 100 (Nuestro algoritmo cogera 2 500 y no podra seguir mientras que s que existira una soluci n: 1 500 + 3 200). o a veces encuentra una soluci n factible, pero no optima; por ejemplo, si los o tipos de billetes de que disponemos son {10, 50, 110, 500} y M = 650, la soluci n que se obtendra sera: 1 500 + 1 110 + 4 10 (6 billetes) o mientras que la soluci n optima es: 1 500 + 3 50 (4 billetes). o

8.2.

Esquema general Voraz

Vamos a describir a nivel general el esquema que ha de seguir un algoritmo que pretenda utilizar una estrategia voraz para la resoluci n de un problema. Para o ello usaremos la siguiente notaci n: o C : Conjunto de elementos o candidatos a elegir. S : Conjunto de elementos de la soluci n en curso (secuencia de decisiones tomadas). o 280

soluci n(S) : funci n que nos indica si S es soluci n o no. o o o factible(S) : funci n que nos indica si S es factible o completable. o selecciona(C) : funci n que selecciona un candidato o elemento de C conforme o al criterio denido. f : funci n objetivo a optimizar. o B sicamente, el algoritmo voraz obtiene un subconjunto de C que optimiza la a funci n objetivo. Para ello, los pasos a seguir son: o 1. Seleccionar en cada instante un candidato. 2. A adir el candidato a la soluci n en curso si es completable, sino rechazarlo. n o 3. Repetir 1 y 2 hasta obtener la soluci n. o NOTA IMPORTANTE: Un algoritmo voraz no siempre proporciona la soluci n optima. o las decisiones son irreversibles. El esquema de dise o voraz tiene esta forma: n Algoritmo Voraz(C) { S = ; /* conjunto vacio */ while ((!soluci n(S)) && (C = )) { o x = selecciona(C); C = C - {x} if (factible(S {x})) { S = S {x}; } } if (soluci n(S)) return(S); o else return(No hay soluci n); o } Por ejemplo, aplicando el esquema anterior al problema del cajero autom tico, a el conjunto C sera el conjunto de billetes que tiene disponibles el cajero: {24 billetes de 100 Euros, 32 billetes de 200 Euros, etc. }; la funci n factible(S) o 281

calculara si al a adir el nuevo billete candidato nos pasamos de la cantidad total n solicitada, etc. En las pr ximas secciones vamos a exponer dos problemas cl sicos resueltos o a mediante algoritmos voraces: el problema de la compresi n de cheros usando o c digos de Huffman y el problema de la mochila con fraccionamiento. o

8.3.

El problema de la compresi n de cheros o

Vamos a abordar el problema de la compresi n de cheros de textos (formados o por secuencias de caracteres) mediante el uso de los c digos de Huffman. o Supongamos que tenemos un chero con 100.000 caracteres (solamente aparecen los caracteres a, b, c, d, e, f ) cuyas frecuencias de aparici n son las siguientes: o caracter Frecuencia 103 a 45 b c 13 12 d e 16 9 f 5

Para codicar esas letras en el computador podramos usar en un principio el cl sico c digo ASCII de 8 bits para cada caracter. Sin embargo, como s lo a o o estamos trabajando con 6 letras, podemos codicar cada una de ellas con 3 bits. As establecemos los siguientes c digos para las letras (codicaci n ja): o o caracter C digo Fijo o a 000 b 001 c 010 d 011 e 100 f 101

Con este sistema de codicaci n, el espacio de memoria requerido para cada o caracter es el mismo, 3 bits, por lo que el tama o del chero de 100.000 caracteres n ser 300 Kbits. a Ahora bien, como se puede comprobar en la tabla de frecuencias de aparici n o de cada caracter en el chero, existen unos caracteres que aparecen muchas m s a veces que otros. As pues, si consiguieramos codicar con menos bits los car acteres muy frecuentes conseguiremos reducir considerablemente el tama o del n chero, para esto veremos m s adelante que ser necesario codicar los caraca a teres menos frecuentes con m s bits, pero a n as se conseguir una reducci n del a u a o n mero total de bits del chero. Proponemos una nueva codicaci n de bits para u o los caracteres del chero (codicaci n variable): o caracter C digo Variable o a b 0 101 c 100 d 111 e 1101 f 1100

Con esta nueva codicaci n el chero de 100.000 caracteres ocupar 224 o a Kbits, con lo que hemos conseguido comprimir el chero original. 282

Con este tipo de codicaciones podemos denir un algoritmo de compresi n. o La compresi n ser a partir de la secuencia de bits del chero original, si los o a interpretamos como los c digos de caracteres correspondientes a la codicaci n o o ja vista anteriormente, obtendremos una secuencia de caracteres, si ahora codicamos esos caracteres con el c digo variable tambi n visto anteriormente, obo e tendremos una secuencia de bits, m s corta que la original, pero que la identica a plenamente. Como ejemplo: 000001010
f ijo

abc

variable

0101100

Hemos reducido la secuencia original de 9 bits a 7 bits. El que esta codicaci n variable sirva como esquema de compresi n se debe o o a que la codicaci n variable que hemos usado cumple la propiedad de prejo: o ning n c digo es prejo de otro. Con lo que cada caracter quedar plenamente u o a determinado por su secuencia de bits asociada, sea de la longitud que sea, y a la hora de interpretar una secuencia de bits del chero comprimido, no existir ama big edad, esto es, no habr confusi n en la interpretaci n, sino que identicar a u a o o a una secuencia de caracteres y solo a una. Un caso particular de c digo que cumple o esta propiedad es el c digo de Huffman, utilizado para compresi n de cheros. Eso o tos c digos se generan a partir del chero a comprimir. Para ello existe un algorito mo voraz que crea un arbol binario que se corresponde con el c digo de Huffman o para ese chero. En ese arbol binario las aristas estan etiquetadas con un 0 o con un 1, en las hojas tendremos los caracteres que forman nuestro vocabulario, y el c digo de Huffman para cada caracter se obtendr con la secuencia de etiquetas o a de las aristas que forman el camino desde la raz hasta la hoja correspondiente al caracter. En la gura 8.1 se puede observar el arbol binario correspondiente a la codicaci n propuesta para el ejemplo anterior. o

0 a

1 0 1

0 c

1 b 0 f

1 d 1 e

Figura 8.1: Arbol binario con la codicaci n Huffman para el ejemplo anterior o 283

Lo que pretende el c digo de Huffman es eliminar la redundancia en la codio caci n de un mensaje. o Para construir un arbol como el de la gura 8.1 necesitaremos tener determinado el conjunto de smbolos C y el conjunto de frecuencias F . Adem s, necesitare a mos un montculo Q, en el que guardaremos el conjunto de frecuencias formando un MINHEAP 1 . A partir de ah, extraeremos cada vez las dos menores frecuencias del montculo Q y colgaremos los nodos correspondientes a ellas como hijo izquierdo e hijo derecho respectivamente de un nuevo nodo que crearemos, el nuevo nodo creado tendr asociada como frecuencia la suma de las frecuencias de sus a hijos y se insertar la frecuencia del nuevo nodo en el montculo Q. As hasta que a no queden m s nodos que juntar. Esto indicar que el arbol que hemos ido cona a struyendo ya incluye a todos los smbolos. Cuando busquemos un determinado smbolo en ese arbol, al recorrer el camino desde la raz al nodo correspondiente al smbolo, si descendemos por un hijo izquierdo, esto supondr un bit 0 y si a descendemos por un hijo derecho, supondr un bit 1. a El algoritmo que, a partir de un conjunto de smbolos (que constituye el vo cabulario) y un conjunto de frecuencias asociadas a cada uno de los smbolos, crea este arbol, es el siguiente: Algoritmo Huffman(C, F ) { for (x C, fx F ) { crear hoja(x,fx ); } Q F ; build heap(Q); for (i=1;i< |C|;i++) { z = crear nodo(); z.hizq = nodo de(minimo(Q)); extract min(Q); z.hder = nodo de(minimo(Q)); extract min(Q); z.frec = z.hizq.frec + z.hder.frec; insertar(Q, z.frec); } return(nodo de(minimo(Q))); }
1

Ver pr ctica de montculos a

284

El coste del algoritmo es O(n log n), siendo n la talla del vocabulario, es decir, el n mero de caracteres diferentes que hay. u Vemos ahora la traza de c mo obtiene el algoritmo el arbol binario propuesto o para el ejemplo anterior. Partimos del conjunto de smbolos con sus frecuencias asociadas y el montculo de las frecuencias:

12

12

13

16

45

13

45

16

12

12

13 0 f 5

14 1 e 9

16

45

13

16

45

14

14

14 0 f 5 1 e 9

16 0 c 12

25 1 b 13

45

25

16

45

285

25 0 c 12 1 b 13 0 f 5 0 14

30 1 d 1 e 9 16

45

25 45

30

45 0 25 0 c 12 1 b 13

55 1 30 0 14 0 f 5 1 e 9 55 1 d 16 45

100 0 a 45 0 25 0 c 12 1 b 13 0 f 5 0 14 1 e 9 1 55 1 30 1 d 16

100

Que se corresponde con el arbol de la gura 8.1.

286

8.4.

El problema de la mochila con fraccionamiento

Supongamos que tenemos una mochila con una capacidad M (en peso), y tenemos tambi n N objetos para incluir en ella. Cada objeto tiene un peso pi y un e benecio bi , 1 i N . Problema: C mo llenar la mochila de forma que el benecio total de los elemeno tos que contenga sea m ximo? Suponemos que los objetos se pueden fraccionar, a esto es, se puede introducir en la mochila 1/3 de un objeto, con el peso y benecio correspondientes a ese 1/3. Tampoco se puede sobrepasar la capacidad de la mochila. Planteamiento del problema: Los datos del problema son: la capacidad M y los N objetos con sus pesos asociados (p1 , . . . , pN ) y sus benecios asociados (b1 , . . . , bN ). El resultado que queremos obtener es una secuencia de N valores (x1 , . . . , xN ), donde xi cumple 0 xi 1, para 1 i N . Esto es, xi indica la parte fraccional que se toma del objeto i. Se desea maximizar el benecio N bi xi (la suma de los benecios correi=1 spondientes a la parte fraccional de cada objeto incluido) con la restricci n o de que los objetos quepan en la mochila N pi xi M i=1 Algunos ejemplos de soluciones factibles para el caso en que la capacidad de la mochila sea M = 20, los pesos de los objetos sean (18, 15, 10) y los benecios asociados sean (25, 24, 15), son: Soluci n o (1/2, 1/3, 1/4) (1, 2/15, 0) (0, 2/3, 1) (0, 1, 1/2) Peso total 16.5 20.0 20.0 20.0 Benecio total 24.25 28.20 31.00 31.50

Como es evidente, la soluci n optima deber ser una que llene la mochila al o a completo. Nuestro objetivo nal es maximizar el benecio total de los elementos de la mochila. Para ello, aplicando una estrategia voraz, aplicaremos un criterio de optimizaci n local con la idea de que este criterio obtenga una soluci n global lo m s o o a optima posible. Para nuestro problema podemos denir tres criterios de selecci n o de objetos: Escoger a cada iteraci n el elemento de mayor benecio . o 287

Escoger a cada iteraci n el elemento de menor peso . o Escoger a cada iteraci n el elemento de mayor relaci n benecio/peso . o o La idea es que cogeremos elementos completos (es decir, xi =1) mientras no sobrepasemos el peso M permitido en la mochila. Cuando no podamos coger m s a elementos completos, cogeremos del siguiente objeto que cumpla el criterio de selecci n, la parte fraccional correspondiente para llenar la mochila hasta el tope. o El algoritmo recibe el vector de pesos, p, el vector de benecios, b, el tama o n de la mochila, M y el n mero de objetos, N . Adem s, crea un conjunto C que u a representa los objetos que hay para elegir asign ndole un n mero identicador a a u cada uno y genera el vector solucion. El algoritmo para resolver el problema de la mochila fraccional es: Algoritmo Mochila-fraccional(p, b, M, N ) { C = {1, 2, . . . , N}; for (i = 1; i <= N ; i + +) solucion[i] = 0; while ((C = ) && (M > 0)) { /* i = elemento de C con m ximo benecio b[i]; */ a /* i = elemento de C con mnimo peso p[i]; */ i = elemento de C con m xima relaci n b[i]/p[i]; a o C = C - {i}; if (p[i] M ) { solucion[i] = 1; M = M p[i]; } else { solucion[i] = M/p[i]; M = 0; } } return(solucion); } Se puede demostrar que si los objetos se seleccionan por orden decreciente del criterio benecio/peso, el algoritmo Mochila-fraccional encuentra una soluci n o optima al problema (ver [Brassard]). Ejemplo: Observa como se obtiene la soluci n con cada uno de los tres criterios o de selecci n de elementos propuestos para un problema donde M = 50, los pesos o son p = (30, 18, 15, 10) y los benecios son b = (25, 13, 12, 10): 288

1. Criterio: seleccionar objeto de mayor benecio bi . Iteraci n o 1 2 3 M 50 20 2 0 C {1,2,3,4} {2,3,4} {3,4} {4} Soluci n o 0 0 0 0 1 0 1 2/15 Benecio 0 0 0 0 25 + 13 + (2/15) 12 = 39.6

0 1 1 1

2. Criterio: seleccionar objeto de menor peso pi . Iteraci n o 1 2 3 4 M 50 40 25 7 0 C {1,2,3,4} {1,2,3} {1,2} {1} Soluci n o 0 0 0 0 0 0 0 0 1 0 1 1 7/30 1 1 Benecio 0 1 1 1 1 (7/30) 25 + 13 + 12 + 10 = 40.83

3. Criterio: seleccionar objeto de mayor benecio unitario bi /pi = (5/30, 13/18, 12/15, 10/10). Iteraci n o 1 2 3 M 50 40 10 0 C {1,2,3,4} {1,2,3} {2,3} {2} Soluci n o 0 0 0 0 0 0 0 10/15 Benecio 0 1 1 1

0 0 1 1

25 + (10/15) 12 +10 = 42.99

Coste temporal del algoritmo de la Mochila fraccional El bucle for que inicializa el vector de solucion en el algoritmo de Mochilafraccional tiene un coste de O(N ). Por otro lado el bucle mientras selecciona uno de los N posibles objetos a cada iteraci n y como mucho realizar tantas iteraciones como objetos existan, es o a decir realizar O(N ) iteraciones como m ximo. a a La operaci n de seleccionar el elemento de C seg n el criterio elegido, en el o u caso en que C estuviera representado en un vector, tendr un coste O(N ). Como a esta operaci n se encuentra dentro del bucle mientras, el coste total de este bucle o ser O(N 2 ). a Sumando el coste de inicializar el vector soluci n y el del bucle mientras o obtenemos que el coste total de Mochila-fraccional es: O(N + N 2 ) O(N 2 ). 289

Sin embargo, podemos realizar una implementaci n m s eciente del algorito a mo, si ordenamos previamente los objetos conforme a la relaci n b/p (o el criterio o escogido), el coste de la selecci n es O(1) y ordenar los objetos tendr un coste o a O(N log N ), con lo que el coste total del algoritmo es: O(N + N + N log N ) O(N log N ).

290

8.5.

Ejercicios

Ejercicio 1: -Se tiene un disco de tama o T y N cheros que se desean grabar en el disco. n El tama o total de los cheros puede ser mayor que T y por tanto puede que no n quepan todos. Se deseara un algoritmo voraz que decidiera qu cheros almace e nar en el disco, suponiendo que: Se dispone de un vector t[1 . . . N ] con los tama os de los N cheros. n La soluci n se da como un vector s[1 . . . N ], en el que s[i] = 1 indica que o el chero i- simo se almacena en el disco, y s[i] = 0 en caso contrario. e Se desea maximizar el coeciente de ocupaci n del disco denido como: o s[1]t[1] + s[2]t[2] + . . . + s[N ]t[N ] T Para ello, se propone el siguiente algoritmo: Ordenar los cheros por orden decreciente de tama o. Suponiendo los cheros numerados de acuerdo con dicho n orden, desde el primero hasta el ultimo, se van grabando mientras quede espacio en el disco. Si un determinado chero no cabe, se descarta y se pasa al siguiente. Se pide: a) Discutir el coste temporal del algoritmo propuesto. b) Dar un contraejemplo para demostrar que este algoritmo no siempre obtiene la soluci n optima. o Soluci n: o a) El coste temporal ser el mismo que para el algoritmo de la mochila (en su a versi n no fraccional, donde los objetos no se pueden fraccionar), esto es o debido a que el problema y el algoritmo de resoluci n son extremadamente o similares. La ordenaci n del vector t de los tama os de los cheros por orden deo n creciente de tama o tendr un coste de O(N log N ) (por ejemplo, usando n a quicksort). Despues habr que realizar un bucle que vaya recorriendo el vector t hasta a que o no queden cheros por comprobar (se han grabado todos los cheros disponibles o ninguno de los que quedan por graban cabe en el espacio restante en disco) o se haya llegado a la ocupaci n m xima del disco (se o a 291

haya llenado todo el disco). Este bucle tendr un coste m ximo correspona a diente al tama o del vector t, esto es, O(N ). n Sumando todo, el coste nal es O(N + N log N ) O(N log N ). b) Un posible contraejemplo podra ser este: Supongamos que tenemos un disco con capacidad T = 100 Mb. y tenemos cheros de tama o (42, 40, 33, 25, 12, 10). En primer lugar ordenamos los n cheros de mayor a menor, con lo que obtenemos el siguiente vector de tama os ordenado: n
1 2 3 4 5 6

42 40 33 25 12 10 Y ahora vamos recorriendo el vector anterior y grabando cheros en el disco conforme a la estrategia dictada anteriormente, para ello usamos un bucle for(i=1;i<=6;i++): i=1
1 2 3 4 5 6

s= 1 0 0 T = 100 - 42 = 58 i=2
1 2 3

s= 1 1 0 T = 58 - 40 = 18 i=3
1 2 3

s= 1 1 0 0 0 0 como t[3] = 33, no cabe en el disco i=4


1 2 3 4 5 6

s= 1 1 0 0 0 0 como t[4] = 25, no cabe en el disco

292

i=5
1 2 3 4 5 6

s= 1 1 0 T = 18 - 12 = 6 i=6
1 2 3

s= 1 1 0 0 1 0 como t[6] = 10, no cabe en el disco NO QUEDAN MAS FICHEROS El coeciente de ocupaci n que obtenemos con esta soluci n es: o o Coeciente de ocupaci n = o 94 42 + 40 + 12 = = 0,94 100 100

Sin embargo la soluci n optima sera la siguiente: o


1 2 3 4 5 6

s=

cuyo coeciente de ocupaci n es: o Coeciente de ocupaci n = o que es el m ximo posible. a 42 + 33 + 25 100 = =1 100 100

293

Ex menes de la asignatura a

294

Estructuras de Datos y Algoritmos


Febrero 2002 Examen de Teora: SOLUCIONES
Departamento de Sistemas Inform ticos y Computaci n a o Escuela Polit cnica Superior de Alcoy e

Ejercicio: (1 punto) -Sup n que tienes que implementar la gesti n de la cola de clientes de un taller o o mec nico. Lo normal sera atender a los clientes en el orden en el que van llegando a al taller. Sin embargo, el due o del taller quiere tener en consideraci n la prisa n o que un cliente pueda tener por recoger su auto. Con lo que habr que permitir a asignar una prioridad para cada cliente seg n la prisa que tenga, por ejemplo, un u comercial necesita urgentemente el coche y no puede esperar mucho porque si no puede viajar, pierde ventas, mientras que un jubilado que s lo lo usa para pasear o al perro, puede esperar mucho m s tiempo. a a) qu estructura de datos (TAD) propones utilizar para poder gestionar esta cola e de clientes. Raz nalo.(0.5 puntos) o b) Nombra dos operaciones (las que quieras) asociadas al TAD que has denido en el apartado anterior y explica brevemente en qu consisten. (0.5 puntos) e Soluci n: o a) La estructura m s adecuada sera una cola con prioridades, reri ndonos con a e esto a una estructura de datos cola tradicional, donde los elementos se insertan por el nal de la cola, pero pueden avanzar tanto hacia la cabeza como su prioridad les permita. Para ello, cada uno de los elementos de la cola deber estar caracterizado por su prioridad. La unica operaci n distinta del a o TAD cola cl sico es la de insertar, pues debe comparar con los elementos de a la cola empezando desde el nal hasta llegar a la posici n de la cola donde o su prioridad indique que debe insertarse. Los elementos se extraen como siempre por la cabeza de la cola, de hecho, si nos jamos, tal y como se ha denido el TAD, el elemento que se encuentre en la cabeza de la cola ser el a que tenga m s prioridad de toda la cola. a

295

b) A gusto del consumidor: las operaciones posibles asociadas a este TAD seran las mismas que para el TAD cola visto en clase, excepto la operaci n de o insertar, que incluira la prioridad adem s del elemento y tendra que inser a tar en la posici n que dicha prioridad le indicase. Por lo dem s, el resto de o a operaciones sera igual.

Ejercicio: (4 puntos) -Supongamos que estamos desarrollando el software de la agencia matrimonial Crazy Casanova S.A.. Para ello tenemos que mantener una lista con las chas de los clientes. Las chas de los clientes van a tener los siguientes datos: Una cadena con el nombre y apellidos del cliente. Sexo del cliente (reri ndonos a su g nero, claro est ). e e a Edad. Una cadena describiendo sus hobbies. Ejemplos de chas pueden ser estas: Nombre: Marieta Piruleta Sexo: Mujer Edad: 27 Hobbies: Cine rom ntico a Nombre: Marianico Cocoliso Sexo: Hombre Edad: 35 Hobbies: Cazar saltamontes

Nuestro cometido va a consistir en solucionar problemas bastante sencillos. Supongamos que dada la lista de clientes tenemos una posici n pos que indica o en cierto modo el nodo activo en un determinado momento, esto es, se ala la n cha de un cliente. Existe una interfaz a la lista de clientes, que muestra por la pantalla el cliente activo (se alado por pos) en el momento actual. n Queremos conseguir que la/el secretaria/o de la agencia pueda cambiar la cha de cliente mostrada por la pantalla a la cha siguiente o a la anterior de la lista pulsando AvPag o RePag respectivamente. Para ello, necesitamos denir una nueva estructura de datos lista similar a la vista en clase pero a la cual podamos aplicarle un recorrido hacia adelante o hacia atr s. a Un esquema gr co de la nueva estructura lista utilizando memoria din mica a a y variables enlazadas sera: 296

nodo centinela primer ultim ficha del cliente ficha del cliente ficha del cliente

pos

donde al igual que en la representaci n din mica enlazada vista en clase, dada una o a posici n p, el elemento correspondiente a dicha posici n, est en el siguiente nodo o o a al que apunta p, o sea, en p->sig. Adem s, tambi n existe un nodo centinela al a e principio de la lista. Esta estructura se conoce normalmente con el nombre de lista doblemente enlazada utilizando memoria din mica. a Se pide: a) Dar la denici n del tipo de datos lista necesario para montar este tipo de o lista doblemente enlazada usando memoria din mica en lenguaje C. (0.50 a puntos) b) Con la denici n de la estructura lista doblemente enlazada del apartado a). o Escribir en lenguaje C la operaci n o posicion siguiente(lista *l, posicion pos) que devuelve la posici n siguiente a la posici n pos en la lista l. (0.25 o o puntos) c) Con la denici n de la estructura lista doblemente enlazada del apartado a). o Escribir en lenguaje C la operaci n o posicion previa(lista *l, posicion pos) que devuelve la posici n previa a la posici n pos en la lista l. (0.25 puntos) o o d) Utilizando las operaciones de los apartados b) y c), escribir una funci n o posicion situa_ficha(lista *l, posicion pos, int tecla) que recibir en el par metro entero tecla un 0 si se ha pulsado AvPag o un a a 1 si se ha pulsado RePag. Bas ndose en este c digo de control, la funci n a o o debe devolver la posici n p siguiente a la posicion pos de la lista si se o 297

ha pulsado AvPag o devolver la posici n previa si se ha pulsado RePag. (1 o punto) e) Suponer ahora que tenemos las siguientes operaciones denidas para nuestra estructura de datos lista doblemente enlazada: lista *crearl() lista *insertar(lista *l, ficha *f, posicion pos) lista *borrar(lista *l, posicion pos) ficha *recuperar(lista *l, posicion pos) int vacial(lista *l) posicion fin(lista *l) posicion principio(lista *l) posicion siguiente(lista *l, posicion pos) posicion previa(lista *l, posicion pos) int compatible(lista *l, posicion pos1, posicion pos2) esta operaci n devuelve verdadero si los clientes cuyas chas son o las correspondientes a las posiciones pos1 y pos2 de la lista son compatibles para emparejarlos. Falso en caso contrario. Se pide escribir una funci n o lista *encuentra_compatibles(lista *l, posicion pos) que dada una lista l y una posici n pos de la lista, crea y devuelve una o nueva lista donde est n incluidas todas las chas de aquellas posiciones de a la lista l compatibles con la de la posici n pos. (2 puntos) o

298

Soluci n: o a) /* definimos por un lado la estructura ficha */ /* para las fichas de clientes */ typedef struct _ficha { char nombre[60]; /* cadena del nombre */ char sexo[10]; /* cadena del sexo */ int edad; /* edad en anyos */ char hobbies[100]; /* cadena de hobbies */ } ficha; /* por otro lado la estructura para la lista */ /* doblemente enlazada de clientes */ typedef struct _lnode { ficha f; /* la ficha del cliente */ struct _lnode *siguiente; /* puntero al siguiente nodo */ struct _lnode *previo; /* puntero al nodo previo */ } lnode; typedef lnode *posicion; /* las posiciones son punteros */ typedef struct { posicion primero,ultimo; /* punteros a los nodos primero y ultimo */ } lista; b) La implementaci n de esta operaci n es id ntica a la vista en clase para una o o e estructura lista simple. posicion siguiente(lista *l, posicion pos) { return(pos->siguiente); /* devolvemos la posicion siguiente a pos */ } c) Esta operaci n consistir en consultar y devolver el puntero al nodo previo al o a nodo al que est apuntando pos. a

299

posicion previa(lista *l, posicion pos) { return(pos->previo); /* devolvemos la posicion previa a pos */ } d) En la funci n basta comprobar cual es el valor de la variable tecla y devolver o la posici n correspondiente. o posicion situa_ficha(lista *l, posicion pos, int tecla) { if (tecla==0) return(siguiente(l,pos)); else if (tecla==1) return(previa(l,pos)); else printf("ERROR\n"); return(NULL); } La llamada desde el programa principal sera de esta manera: pos = situa_ficha(l, pos, tecla); e) Consistir en recorrer la lista de clientes e ir creando otra lista nueva con los a clientes que sean compatibles con el de la posici n pos: o lista *encuentra_compatibles(lista *l, posicion pos) { lista *nueva_l; posicion act; posicion new; ficha *fich; nueva_l = crearl(); act = principio(l); new = principio(nueva_l); while (act != fin(l)) { if (compatible(l, pos, act) && (pos!=act)) { fich = recuperar(l, act); nueva_l = insertar(nueva_l, fich, new); new = siguiente(nueva_l, new); } act = siguiente(l, act); } return(nueva_l); }

300

Ejercicio: (1 punto) -Sup n que eres el/la profesor/a de la asignatura, debido a ello tienes disponible o una lista de chas de alumnos ordenada alfab ticamente por apellidos. Las chas e contienen otros campos, como por ejemplo la nota de la asignatura, la cual ya est puesta. Resulta que ahora queremos saber cu l es el alumno que ha obtenido a a a la 3 mejor nota. qu algoritmo puedes aplicar para saber cu l es el alumno que e a ha obtenido la 3a mejor nota (no implementes el algoritmo, solo d cual sera)? Razona tu respuesta. (1 punto) Soluci n: o Como es un lista, podemos considerarla como un vector donde se guardaran en una determinado posici n cada uno de los alumnos ordenados por su apellido. o Podramos optar, pues, por una representaci n vectorial de la lista y ahora el algo o ritmo m s correcto (o adecuado) sera el desarrollado en clase para la b squeda del a u k- simo menor elemento mediante Divide y Vencer s, pero en este caso aplicado e a a la b squeda del k- simo mayor elemento. De hecho, este algoritmo debera apliu e carse al campo de nota de cada cha, puesto que buscamos a un alumno por su nota. Para esto se debe cambiar la implementaci n del algoritmo partition visto o en clase de manera que en el subvector izquierdo queden los elementos mayores o iguales que el pivote y en el subvector derecho queden los elementos menores o iguales que el pivote: esto se conseguira cambiando las siguientes lneas del algoritmo partition } while (A[j]>x) por las siguientes lneas donde se han cambiado los signos de comparaci n: o } while (A[i]<x) del algoritmo de clase por } while (A[j]<x) y } while (A[i]>x)

301

Ejercicio: (1 punto) -Dado el algoritmo Quicksort estudiado en clase, del cual una codicaci n posible o es: void quicksort(int *A, int l, int r) { int q; if (l<r) { q = partition(A, l, r); quicksort(A, l, q); quicksort(A, q+1, r); } } y considerando tambi n el algoritmo de Inserci n directa visto en clase, cuyo e o perl era: void insercion_directa(int *A, int l, int r) Se pide: implementar la optimizaci n propuesta en clase para el Quicksort o mediante la cual se trataban los problemas inferiores a una cierta talla t, con un m todo de ordenaci n directo, en este caso deber utilizarse el m todo de insere o a e ci n directa. (1 punto) o Soluci n: o Hay que tener en cuenta que un nuevo par metro del algoritmo sera la talla t a a partir de la cual se ha de aplicar inserci n directa. El algoritmo Quicksort quedara o de esta manera: void quicksort(int *A, int l, int r, int t) { int q; if ((r-l+1)<t) insercion_directa(A,l,r); else { q = partition(A, l, r); quicksort(A, l, q, t); quicksort(A, q+1, r, t); } }

302

Ejercicio: (3 puntos) -Tenemos un vector A de n enteros positivos que se ajusta al perl de una curva c ncava y se desea encontrar el mnimo de esta curva en este intervalo mediante o un algoritmo divide y vencer s con coste logartmico, esto es, O(log n) (con esto a se mejorar el algoritmo directo de b squeda del mnimo de coste O(n)). a u Para ello vamos a aprovechar el perl de la curva. En las siguientes gr cas a podemos observar la estrategia a seguir para realizar el algoritmo de b squeda: u Como nuestro algoritmo va a ser divide y vencer s y estamos buscando dentro a de un intervalo [l,r] del vector que representa la curva, pues vamos a obtener el punto medio k del intervalo, comprobaremos el valor de la curva para ese punto y a partir de ah decidiremos si hay que seguir buscando en la parte del intervalo a la izquierda del punto medio o en la parte derecha. Fij ndonos podemos distinguir a 3 casos en la b squeda: u

10

0 0 2 4 6 8 10 12

A[k-1] > A[k] < A[k+1] encontrado mnimo.

8 "minik.dat" "imk.dat"

303

10

0 0 2 4 6 8 10 12 0 0 2 4 6 8 10 12

A[k-1] < A[k] < A[k+1] buscar mnimo en [l,k-1]

A[k-1] > A[k] > A[k+1] buscar mnimo en [k+1,r] Se pide: escribir la codicaci n en lenguaje C del algoritmo. (3 puntos) o

2 2

4 4

6 6

8 "miniki.dat" "imki.dat" 8 "minikd.dat" "imkd.dat"

10

304

Soluci n: o La codicaci n de este algoritmo Divide y Vencer s de manera recursiva sera: o a int min_concava(int *A, int l,r) { int k; if (l<r) { k = (int ) (l+r)/2; if ((A[k-1]>A[k]) && (A[k]<A[k+1])) min=A[k]; /* el minimo en k */ else if ((A[k-1]<A[k]) && (A[k]<A[k+1])) min = min_concava(A,l,k-1); /* buscamos en la parte izquierda */ else min = min_concava(A,k+1,r); /* buscamos en la parte derecha */ } else min=A[l]; return(min); } Una versi n iterativa del algoritmo equivalente a la anterior sera: o int min_concava(int *A, int l,r) { int k; while (l<r) { k = (int ) (l+r)/2; if ((A[k-1]>A[k]) && (A[k]<A[k+1])) return(A[k]); /* el minimo en k */ else if ((A[k-1]<A[k]) && (A[k]<A[k+1])) r = k-1; /* buscamos en la parte izquierda */ else l = k+1; /* buscamos en la parte derecha */ } return(A[l]); }

305

Estructuras de Datos y Algoritmos


Junio 2002 Examen de Teora: SOLUCIONES
Departamento de Sistemas Inform ticos y Computaci n a o Escuela Polit cnica Superior de Alcoy e Ejercicio : (1.5 puntos) - Dadas las siguientes deniciones de tipos y variables para representar arboles binarios de b squeda: u typedef ... tipo_baseT; typedef struct snodo { tipo_baseT clave; struct snodo *hizq, *hder; } abb; abb *T; Se pide escribir una versi n recursiva del algoritmo visto en clase de teora o que obtiene el mnimo de un arbol binario de b squeda. u abb *minimo(abb *T)

Soluci n: o El esquema para la funci n que obtiene el mnimo de un arbol binario de o b squeda de manera recursiva es sencillo. Para buscar el mnimo siempre debamos u buscar por el hijo izquierdo del nodo actual en el caso de que lo tuviera, si no tena hijo izquierdo es porque el era el mnimo. El esquema recursivo consistir en: si un nodo tiene hijo izquierdo, el problea ma se reduce a buscar el mnimo de su sub rbol izquierdo, en el caso de que no a tenga hijo izquierdo, el es el mnimo. El c digo de la funci n es: o o

306

abb *minimo(abb *T) { abb *min=NULL; if (T!=NULL) if (T->hizq != NULL) min = minimo(T->hizq); else min = T; return(min); }

307

Ejercicio : (2 puntos) -Resolver las siguientes cuestiones relacionadas con el algoritmo build_heap(): a) Cu l ser el coste temporal del algoritmo build_heap() al aplicarlo sobre a a un vector A de talla n, si los elementos del vector est n ordenados en orden a creciente? Pon un peque o ejemplo. n (1 punto). b) Y si estuvieran ordenados en orden decreciente? Pon un peque o ejemplo. (1 n punto). Soluci n: o a) Vemos primero un peque o ejemplo de c mo se aplicara build_heap() n o sobre un vector cuyos elementos est n ordenados en orden creciente. a

6 8 9 build_heap(A,5)
3 3 9

heapify(A,2)

heapify(A,1)
9

heapify(A,2)

Como podemos ver en el ejemplo (y a partir de el, generalizar), construir un montculo a partir de un vector ordenado crecientemente tendr un coste a O(n). Vamos a analizarlo con un poco m s de detenimiento: a 308

Como el vector inicial estaba ordenado crecientemente, a partir de el obten emos directamente un arbol binario completo donde para cada nodo interno, la clave del nodo va a ser menor que la claves de todos los nodos que est n en sus sub rboles. Por ello, cada vez que apliquemos heapify() e a sobre un nodo interno i, el coste de heapify() va a ser el m ximo posia ble para ese nodo, esto es, O(hi ), siendo hi la altura del nodo i. Con esta situaci n llegamos al mismo an lisis de costes visto en clase de teora para o a build_heap(), por lo que aplicando el mismo desarrollo de clase llegamos a que el coste de build_heap() cuando el vector inicial est ordea nado crecientemente es O(n). Siendo n el n mero de elementos del vector u y por tanto el n mero de nodos del montculo. u b) Vemos ahora un ejemplo de aplicaci n de build_heap() sobre un vector o cuyos elementos est n ordenados en orden decreciente. a

8 6 5 build_heap(A,5)
11 11 11

11

heapify(A,2)

heapify(A,1)

Como se ve en el ejemplo, y se intuye que va a ocurrir en cualquier caso en el que el vector est ordenado decrecientemente, el coste temporal de e build_heap() va a ser igual que para el caso anterior O(n). Esto es debido a que aunque cualquier vector ordenado decrecientemente va a ser un montculo, tal y como se ha denido el algoritmo build_heap() (ver apuntes teora) se ejecuta un bucle que recorre desde la posici n n del o 2 vector hasta la posici n 1 (la raz) del montculo) aplicando heapify() o a cada posici n. Si nos jamos, en un vector ordenado decrecientemente o para cualquier elemento en una posici n i, tendremos que los elementos en o las posiciones 2i y 2i + 1 ser n menores que el, por lo que heapify() a detectar que la condici n de montculo se cumple y por ello cada llamada a a o 309

heapify() tendr un coste O(1). As pues, tendremos que build_heap() a realiza n llamadas de coste O(1), con lo que el coste total de build_heap() 2 ser O(n). a

Ejercicio : (1.5 puntos) -Considerando un MF-set con una representaci n de arboles mediante apuntao dores al padre, y, suponiendo que utilizamos una estructura como la vista en clase de teora donde el MF-set se representa con un vector en el que en cada posici n o i se almacena el ndice del elemento que es padre del elemento i. a) qu estrategia propones para hallar el n mero de clases de equivalencia (sube u conjuntos) existentes en el MF-set? Describe el algoritmo que aplicaras. (0.75 puntos) b) Y qu algoritmo aplicaras si adem s quisieras conocer cu les son los repree a a sentantes de las clases de equivalencia del MF-set? Pon un peque o ejemn plo de MF-set representado mediante un vector e indica cu ntas clases de a equivalencia tiene y cu les son sus representantes. (0.75 puntos). a Soluci n: o a) La estrategia a seguir sera usar una variable como contador del n mero de u subconjuntos que hay. Inicializamos esta variable a 0. Y ahora hacemos un recorrido a lo largo del vector que representa al MFset y comprobamos para cada componente cual es su padre, es decir, comprobamos el valor de cada componente. En el caso de que el padre de un elemento sea el mismo (esto es, V [i] == i) entonces quiere decir que es un representante de una clase de equivalencia, con lo que habr una clase de a equivalencia m s que las que llevamos contadas, as pues, incrementaremos a en 1 el contador del n mero de subconjuntos y seguiremos recorriendo el u resto del vector. b) El algoritmo a aplicar sera casi id ntico al descrito en el apartado a), realizar e un recorrido sobre el vector que representa al MF-set y comprobar quien es el padre de cada elemento. En el caso de que el padre de un elemento sea el mismo (esto es, V [i] == i) entonces quiere decir que es un representante de una clase de equivalencia y podramos imprimirlo o guardarlo donde se necesite. Un ejemplo de MF-set representado mediante un vector sera: 310

6
1 2 3 4 5 6 7

Que tiene 3 clases de equivalencia cuyos representantes son: 1, 3 y 6.

Ejercicio : (3 puntos) -Sea G = (V, E) un grafo no dirigido con n > 0 nodos. Dado que la relaci n ser o dos nodos mutuamente conectados es reexiva, sim trica y transitiva, se pueden e calcular las componentes conexas de un grafo no dirigido utilizando un MF-set de la siguiente forma: 1. Se inicializa el MF-set con n subconjuntos disjuntos: cada uno de los subconjuntos contiene un v rtice del grafo. Esto indicar que, inicialmente, los e a v rtices del grafo son inalcanzables entre s. e 2. Para cada arco (u, v) E, si u y v pertenecen a subconjuntos disjuntos, se unen los subconjuntos disjuntos a los que pertenecen u y v. Cuando el algoritmo nalice, cada una de las componentes conexas que contenga el grafo G estar representada mediante un subconjunto disjunto. Y cada a subconjunto disjunto contendr aquellos v rtices que pertenecen a la misma coma e ponente conexa. a) Se pide escribir un algoritmo en lenguaje C int conexo_grafo(grafo *G); que siguiendo esta estrategia determine si un grafo no dirigido es conexo. La funci n debe devolver un 1 si el grafo es conexo y 0 si no lo es. o Supondremos que el grafo se representa mediante una matriz de adyacencia utilizando esta denici n: o 311

#dene MAXVERT 1000 typedef struct{ int talla; int A[MAXVERT][MAXVERT]; } grafo; (2 puntos) b) Se pide tambi n analizar cu l sera el coste temporal del algoritmo si las ope a eraciones void union(int i, int j, int *mfset); y int buscar(int i, int *mfset); del MF-set se realizan aplicando los heursticos uni n por rango y compre o si n de caminos. (1 punto) o Soluci n: o a) Necesitaremos denir un vector de enteros que utilizaremos para representar el MF-set. Debido a que la representaci n usada para el grafo es mediante o matriz de adyacencia, haremos un recorrido por toda la matriz, y para cada arista que conecte dos v rtices i y j (es decir G > A[i][j] == 1) comproe baremos si ya est n unidas las componentes conexas que los contienen, y si a no es as, las uniremos en el MF-set. Por ultimo, una vez el MF-set contiene los v rtices agrupados en subconjuntos que representan las diferentes come ponentes conexas, debemos realizar un recorrido por el MF-set contando cuantas componentes conexas existen, en el caso en que haya un n mero de u componentes diferente a 1, el grafo no ser conexo. a Siguiendo la estrategia propuesta en el enunciado podemos escribir la siguiente funci n en C: o

312

/* La funcion devuelve 1 si el grafo es conexo y */ /* 0 si no lo es. */ int conexo_grafo(grafo *G) { int mfset[MAXVERT]; /* El MFset. */ int i,j; /* Para recorrer las aristas del grafo. */ int cont; /* Contador de componentes conexas. */ /* Inicializar MFset. */ for (i=1;i<=G->talla;i++) mfset[i]=i; /* Comprobamos que vertices unen todas las aristas. */ for (i=1;i<=G->talla;i++) for (j=1;j<=G->talla;j++) if (G->A[i][j] == 1) /* Hay arista entre i y j. */ if (buscar(i,mfset) != buscar(j,mfset)) union(i,j,mfset); /* Comprobamos cuantas componentes conexas hay. */ cont = 0; for (i=1;i<=G->talla;i++) if (mfset[i] == i) cont++; if (cont != 1) return(0); /* NO ES CONEXO. */ else return(1); /* ES CONEXO. */ } b) Considerando que las operaciones buscar(i,mfset) y union(i,j,mfset) tienen un coste constante debido a los heursticos empleados con el MF-set, como el grafo G tiene n v rtices, el coste de las diferentes operaciones que e se realizan en el algoritmo es: Inicializar el MF-set tiene un coste O(n) (un bucle for). Comprobar a qu componente conexa (clase de equivalencia) pertenece e cada v rtice tiene un coste O(n2 ) (dos bucles for anidados). e Contar cuantas componentes conexas tiene el grafo tiene un coste O(n) (un bucle for). Por tanto el coste es: O(n + n2 + n) O(n2 ).

313

Ejercicio : (2 puntos) -Dados una cinta magn tica de longitud L y n cheros f1 , f2 , . . . , fn de longitudes e l1 , l2 , . . . , ln , Cu l sera la estrategia voraz de almacenamiento de cheros que a minimiza el tiempo medio de acceso? Asumimos: Todos los cheros caben dentro de la cinta magn tica. e Cuando se va a realizar cualquier acceso, la cinta est siempre al principio. a Si el orden de almacenamiento de cheros es i1 , i2 , . . . , in , el tiempo necesario para acceder al programa ij ser tj = j1 lik , siendo lik la longitud a k=1 del chero ik , 1 k < j. Si la probabilidad de acceder a cualquier programa es la misma, el tiempo 1 medio de acceso se dene como T M A = n n tj . j=1 Soluci n: o Observando la expresi n para el tiempo medio de acceso: o 1 TMA = n
n

tj
j=1

con esto, minimizar el T M A signica que hay que minimizar tj . La expresi n o para tj es:
j1

tj =
k=1

lik

si el orden de almacenamiento de cheros es i1 , i2 , . . . , in . Si todos los cheros ik , 1 k < j, que est n almacenados delante del chero ij tienen un tama o menor a n que ij , entonces el tiempo de acceso al chero ij ser el menor posible, esto es, a tj ser el menor posible. As pues, la estrategia voraz para almacenar los cheros a ser almacenarlos en orden no decreciente de tama o. Para ello, los ordenaremos a n de menor a mayor por tama o y los almacenaremos en ese mismo orden, con esto n conseguiremos reducir el tiempo de acceso tj para cada chero ij , 1 j n, con lo que se reducir el T M A. a

314

Estructuras de Datos y Algoritmos


Enero 2003 Examen: SOLUCIONES
Departamento de Sistemas Inform ticos y Computaci n a o Escuela Polit cnica Superior de Alcoy e

Ejercicio : (1 punto) -Suponiendo que trabajamos con una lista de enteros con una representaci n eno lazada de listas con variable din mica como la vista en clase y cuya denici n de a o tipos es: typedef struct _lnodo { int e; struct _lnodo *sig; } lnodo; /* Un elemento. */ /* Puntero al siguiente nodo. */

typedef lnodo *posicion; /* Cada posicion es un puntero. */ typedef struct { posicion primero, ultimo; } lista;

/* primer y ultimo nodos. */

Donde dada una posici n p, su elemento correspondiente se encuentra almao cenado en el nodo apuntado por p->sig. Escribe una funci n en lenguaje C con el perl o lista *intercambia(lista *L, posicion pos1, posicion pos2) que devuelva la lista L modicada de manera que haya intercambiado los elementos correspondientes a las posiciones pos1 y pos2.

315

Soluci n: o lista *intercambia(lista *L, posicion pos1, posicion pos2) { int aux; aux = pos1->sig->e; pos1->sig->e = pos2->sig->e; pos2->sig->e = aux; return(L); }

Ejercicio : (3 puntos) -En el programa de radio La Gramola, disponen de una gramola gigantesca que almacena centenares de CDs de m sica. Los CDs almacenados se encuentran oru ganizados en una lista ordenada, donde cada posici n de la lista identica a un o CD. Existe adem s un sistema que permite seleccionar un determinado CD por la a posici n que ocupa en la lista y que acciona un brazo autom tico que carga el CD o a seleccionado para ser escuchado por antena. Sin embargo, la direcci n del programa quiere optimizar el tiempo de seleco ci n de un CD, esto es, el tiempo que tarda el brazo en buscar un CD y cargarlo o en el lector. Para ello se desea hacer m s accesibles para el brazo los CDs m s a a solicitados, es decir, ponerlos m s cerca de la unidad de lectura de CDs. Por esa to, nos han pedido que contemos el n mero de veces que se solicita cada CD y u reorganizar la lista en consecuencia. Se pide: a) Escribe la denici n de tipos necesaria para usar una estructura de datos lista o con representaci n vectorial, de manera que esta estructura permita llevar o un contador para cada elemento de la lista y as poder conocer el n mero de u veces que cada CD ha sido solicitado. El identicador del CD es un entero simple, dentro del cual se encuentra codicada la posici n fsica de este en o la gramola. (0.8 puntos) b) Reescribe la funci n de selecci n de un elemento de la lista (CD) o o int recuperar(lista *l, posicion p)

316

de forma que quede reejado que el CD de la posici n p ha sido solicitado o una vez m s. (0.8 puntos) a c) Escribe una funci n con el siguiente perl o lista *reordena(lista *l) que reordene la lista de CDs bas ndose en los contadores de solicitud para a cada CD, de manera que la lista quede ordenada con los CDs m s solicitados a en las primeras posiciones y los menos solicitados en las ultimas posiciones. (1.4 puntos) Soluci n: o a) Bastar con incluir dentro de la estructura para la lista un vector de contadores, a de esta manera: #dene maxL ... /* Talla maxima del vector. */

typedef int posicion; /* Cada posicion se referencia con un entero. */ typedef struct { int v[maxL]; int cont[maxL]; posicion ultimo; } lista;

/* Vector para elementos. */ /* Vector para contadores. */ /* Posicion del ultimo elemento. */

b) Tan s lo hay que incrementar el contador de solicitudes del CD de la posici n o o p. int recuperar(lista *l, posicion p) { l->cont[p]++; /* Incrementamos el contador de este elemento. */ return(l->v[p]); /* Devolvemos el elemento que hay en la posicion p. */ } c) Aplicamos un m todo de ordenaci n sobre el vector de contadores l->cont. e o Por ejemplo, aplicamos el de inserci n directa. Hay que tener en cuenta que o

317

cuando se cambie de posici n alg n contador de solicitudes, tambi n debe o u e cambiarse el identicador del CD correspondiente. lista *reordena(lista *l){ int i, j, aux_cont, aux_v; for (i=1; i<=l->ultimo; i++) { aux_cont = l->cont[i]; aux_v = l->v[i]; j = i; while ((j>0) && (l->cont[j-1]<aux_cont)) { l->cont[j] = l->cont[j-1]; l->v[j] = l->v[j-1]; j--; } l->cont[j] = aux_cont; l->v[j] = aux_v; } return(l); }

Ejercicio : (2 puntos) -Sea A[l..r] un vector ordenado de menor a mayor de enteros diferentes (positivos). Dise a un algoritmo Divide y Vencer s que encuentre un i tal que l i r y n a A[i]=i, siempre que este i exista; en caso contrario, debe devolver -1. El perl de la funci n a dise ar es: o n int busca_igual(int *A, int l, int r) Por ejemplo, para el siguiente vector:
l l+1 ... 8 ... r-1 r

8 11 14

La funci n debera devolver 8. o El algoritmo debe tener un coste O(log n). NOTA: puedes basarte en el algoritmo de b squeda binaria (dicot mica) en un u o vector ordenado. 318

Soluci n: o La idea b sica es ir cercando el subvector de b squeda donde puede estar a u ese posible elemento tal que i=A[i]. Buscaremos el elemento en la posici n o media del vector actual, si no est all, entonces habr que seguir buscando en a a la mitad izquierda del vector o en la derecha. Para saber en qu parte debemos e buscar, podemos jarnos que si i>A[i], entonces el encontrar un i=A[i] s lo o podr ocurrir en la mitad izquierda del vector, del mismo modo si i<A[i], dea beremos buscar s lo en la mitad derecha del vector. Aplicando este razonamiento o de manera sucesiva, si existe un i tal que i=A[i], lo encontraremos, en caso contrario, devolveremos -1. La soluci n de manera recursiva sera: o int busca_igual(int *A, int l, int r) { int i; if (l == r) if (l == A[l]) return(l); else return(-1); else { i = (int ) (l+r)/2; if (i == A[i]) return(i); else if (i < A[i]) return(busca_igual(A,l,i-1)); else return(busca_igual(A,i+1,r)); } } La soluci n de manera iterativa sera: o int busca_igual(int *A, int l, int r) { int i; while (l<r) { i = (int ) (l+r)/2; if (i == A[i]) return(i); else if (i < A[i]) l = i+1; else r = i-1; } if (l == A[l]) return(l) else return(-1); }

319

Ejercicio : (1.5 puntos) -Dada la denici n para arboles con representaci n mediante listas de hijos: o o #dene N 100 typedef struct snodo { int e; struct snodo *sig; } nodo; typedef struct { int raiz; nodo *v[N]; } arbol; Escribe una funci n recursiva en lenguaje C que determine si un sub rbol de o a un arbol T es binario o no, devolviendo 1 en el caso de que el sub rbol sea binario a y 0 cuando no lo sea. El perl de la funci n a denir es: o int es_binario(arbol *T, int n) Donde n es el n mero que indica el nodo que es la raz del sub rbol (o sea, el u a nodo a partir del cual se considera el sub rbol). Por ejemplo, si suponemos que T a es el arbol de la siguiente gura:
7 1 2 3 4 5 6 raiz 7 8 9 10 11 12 4 2

3 1 4

5 8 2 9

11 6 12 10

1 9 3

8 12 5

10 11

recibiramos estos valores para estas llamadas: es_binario(T, 5) 1 es_binario(T, 11) 0 320

Soluci n: o Bas ndonos en un recorrido en postorden del sub rbol, comprobaremos si caa a da nodo tiene m s de 2 hijos, en el caso de que ocurra alguna vez, devolveremos a 0, la recursividad se encargar de transmitir dicho 0 indicando que el sub rbol no a a es binario. int es_binario(arbol *T, int n) { int cont; nodo *aux; if (T==NULL) return(1); aux=T->v[n]; cont=0; while (aux!=NULL) { if (es_binario(T, aux->e)==0) return(0); cont++; aux=aux->sig; } if (cont>2) return(0); else return(1); }

321

PRACTICAS
Ejercicio : (2 puntos) -Dada la denici n de tipos para las listas de chas de datos de alumnos vistas en o la pr ctica 6: a #dene maxcad 100 typedef struct { char nombre[maxcad]; oat nota; char asignatura[maxcad]; } ficha; typedef struct _lnodo { ficha e; struct _lnodo *sig; } lnodo; typedef lnodo * posicion; typedef struct { posicion primero, ultimo; } lista; Que implementa una lista con representaci n enlazada con variable din mica. o a Se pide escribir el c digo correspondiente de la funci n: o o lista * insertar(lista *l, ficha *f, posicion p) que inserta los datos almacenados en la cha f en la posici n p de la lista l. o Soluci n: o Es una inserci n normal en una lista enlazada: se crea la memoria para el o nuevo nodo, se copian los datos y se actualizan todos los punteros que sean necesarios:

322

lista * insertar(lista *l, ficha *f, posicion p){ posicion q; q = p->sig; p->sig = (lnodo *)malloc(sizeof(lnodo)); strcpy(p->sig->e.nombre,f->nombre); p->sig->e.nota = f->nota; strcpy(p->sig->e.asignatura,f->asignatura); p->sig->sig = q; if (l->ultimo==p) l->ultimo = p->sig; }

Ejercicio : (0.5 puntos) -Dada la denici n del tipo de datos para la pila de operadores vista en la pr ctica o a 5: #dene maxP 100 typedef int Toperador; typedef struct { Toperador v[maxP]; int tope; } Tpila_es; Redene el tipo de datos para la pila de operadores de manera que se implemente mediante una representaci n enlazada de pilas con variable din mica y o a escribe la operaci n: o Toperador tope_es(Tpila_es *p) que consulta el elemento tope de la pila utilizando la nueva representaci n o enlazada con variable din mica que has denido. a Soluci n: o La nueva denici n de tipos: o

323

typedef int Toperador; typedef struct _pnodo { Toperador e; struct _pnodo *sig; } Tpila_es; y la funci n de consulta del tope: o Toperador tope_es(Tpila_es *p){ return(p->e); }

324

Estructuras de Datos y Algoritmos


Junio 2003 - SEMESTRE A Examen: SOLUCIONES
Departamento de Sistemas Inform ticos y Computaci n a o Escuela Polit cnica Superior de Alcoy e

Ejercicio : (2 puntos) -Se desea implementar una cola mediante una representaci n enlazada con vario able din mica como la que aparece en la gura, en la que el ultimo nodo apunta a al primero:
pcab pcol Q
1

n1

Las deniciones de datos que hemos hecho son: typedef struct _cnodo { int e; struct _cnodo * sig; } cnodo; typedef struct { cnodo *pcab, *pcol; } cola; Escribir una funci n con el perl o cola *desencolar(cola *q) que elimine el elemento de la cabeza de la cola q, liberando la memoria correspondiente, etc.

325

Soluci n: o cola *desencolar(cola *q) { cnodo *qaux; qaux = q->pcab; q->pcab = q->pcab->sig; if (q->pcab == qaux) { /* si la cola se queda vacia */ q->pcab = NULL; q->pcol = NULL; } else q->pcol->sig = q->pcab; free(qaux); return(q); }

Ejercicio : (2 puntos) -Suponemos las siguientes deniciones de datos: #dene DRAGON 1 #dene PRINCESA 2 typedef struct snodo { int personaje; struct snodo *hizq, *hder; } nodo; typedef nodo arbol; Suponemos que el campo personaje de la estructura nodo solo toma los valores DRAGON y PRINCESA. Dado un arbol binario de personajes, se denominan nodos accesibles aquellos nodos que guardan una princesa de manera que a lo largo del camino desde la raz hasta el nodo (ambos inclusive) no se encuentra ning n drag n. u o Se pide escribir una funci n en lenguaje C, o int cuenta_princesas(arbol *T) la cual devuelva el n mero de nodos accesibles dado un arbol binario de peru sonajes. 326

Soluci n: o La soluci n pasar por realizar una variante del recorrido en preorden, donde o a si un nodo alcanzado se corresponde con un drag n ya no hay que seguir buscando o por ninguno de sus hijos, pues ning n nodo que se encuentre en ellos ser acceu a sible. Si es princesa, debe sumarse 1 al total de princesas accesibles y comprobar cuantas princesas accesibles hay en sus sub rboles. a int cuenta_princesas(arbol *T) { int num_princesas; num_princesas = 0; if (T != NULL) { if (T->personaje != if (T->personaje num_princesas num_princesas } } return(num_princesas); }

DRAGON) == PRINCESA) { = 1 + cuenta_princesas(T->hizq); += cuenta_princesas(T->hder);

Ejercicio : (3.5 puntos) -Realizar un algoritmo que utilizando la t cnica de divide y vencer s encuentre el e a mayor y segundo mayor elemento de un vector de enteros v. El vector lo delimitaremos entre dos ndices a y b. el perl de la funci n ser : o a void maximos(int *v, int a, int b, int *M, int *m) donde v es el vector de enteros que contiene los datos, a y b son los ndices que delimitan el subvector que estamos tratando en un determinado momento, y M y m son punteros a variables enteras sobre las cuales devolveremos el mayor y el segundo mayor del vector, respectivamente. Es necesario manejar estas dos variables M y m como punteros puesto que se pasan por referencia, es decir, los valores que buscamos se han de devolver por medio de dichos punteros. Nota: para almacenar un entero en la posici n de memoria correspondiente al o entero deber hacerse por ejemplo a (*M)=3;

327

Soluci n: o Recordando el esquema b sico de divide y vencer s, debemos dividir el proba a lema en subproblemas de talla menor, encontrar la soluci n a estos subproblemas o y recombinar dichas soluciones. Para resolver el problema vamos a emplear un esquema recursivo. c mo o podemos encontrar el mayor y el segundo mayor de un vector de n meros? u Que tal si dividimos el vector en dos partes y para cada una de esas partes (que son subvectores) hallamos el mayor y el segundo mayor? Aqu ya se puede ver la recursividad que vamos a hacer. Despu s de haber resuelto esos subproblemas, nos quedar la recombinaci n e a o de las soluciones parciales, es decir, debemos obtener el mayor y el segundo mayor de todo el vector a partir del mayor y el segundo mayor del primer subvector y del mayor y el segundo mayor del segundo subvector. Para ello deberemos utilizar unas cuantas estructuras if y else. Por ultimo, la soluci n a los subproblemas ser el caso base de la recursi n. o a o Para este problema un caso base es si la talla del vector con el que estamos tratando es 1, entonces podemos decir que el elemento que tenemos es el mayor de este subvector y que no habra segundo mayor lo cual podemos indicar diciendo que es -innito (a nivel de implementaci n -MAXINT). Podemos acabar la recursi n o o en otro caso base que es cuando la talla del vector con el que estamos tratando es 2, como solo hay dos elementos, podemos decir que el mayor de ellos es el mayor de este vector y el otro ser el segundo mayor. a

328

void maximos(int *v, int a, int b, int *M, int *m) { int q; int *M1, *M2, *m1, *m2; if (a == b) { (*M) = v[a]; (*m) = -MAXINT; } else if (b-a == 1) if (v[a] > v[b]) { (*M) = v[a]; (*m) = v[b]; } else { (*M) = v[b]; (*m) = v[a]; } else { q = (int ) (a+b)/2; maximos(v,a,q,M1,m1); maximos(v,q+1,b,M2,m2); if ((*M1) > (*M2)) { (*M) = (*M1); if ((*M2) > (*m1)) (*m) = (*M2); else (*m) = (*m1); } else { (*M) = (*M2); if ((*M1) > (*m2)) (*m) = (*M1); else (*m) = (*m2); } } } La primera llamada se hara con maximos(v,1,n,M,m);

329

PRACTICAS
Ejercicio : (2.5 puntos) -Dada una estructura de datos para listas de chas de alumnos como la propuesta en la pr ctica 6: a #dene maxcad 100 typedef struct { char nombre[maxcad]; oat nota; char asignatura[maxcad]; } ficha; typedef struct _lnodo { ficha e; struct _lnodo *sig; } lnodo; typedef lnodo *posicion; typedef struct { posicion primero, ultimo; } lista; y suponiendo denidas las operaciones: ficha *recuperar(lista *l, posicion p); posicion fin(lista *l); posicion principio(lista *l); posicion siguiente(lista *l, posicion p); Se pide escribir un procedimiento en lenguaje C que imprima los nombres de todos los alumnos de una lista l que han aprobado la asignatura cuyo nombre se facilita como par metro del procedimiento. Las notas van de 0 a 10, considerando a aprobado a partir de 5 (inclusive).

330

Soluci n: o void imprime_aprobados(lista *l, char *asig) { ficha *e; posicion pos; printf("\n\nImprimiendo aprobados de %s:\n",asig); pos = principio(l); while (pos != fin(l)) { e = recuperar(l, pos); if (strcmp(e->asignatura,asig)==0) if (e->nota>=5.0) printf("Nombre: %s\n",e->nombre); pos = siguiente(l, pos); } printf("\n\n"); }

331

Estructuras de Datos y Algoritmos


Junio 2003 - SEMESTRE B Examen: SOLUCIONES
Departamento de Sistemas Inform ticos y Computaci n a o Escuela Polit cnica Superior de Alcoy e

Ejercicio : (2.5 puntos) -Dada la denici n para arboles binarios de b squeda cuyos elementos son n meros o u u enteros: typedef struct snodo { int clave; struct snodo *hizq, *hder; } abb; y suponiendo que est denida la funci n para arboles binarios de b squeda: a o u abb *abb_insertar(abb *T, int x); que inserta un entero en un arbol binario de b squeda (se supone que esta u funci n hace las reservas de memoria necesarias, etc.). Se pide: o a) Escribir en C la funci n o void ordenar_vector(int *v, int n) y otras funciones que sean necesarias de manera que se consiga ordenar el vector de n meros v con talla n usando como estructura auxiliar un arbol u binario de b squeda. Se supone que los enteros almacenados en el vector u son todos distintos. (1.5 puntos) b) Cual es el coste temporal del algoritmo de ordenaci n del apartado a)? (1 o punto)

332

Soluci n: o a) Hay que insertar todos los n meros enteros del vector uno por uno en un arbol u binario de b squeda y despu s hacer un recorrido en inorden por el arbol e u e ir almacenando nuevamente los elementos en el vector de manera consecu tiva, de esta forma ya quedar n ordenados. El unico punto con un poco de a dicultad reside en mantener en un determinado momento la posici n del o vector donde se debe insertar un entero almacenado en el abb al realizar inorden. Para ello mantendremos una variable pos que se actualizar utia lizando la recursividad del algoritmo inorden. void ordenar_vector(int *v, int size) { int i; abb *T; T=NULL; /* creo un arbol vacio */ for (i=0;i<size;i++) T = abb_insertar(T,v[i]); i=0; inorden(T,v,&i); } void inorden(abb *T, int *v, int *pos) { if (T!=NULL) { inorden(T->hizq,v,pos); v[*pos] = T->clave; (*pos)++; inorden(T->hder,v,pos); } } b) El coste del algoritmo para ordenar vectores vendr dado por el coste del bucle a for m s el coste de la llamada al procedimiento inorden: a El bucle for se ejecutar n veces puesto que hay que insertar n enteros a en el abb. En el peor de los casos, cada inserci n en el abb va a tener o un coste de O(n), puesto que insertar en un abb tiene coste O(h) y en el caso peor todos los elementos estaran en una sola rama. As pues 2 el bucle for tendr un coste total de O(n ). a La llamada a inorden tiene claramente un coste de O(n). El coste total del algoritmo es O(n2 + n) O(n2 ). 333

Ejercicio : (2 puntos) -Dado un grafo G = (V, E) no dirigido, conexo y ponderado, se dene su arbol de expansi n de coste m ximo como el arbol de expansi n cuyo coste asociado es el o a o mayor posible respecto al coste de cada uno de los posibles arboles de expansi n o que contiene el grafo. Se pide escribir un algoritmo en pseudoc digo que obtenga el arbol de expano si n de coste m ximo para un grafo G = (V, E) no dirigido, conexo y ponderado. o a Soluci n: o Podemos aplicar una modicaci n al algoritmo de Kruskal visto en clase de o manera que en vez de obtener el arbol de expansi n de coste mnimo obtengamos o el de coste m ximo. a Las estructuras de datos que utilizaremos ser n las mismas que utiliza Kruskal. a Y la estrategia ser la misma pero las aristas habr que ordenarlas seg n su coste a a u asociado en orden no creciente, es decir, de mayor a menor. A cada paso habr que a seleccionar la arista de mayor peso que no se haya seleccionado todava y que no forme ciclos en el arbol de expansi n. o As el algoritmo ser : a Algoritmo Kruskal max(G) { A= para cada v rtice v V hacer e Crear Subconjunto(v) n para ordenar las aristas pertenecientes a E, seg n su peso, en orden no creciente u para cada arista (u,v) E, siguiendo el orden no creciente hacer si Buscar(u) = Buscar(v) entonces A = A {(u,v)} Union(Buscar(u),Buscar(v)) n si n para devuelve(A) } 334

Ejercicio : (3 puntos) -Tenemos un conjunto de procesos, cada uno de los cuales tiene un tiempo de ejecuci n predenido, ti . o Suponiendo que disponemos de m procesadores para ejecutar los procesos (en lugar de uno solo), cada uno con su cola de entrada (lineal) para los procesos. Se pide construir un algoritmo voraz para resolver el problema de la minimizaci n del tiempo de espera de todos los procesos. o Para ello, suponemos que los procesos est n almacenados en un MINHEAP, a Q, donde la clave asociada a cada proceso es su tiempo de ejecuci n (suponemos o que no hay procesos con igual tiempo de ejecuci n), por ejemplo: o
2

11

y tenemos las siguientes funciones predenidas para el MINHEAP. int minimo(int *Q); int *extract_min(int *Q); int vacio(int *Q); A su vez, las colas de entrada a los procesadores se implementan como colas lineales. Cada una de estas colas se guarda en una posici n del vector M (entre las o posiciones 1 y m). cola *M[1000]; y tenemos la siguiente operaci n denida para colas: o cola *encolar(int x, cola *q); Debe escribirse el c digo para la funci n o o cola **gestionar_procesos(int *Q, cola **M, int m)

335

utilizando una estrategia voraz que permita devolver el vector de colas M con una organizaci n de procesos en las colas tal que minimice los tiempos de espera o de todos los procesos. Por ejemplo, si tuvieramos 3 procesadores (m=3) para el conjunto de procesos anterior deberamos obtener este vector de colas.
M 1 2 3 2 3 5 11 8 6

Soluci n: o La soluci n voraz para minimizar tiempos de espera de todos los procesos o para un sistema monoprocesador consistira en encolar los procesos a ejecutar comenzando por el de menor tiempo de ejecuci n, despu s el de segundo menor o e tiempo de ejecuci n, etc. o Ahora que debemos crear varias colas de procesos y no una sola, seguiremos una estrategia parecida, intentando insertar en cada cola primero los procesos con menor tiempo de ejecuci n, por eso, a cada vez que queramos insertar en una cola o extraeremos el mnimo del heap que almacena los procesos. Adem s, la idea es a repartir los procesos en todas las colas, por ello realizaremos un bucle que cada vez que extraiga un proceso del heap lo insertar en una cola i de manera que la a siguiente inserci n se realice en la cola i+1. o Adem s hay que tener en cuenta que si vamos insertando en las colas desde a la 1 hasta la m y despu s volvemos a aplicar esa estrategia, estaremos almacee nando en las colas de las primeras posiciones del vector M procesos con tiempos peque os y en las colas de las ultimas posiciones del vector procesos con tiempos n grandes, con lo que los tiempos de espera de los procesos almacenados en las co las de las ultimas posiciones de M seran muy grandes. Para reducir esos tiempos, despu s de haber echo un bucle insertando procesos desde la cola 1 hasta la m, e realizaremos un bucle insertando procesos desde la cola m hasta la 1, teniendo en cuenta que siempre insertamos el proceso que hay con menor tiempo de ejecuci n. o La funci n quedara de esta manera: o

336

cola **gestionar_procesos(int *Q, cola **M, int m) { int i; int proc; while (!vacio(Q)) { for (i=1;i<=m;i++) if (!vacio(Q)) { proc=minimo(Q); Q=extract_min(Q); M[i]=encolar(proc, M[i]); } for (i=m;i>=1;i--) if (!vacio(Q)) { proc=minimo(Q); Q=extract_min(Q); M[i]=encolar(proc, M[i]); } } return(M); }

337

PRACTICAS
Ejercicio : (2.5 puntos) -Considerando la siguiente denici n de tipos para tablas hash vista en la pr ctica o a 9: typedef struct snodo { char palabra[MAX_TAM_PAL]; int identificador; struct snodo *sig; } tnodo; typedef tnodo *ttabla[MAX_CUBETAS]; Tenemos la siguiente funci n que realiza la lectura de una secuencia de pao labras a partir de un chero de texto y va insert ndolas en una tabla hash. a void leer_fichero(char *fich_entrada, ttabla t, int m, int funcion_hash){ FILE *f; char cadena[MAX_TAM_PAL]; int nuevo_id; tnodo *n; f=fopen(fich_entrada,"r"); nuevo_id = 0; while (obtener_palabra(f)!=NULL) { cadena = obtener_palabra(f); n = perteneceTabla(t,cadena,m,funcion_hash); if (n==NULL) { insertarTabla(t, cadena, nuevo_id, m, funcion_hash); nuevo_id++; } } fclose(f); } Donde la funci n obtener_palabra(f) obtiene una cadena de texto del o chero f que se corresponde con una palabra. La funci n o perteneceTabla(t,cadena,m,funcion_hash) consulta si la palabra cadena est dentro de la tabla hash y devuelve un puntero al nodo correspondia ente a dicha palabra, o NULL en caso de que no se encuentre. La funci n o 338

insertarTabla(t, cadena, nuevo_id, m, funcion_hash) inserta la palabra cadena en la tabla hash. Suponiendo que disponemos de la funci n clock() (de la librera time.h) o del lenguaje C, que devuelve un entero que es proporcional a la cantidad de tiempo de procesador usado por el proceso en curso, se pide: Reescribir la funci n leer_fichero de manera que podamos medir el o tiempo que se tarda en realizar todo el proceso de lectura del chero de texto, inserci n de las palabras, etc. y que al nal tambi n se ofrezca cual es el tiempo o e promedio para todas las palabras leidas del chero que cuesta ejecutar la funci n o perteneceTabla. Estos dos tiempos deben imprimirse antes de acabar la funci n. o Soluci n: o Para medir el tiempo total del proceso solo ser necesario tomar tiempos al a principio y al nal de la funci n y realizar la resta de rigor. o Para medir el tiempo promedio de la funci n perteneceTabla debemos o acumular el tiempo de todas las llamadas a perteneceTabla as como contar el n mero de veces que se ejecuta, para al nal realizar la divisi n y obtener el u o promedio.

339

void leer_fichero(char *fich_entrada, ttabla t, int m, int funcion_hash){ FILE *f; char cadena[MAX_TAM_PAL]; int nuevo_id; tnodo *n; int t1,t2,tp1,tp2; /* para medir tiempos */ oat acum; int veces; t1=clock(); acum=0.0; veces=0; f=fopen(fich_entrada,"r"); nuevo_id = 0; while (obtener_palabra(f)!=NULL) { cadena = obtener_palabra(f); tp1=clock(); n = perteneceTabla(t,cadena,m,funcion_hash); tp2=clock(); acum+=(tp2-tp1); veces++; if (n==NULL) { insertarTabla(t, cadena, nuevo_id, m, funcion_hash); nuevo_id++; } } fclose(f); t2=clock(); printf("Tiempo total del proceso:%d\n",t2-t1); printf("Tiempo promedio de perteneceTabla:%d\n",acum/veces); }

340

Bibliografa
[Aho] A.V. Aho, J.E. Hopcroft,J.D. Ullman, Estructuras de datos y algoritmos, Addison-Wesley, 1988. [Aoe] J. Aoe, An efcient digital search algorithm by using a double-array structure, IEEE Transactions on Software Engineering, Vol. 15, 9, pp. 1066-1077, 1989. [Brassard] G. Brassard, P. Bratley, Algortmica. Concepci n y an lisis, Mas o a son, 1990. [Cormen] T. H. Cormen, C. E. Leiserson, R. L. Rivest, Introduction to algorithms, MIT Press, 1990. [Ferri] F. Ferri, J. Albert, G. Martin, Introducci a lan` lisis i disseny o a dalgorismes. Universitat de Val ncia, 1998. e [Horowitz] E. Horowitz, S. Sahni, Computer algorithms Computer Science Press, 1997. [Kernighan] B.W. Kernighan, D. M. Ritchie, El lenguaje de programaci n C, o Prentice-Hall, 1991. [Sedgewick] R. Sedgewick, Algorithms in C. Fundamentals. Data structures. Sorting. Searching, Third edition. Addison-Wesley, 1998. [Weiss] M. A. Weiss, Estructuras de datos y algoritmos, Addison-Wesley, 1995. [Wirth] N. Wirth., Algoritmos + Estructuras de Datos = Programas, Ed. Castillo, 1980.

341

You might also like