Está en la página 1de 427

Apuntes de Programacin de o Sistemas

Rosa Ma Jimnez e Conrado Mart nez


Departament de Llenguatges i Sistemes Inform`tics a Univ. Polit`cnica de Catalunya e

15 de marzo de 2010

Typeset by FoilTEX

Indice 
I.

Tipos Abstractos de Datos y Programacin Orientada a Objetos . . . . . o TADs . . . . . . . . . . . . . . .

4 5

Denicin de TADs . . . . . . . . 11 o Semntica de un TAD . . . . . . 16 a Especicacin de TADs . . . . . . 31 o Correccin de la implementacin . 44 o o II. Anlisis de Algoritmos . . . . . . . . . 49 a Anlisis de algoritmos recursivos . 68 a III. Secuencias . . . . . . . . . . . . . . . 71 Introduccin o . . . . . . . . . . . 72

Listas . . . . . . . . . . . . . . . 73 Conceptos generales . . . . . . . 78 Listas con punto de inters . . . . 81 e 15 de marzo de 2010

2 Listas con iteradores . . . . . . . 93 Ordenacin por fusin . . . . . . 109 o o Pilas . . . . . . . . . . . . . . . . 115 Colas . . . . . . . . . . . . . . . 121 IV. Arboles . . . . . . . . . . . . . . . . . 130 Recorridos de rboles . . . . . . . 139 a V. Diccionarios . . . . . . . . . . . . . . 163 Arboles binarios de bsqueda . . . 175 u Quicksort . . . . . . . . . . . . . 200 Arboles equilibrados . . . . . . . 216 Tablas de dispersin (hash) . . . 250 o Tries . . . . . . . . . . . . . . . . 277 Skip lists . . . . . . . . . . . . . 296 VI. Colas de prioridad . . . . . . . . . . . 304 Heapsort . . . . . . . . . . . . . 324 15 de marzo de 2010

3 VII. Particiones . . . . . . . . . . . . . . . 334 Particiones . . . . . . . . . . . . 335 VIII. Grafos . . . . . . . . . . . . . . . . . 356 Algoritmo de Dijkstra . . . . . . . 357 IX. Ejemplos . . . . . . . . . . . . . . . . 377 Pilas: Evaluacin de expresiones . 378 o Secuencias: Dequeues . . . . . . 382

Listas autoorganizadas . . . . . . 389 Arboles: Derivadas simblicas . . 397 o Arboles: Ejemplos diversos . . . . 403 BSTs: Ejemplos diversos . . . . . 416 Particiones: Generacin de labeo rintos . . . . . . . . . . . . . . . 421

15 de marzo de 2010

I Tipos Abstractos de Datos y Programacin Orientada a Objetos o

 

TADs

Recordemos que un Tipo Abstracto de Datos (TAD) es un conjunto de valores y operaciones sobre dicho conjunto. Por ejemplo, los nmeros u naturales y las operaciones de suma y producto (de naturales) constituyen un ejemplo simple de TAD. El adjetivo abstracto subraya el hecho de que los valores y las operaciones pueden ser usadas sin tener en consideracin los detalles de cmo se representan o o los valores ni cmo se implementan las operaciones; o lo unico relevante son las operaciones disponibles y sus propiedades en sentido abstracto. Por ejemplo, una propiedad de la suma de naturales es que es conmutativa y eso no depende de que los nmeros u naturales estn representados en binario o en e decimal ni del algoritmo que se emplee. Cualquier implementacin de la operacin suma debe o o preservar esta propiedad (y otras). Esta nocin no o es nueva: cualquiera que haya utilizado un lenguaje de programacin de alto nivel ha escrito sus o programas y usado enteros, booleanos, reales, etc.
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 5

sin preocuparse de cmo se representan ni de cmo o o funcionan las operaciones denidas sobre stos. e

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

Lo que caracteriza por lo tanto a un TAD es su especicacin y sta no debe incluir ningn detalle o e u sobre su implementacin. o Existen diversas formas de especicar un TAD, pero en todo caso una especicacin debe ser completa y o precisa. El comportamiento y propiedades de las operaciones debe ser descrito sin ambigedad y sin u dejar ningn caso sin contemplar: programar u mediante TADs establece un contrato o compromiso entre los programadores del TAD y los que usen al TAD: los primeros deben implementar el TAD de modo que se satisfaga su especicacin; los o segundos deben usar el TAD sin esperar menos que esto, pero tampoco ms. a

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

La especicacin de un TAD puede ser informal en o el sentido de que no se emplea un formalismo y unas reglas muy precisas para construirla. Las operaciones y valores se describen en lenguaje natural, con las ventajas (sencillez, facilidad de comprensin) e inconvenientes (imprecisin, o o ambigedad) que ello conlleva. Tambin es u e frecuente utilizar una mezcla de formalidad e informalidad, usando modelos ampliamente conocidos como puedan ser conjuntos, secuencias, productos cartesianos o funciones (en el sentido matemtico) para describir otros TADs. Por a ejemplo, un valor de un TAD grafo dirigido puede ser descrito como un par de conjuntos V y E de vrtices y aristas, donde E es un subconjunto de e V V . Tambin puede utilizarse algn tipo de formalismo e u (existen varios), que permite una absoluta precisin o en la descripcin y su eventual uso para desarrollar o demostraciones formales de la correccin tanto de la o implementacin del TAD como de los programas o que lo usen. En la signatura emplearemos la
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 8

denominada especicacin ecuacional, que el o alumno ya conoce de asignaturas previas.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

En especicacin ecuacional, un TAD consta de dos o partes: por un lado, su signatura, que introduce un nuevo gnero (el nombre que se le da al conjunto e de valores que describe el TAD) y una serie de operaciones; por otro lado, las ecuaciones que describen el comportamiento de las operaciones del TAD. Para cada operacin del TAD se da un nombre, la o lista de los gneros (tipos) de sus argumentos (o e parmetros) y el gnero del resultado que devuelve a e la operacin. La lista de los gneros de los o e parmetros y el gnero del resultado constituyen el a e perl de la operacin. o

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

10

 

Denicin de TADs o

La denicin de un TAD involucra habitualmente o varios gneros, adems del que propiamente e a pretende denirse, y es frecuente que se base en uno o ms TADs auxiliares. Esta informacin puede a o explicitarse mediante las clusulas usa y gneros: a e TAD NAT NAT es el nombre del TAD usa BOOL usamos el TAD BOOL e ste define el gnero bool e gneros nat, bool e nat es el gnero de inters e e opns 0: nat suc: nat nat menor: nat nat bool el perfil de $menor$ es nat nat bool fopns ...

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

11

Por lo general, omitiremos las clusulas usa y a gneros, ya que resulta obvio por el contexto cules e a son los gneros y TADs auxiliares involucrados. e Adems permitiremos una sintaxis ms general para a a operadores unarios prejos o sujos y operadores binarios injos. Ej: < : nat nat bool Dado un TAD que dene el gnero s, se dice que e una operacin cuyo perl sea de la forma s o (sin parmetros) es constante. Aqullas cuyo a e resultado sea del gnero s son constructoras e (generadoras o modicadoras, segn una u clasicacin ms precisa que veremos ms adelante) o a a y las que tienen uno o ms parmetros de genro s a a e pero su resultado pertenece a un gnero distinto se e denominan consultoras. En nuestro ejemplo anterior, 0 y suc son constructoras (en particular, generadoras) y menor es consultora. La operacin 0 es constante. o

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

12

Frecuentemente un TAD depende de otro en un sentido formal y en realidad lo que se pretende es denir una familia innita de TADs que se comportan de manera idntica. Por ejemplo, si e tenemos conjuntos nitos y operaciones sobre ellos (p.e. la unin, la interseccin o la pertenencia), o o tanto da si los elementos de estos conjuntos son nmeros naturales, nmeros enteros, cadenas de u u caracteres, o cualquier otra cosa. Para los TADs genricos (tambin llamados parametrizados) e e usamos la sintaxis que muestra el siguiente ejemplo: TAD CONJUNTO ELEM opns / 0: cjt { }: elem cjt : cjt cjt cjt : cjt cjt cjt : elem cjt bool ...

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

13

La instanciacin de un TAD genrico T X consiste o e en reemplazar el parmetro formal X por un TAD a concreto. Denotaremos la instanciacin mediante o una sintaxis convencional: CONJUNTO NAT es el TAD que especica a los conjuntos nitos de nmeros naturales obtenido a partir del TAD u genrico CONJUNTO ELEM de los conjuntos nitos e de elementos cualesquiera. Se entiende en este caso que el gnero elem del e TAD formal ELEM se sustituye por el gnero nat del e TAD NAT. En ocasiones la relacin entre parmetro o a formal y el actual en una instaciacin no es tan o clara, pero como estos casos se presentan raramente en esta asignatura no introduciremos una sintaxis especial o ms compleja para expresarlos. a

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

14

Tambin daremos por sentado que el parmetro e a actual puede sustituir al formal: por ejemplo, si tenemos un TAD con parmetro formal X y ste a e tiene una operacin de comparacin, entonces el o o parmetro actual empleado al instanciar el TAD a genrico deber estar equipado con (al menos) una e a operacin de comparacin. o o El TAD formal ELEM dene slo un gnero, y no o e exige que el TAD actual tenga operaciones o que stas satisfagan tal o cual propiedad; cualquier TAD, e excepto el TAD vac puede instanciar a ELEM. En o, algunos ejemplos usaremos el TAD formal CLAVE que exige que el parmetro actual tenga un gnero a e elem y sobre ste una operacin de comparacin e o o que sea un orden total: es decir, la operacin o est denida para cualesquiera dos elementos y es a reexiva (a a = cierto), antisimtrica e (a = b = a b = falso b a = falso) y transitiva (a b b c = a c).

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

15

 

Semntica de un TAD a

Volvamos al ejemplo del TAD NAT. Parece razonable pensar que menor(x, y) (o equivalentemente, x < y) devuelve cierto si y slo si o x es menor que y en el orden habitual de los naturales . . . pero por el momento no se dice esto en ninguna parte del TAD! Las ecuaciones tienen justamente el propsito de describir con precisin y o o sin ambiguedades el comportamiento de las operaciones del TAD. Y hay todav otro problema . . . dnde se a o especica que los valores del TAD NAT son los nmeros naturales? El hecho de que le hayamos u llamado NAT al TAD y nat al gnero no signica e nadaaunque es bueno que empleemos identicadores coherentes con el signicado que se pretende. Para entender cul es el TAD descrito por una a especicacin ecuacional (la semntica del TAD) y o a cmo funcionan las ecuaciones necesitamos o introducir algunos conceptos previamente.
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 16

Dada una signatura , un trmino es una expresin e o constru a partir de las operaciones de , da respetando las normas sintcticas y de tipos. Por a ejemplo, suc(suc(suc(0))) es un trmino e constru a partir de la signatura de NAT. En do cambio, suc(menor(0)) no lo es: menor tiene dos argumentos y su resultado es del gnero bool; e adems, suc se aplica a un nat. a Denotaremos T al conjunto de los trminos e constru bles mediante la signatura . 1. Si : s es una operacin constante de o entonces es un trmino de gnero s en T. e e 2. Si : s1 s2 . . . sn s es una operacin de o y t1,t2, . . . ,tn son trminos de T de e gneros s1, s2, . . . , sn, respectivamente, entonces e (t1, . . . ,tn) es un trmino de gnero s de T. e e

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

17

Un concepto relacionado es el de trmino con e variables. Dado un conjunto de variables (identicadores) cada una de las cuales tiene asociado uno de los gneros del TAD, podemos e escribir una variable del gnero adecuado en e cualquier punto donde puede aparecer un trmino e (con o sin variables) de ese mismo gnero. Por e ejemplo, si x e y son del gnero nat (se suele e escribir x, y : nat) entonces suc(x) es un trmino e con variables correcto; tambin lo son x < y y e suc(0) < suc(x).

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

18

Sea Vs un conjunto de variables cuyo gnero es s y e V el conjunto de todas las variables consideradas: V = sSVs, donde S es el conjunto de los gneros de la e signatura (los que se denen en la clusula a gneros). El conjunto de los trminos con variables e e T(V ) se dene mediante las siguientes reglas: 1. Si : s es una operacin constante de o entonces es un trmino de gnero s en T(V ). e e 2. Si : s1 s2 . . . sn s es una operacin de o y t1,t2, . . . ,tn son trminos de T de e gneros s1, s2, . . . , sn, respectivamente, entonces e (t1, . . . ,tn) es un trmino de gnero s de T(V ). e e 3. Si x es una variable de V de gnero s entonces x e es un trmino de gnero s en T(V ). e e / Observad que T = T(0).
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 19

La clusula ecns. . . fecns de una especicacin a o ecuacional dene un conjunto nito de variables y sus correspondientes gneros, y un conjunto nito e de pares de trminos con variables (las ecuaciones e propiamente dichas). Los dos componentes de una ecuacin suelen separarse por el signo de igualdad. o TAD NAT ... ecns var x, y: nat 0 < 0 = falso 0 < suc(x) = cierto suc(x) < suc(y) = x < y ... fecns

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

20

El signicado intuitivo de las ecuaciones mostradas en el ejemplo previo es bastante inmediato: el 0 no es menor que 0; el 0 es menor que cualquier otro natural (por tanto, sucesor de algn otro natural x); u nalmente, si x es menor que y entonces el sucesor de x tiene que ser necesariamente menor que el sucesor de y, y a la inversa. Ya estamos casi a punto para formalizar la nocin o de semntica de un TAD1. Adicionalmente esta a formalizacin nos dar una herramienta valiosa para o a guiarnos en la construccin de especicaciones. o

1 En esta asignatura presentamos una de las muchas maneras posibles de

hacerlo. Para ser precisos, presentaremos la denominada semntica inicial, a que es la que ms frecuentemente se emplea. a
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 21

Denicin 1. Un modelo de un TAD con signao tura y ecuaciones E es un conjunto de valores y de operaciones isomorfo (=que se puede poner en correspondencia biun voca) con el conjunto cociente de los trminos T respecto a la relacin de e o equivalencia inducida por las ecuaciones E y operaciones entre las clases de equivalencia del conjunto cociente. Un ejemplo ayudar a comprender esta denicin2. a o Supongamos la siguiente especicacin: o TAD T gneros t e opns / 0: t ins: t nat t fopns fTAD

2 De hecho no es totalmente rigurosa ya que un TAD involucra poten-

cialmente a varios gneros y no se ha tenido en cuenta. e


Tipos Abstractos de Datos y Programacin Orientada a Objetos o 22

Puesto que no hay ecuaciones, el conjunto cociente al que se reere la denicin es el propio T: o / / / 0, ins(0, 0), ins(0, 1), . . . , / / ins(ins(0, 0), 0), ins(ins(0, 0), 1), . . . / / ins(ins(0, 1), 0), ins(ins(0, 1), 1), . . . / ins(ins(ins(0, 1), 0), 2), . . . / ins(ins(ins(0, 2), 0), 2), . . . donde cada trmino forma una clase de equivalencia e por s solo. La operacin 0 considerada sobre dicho o / conjunto es una funcin constante que siempre o / devuelve el trmino 0. Por otra parte la funcin e o ins : T T es tambin obvia y fcil de denir . . . e a

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

23

Un modelo del TAD T ha de ponerse en correspondencia biun voca con este conjunto y las dos funciones mencionadas. Un ejemplo simple de modelo son las secuencias de naturales, donde [ ] denota la secuencia vac a: / 0 [] / ins(0, 0) [0] / ins(ins(0, 0), 0) [0, 0] / ins(ins(0, 0), 1) [1, 0] / ins(ins(0, 1), 0) [0, 1] / ins(ins(ins(0, 1), 0), 2) [2, 0, 1] ...

o La funcin 0 se corresponde con la funcin que nos o / da la secuencia vac y la funcin ins con la a o funcin que devuelve la secuencia resultante de o aadir un natural dado al inicio de una secuencia. n

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

24

Sin embargo, los multiconjuntos nitos de naturales con la operacin de adicin de un elemento a un o o multiconjunto no es un modelo ya que no se puede poner en correspondencia biun voca: trminos e distintos tendr que corresponder a un mismo an multiconjunto. Se producir lo que se denomina a confusin. Tampoco los conjuntos nitos de o naturales constituyen un modelo pues nuevamente tendr amos confusin. o Supongamos ahora que aadimos la siguiente n ecuacin al TAD T, siendo las variables x e y del o gnero nat y C del gnero t: e e ins(ins(C, x), y) = ins(ins(C, y), x).

Intuitivamente esta ecuacin nos dice que no o importa el orden de insercin. o

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

25

En efecto, ahora el conjunto cociente est constitu por las clases: a do / / / 0, ins(0, 0), ins(0, 1), . . . , / / ins(ins(0, 0), 0), ins(ins(0, 0), 1), . . . / ins(ins(0, 1), 1), . . . / ins(ins(ins(0, 1), 0), 2), . . . / ins(ins(ins(0, 2), 0), 2), . . . Observad, por ejemplo, que los trminos e / / ins(ins(0, 0), 1) y ins(ins(0, 1), 0) son equivalentes con arreglo a la ecuacin que hemos o / / introducido, pero ins(ins(0, 0), 0) y ins(0, 0) no lo son. La funcin 0 sigue siendo una funcin constante que o / o nos devuelve la clase constitu por un unico da / trmino: 0. Si cada clase se representa por el e trmino en el que los elementos insertados aparecen e ordenados de menor a mayor, la funcin ins(C, x) o nos devuelve la clase representada por el trmino e
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 26

que se obtiene al insertar a x en orden con respecto a los elementos que aparecen en C.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

27

Por ejemplo, al aplicar ins( , 2) a la clase cuyo / representante es ins(ins(ins(0, 1), 1), 3)), se obtiene la clase representada por el trmino e / ins(ins(ins(ins(0, 1), 1), 2), 3). Ahora est claro que podemos establecer la a correspondencia biun voca entre multiconjuntos de naturales y el conjunto cociente, y las operaciones comentadas. / 0 {} / ins(0, 0) {0} / ins(ins(0, 0), 0) {0, 0} / ins(ins(0, 0), 1) {0, 1} / ins(ins(ins(0, 0), 1), 1) {0, 1, 1} ...

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

28

Por el contrario, las secuencias ya no constituyen un modelo vlido: hay secuencias (p.e. [1, 0, 1]) que no a corresponder a ninguna clase del conjunto an cociente. Se dice entonces que habr basura (los a valores del modelo que no tienen trmino e correspondiente). En la jerga, se dice que la semntica inicial se caracteriza por no contener a basura ni confusin. o Por ultimo si aadisemos la ecuacin n e o ins(ins(C, x), x) = ins(C, x) estar amos especicando los conjuntos nitos de naturales, ya que la ecuacin indica que no o importan los duplicados.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

29

La segunda ecuacin reduce el conjunto cociente a o las siguientes clases: / / / 0, ins(0, 0), ins(0, 1), . . . , / / ins(ins(0, 0), 1), ins(ins(ins(0, 2), 0), . . . ... / ins(ins(ins(0, 0), 1), 2), . . .

cuyo modelo ms inmediato son los conjuntos de a naturales (nitos) con una operacin de insercin de o o un elemento en un conjunto dado. Por ejemplo, los / trminos ins(ins(ins(0, 2), 0), 2) y e / ins(ins(0, 0), 2) son equivalentes. Por la primera / ecuacin, ins(ins(ins(0, 2), 0), 2) es equivalente a o / ins(ins(ins(0, 0), 2), 2) y ste ultimo es e equivalente, por la segunda ecuacin, a o / ins(ins(0, 0), 2). Ejercicio: Hallad un modelo para el TAD que resulta de incluir en el TAD T la segunda ecuacin, pero no o la primera.
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 30

Especicacin de TADs o
 

De toda la discusin previa, entorno al modelo o inicial de un TAD, se desprende la siguiente clasicacin de las operaciones de un TAD y una o serie de reglas utiles para la construccin de o especicaciones ecuacionales. Operaciones generadoras: aqullas que permiten e describir todos los valores del TAD; ms formala mente, las que generan el conjunto de trminos e vlidos a partir de los cuales se dene la semntica a a del TAD. Operaciones modicadoras: operaciones constructoras no generadoras; incrementan la expresividad y hacen util al TAD, pero no intervienen en la denicin de la semntica. El valor resultano a te de aplicar un modicadora es siempre obtenible por otra v sin hacer uso de la modicadora. a,
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 31

Operaciones consultoras: las que permiten extraer informacin; aplicadas sobre un valor del o TAD devuelven un valor perteneciente a otro TAD.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

32

Adicionalmente, se distingue entre generadoras puras (o libres) y generadoras impuras. Las primeras son aqullas en las que cada trmino se e e pondr en correspondencia con un valor del TAD; a para las segundas es necesario introducir ecuaciones ya que se pretende que trminos distintos e representen a un mismo valor (como en nuestro ejemplo de los multiconjuntos o los conjuntos). Una vez decidido un conjunto de generadoras para un TAD, procurando que incluya el m nimo nmero u de operaciones posible, se habrn de escribir a ecuaciones en las que intervienen las operaciones del TAD aplicadas sobre trminos de TG donde G es e el conjunto de operaciones generadoras de la signatura. Ntese que al menos una operacin o o constante (y por lo comn todas) debe estar u necesariamente presente en G.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

33

Se escribirn ecuaciones a 1. para hacer equivalentes trminos distintos que se e pretenden representen al mismo valor, cuando las generadoras son impuras; 2. para cada una de las operaciones modicadoras 3. para cada una de las operaciones consultoras En el caso de las ecuaciones de los casos 2 y 3, se proceder del siguiente modo. Sea una operacin a o modicadora y supongamos, para simplicar que su perl es : s s. Supongamos adems que g1 y g2 a son las operaciones generadoras del TAD, siendo g1 constante y g2 una operacin cuyo perl es o g2 : s t1 t2 s. Entonces las ecuaciones para sern de la forma a (g1) = (g2(C, x1, x2, . . .)) = . . . , donde C es una variable del gnero s y x1, x2, e . . . son variables de los gneros t1,t2, . . . e
Tipos Abstractos de Datos y Programacin Orientada a Objetos o 34

Cmo es la parte derecha de las ecuaciones? La o idea fundamental es que las ecuaciones de un TAD son un sistema de reescritura y que partiendo de un trmino que incluya una operacin no generadora e o debemos poder llegar a un trmino formado e exclusivamente por operaciones generadoras (del TAD en cuestin o de otro TAD en el caso de las o operaciones consultoras). Eso signica que en la parte derecha de una ecuacin pueden (y suelen) aparecer operaciones no o generadoras, pero aplicadas sobre trminos ms e a simples que los trminos sobre los que se aplican e en la parte izquierda. Y que siempre debe existir un o ms casos base, generalmente la aplicacin de la a o operacin no generadora sobre trminos constantes, o e donde la parte derecha no contiene a ninguna operacin no generadora. o

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

35

Supongamos el TAD CONJUNTO. Ya hemos visto que podemos considerar como operaciones generadoras / a 0 e ins. Puesto que no son puras, necesitamos dos ecuaciones para establecer equivalencias entre trminos. Supongamos que queremos incluir en el e TAD una operacin ms: , que dados dos o a conjuntos nos devuelve su unin. Para esta o modicadora debemos escribir, en principio, cuatro ecuaciones, correspondientes a las siguientes situaciones: / / 1. 0 0 =? / 2. 0 ins(C, x) =? / 3. ins(C, x) 0 =? 4. ins(C, x) ins(D, y) =?

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

36

Las tres primeras pueden reducirse a dos ecuaciones, gracias a propiedades de sobras conocidas de la unin de conjuntos: o var C : cjt / 3) C 0 = C / 4) 0 C = C Para expresar el comportamiento de la operacin de o unin cuando ambos conjuntos son no vac o os escribiremos: var C, D : cjt; x, y : elem 5) ins(C, x) ins(D, y) = ins(ins(C D, x), y) ya que las ecuaciones sobre ins garantizan que no se tienen en cuenta ni el orden de las inserciones ni los duplicados.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

37

Observad que en la parte derecha de la ecuacin la o operacin se aplica sobre trminos ms simples o e a (C y D) que en la parte izquierda. Eventualmente, aplicando esta ecuacin recursivamente el o trmino C o el trmino D sern 0 y se aplicarn la e e a / a ecuaciones 3 4, que constituyen los casos de base. o Supongamos que tenemos el siguiente trmino: e / / / ins(0, 1) (ins(ins(0, 0), 2) ins(0, 0)) Aplicando la ecuacin 5 se puede escribir: o / / / ins(0, 1) ins(ins(ins(0, 0) 0), 2), 0) Y con la ecuacin 3: o / / ins(0, 1) ins(ins(ins(0, 0), 2), 0)

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

38

Volviendo a aplicar la ecuacin 5 tenemos: o / / ins(ins(0 ins(ins(0, 0), 2), 1), 0) La ecuacin 4 establece que el trmino anterior es o e equivalente a: / ins(ins(ins(ins(0, 0), 2), 1), 0) Finalmente las dos primeras ecuaciones nos permiten simplicar y obtener el trmino: e / ins(ins(ins(0, 0), 2), 1) que representa al conjunto {0, 1, 2}.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

39

A veces los trminos carecen de sentido y no e corresponden a ningn valor vlido del TAD. En u a otras palabras, algunas operaciones del TAD son parciales, pues aplicadas a determinados valores del TAD no tienen imagen. Por ejemplo, supongamos que queremos incluir en el TAD SECUENCIA una operacin elim primero que elimina el primer o elemento de una secuencia dada. Una operacin o util, pero obviamente no tiene sentido aplicarla a la secuencia vac En estos casos escribiremos una a! ecuacin de error. El planteamiento que o presentamos aqu no es muy riguroso, pero un tratamiento correcto y completamente formal de este tipo de situacin (muy comn) es bastante o u tedioso. As pues escribiremos var S: seq; x : elem / elim primero(0) = error elim primero(ins(S, x)) = S Si un trmino contiene un subtrmino errneo, el e e o trmino completo lo es (el error se propaga): e / ins(elim primero(0), 2) = error.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

40

Muy a menudo el comportamiento de una operacin o depende de los datos de entrada; por ello ser necesario poder introducir condiciones para la a aplicacin de una ecuacin u otra. Se habla de o o ecuaciones condicionales. Por ejemplo, si tenemos un conjunto descrito por el trmino ins(C, x), el e resultado de eliminar y de dicho conjunto depender de que x sea o no igual a y. a opns ... elim: cjt elem cjt ... ecns var C : cjt; x, y : elem / / elim(0, x) = 0 x = y = elim(ins(C, x), y) = elim(C, y) x = y = elim(ins(C, x), y) = ins(elim(C, y), x)

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

41

Conviene remarcar que en la segunda ecuacin de o elim se asume que C es un trmino en el que slo e o hay operaciones generadoras, pero no necesariamente es un trmino m e nimo: p.e. podr a / ser C = ins(ins(0, 0), 0) y por eso la parte derecha de la ecuacin no es C, sino elim(C, y). o La construccin de especicaciones puede hacerse o de modo incremental y es usual que un TAD se obtenga a partir de otro TAD ms simple aadiendo a n nuevas operaciones. Esto es lo que se denomina enriquecer un TAD. En alguna ocasin escribiremos o TAD X TAD Y + ... para indicar que el TAD X se obtiene enriqueciendo el TAD Y con los elementos a continuacin del o s mbolo +.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

42

Tambin es util poder introducir operaciones e privadas que no siendo utilizables ni visibles para los usuarios del TAD (programas, otros TADs) facilitan la especicacin del TAD. En la signatura se o distinguirn las operaciones privadas anteponiendo a la palabra privada al nombre de la operacin; por o lo dems, no existe diferencia entre las operaciones a privadas y las restantes. Las operaciones privadas de especicacin no deben confundirse con las o operaciones privadas de implementacin: ni todas o las operaciones privadas de especicacin son o necesariamente implementadas ni las operaciones privadas de implementacin son especicadas. o Tanto unas como otras son invisibles para los usuarios y slo relevantes para quienes hyan de o especicar e implementar el TAD.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

43

Correccin de la implementacin o o
 

Dos conceptos importantes relacionados con la implementacin de TADs son el de funcin de o o abstraccin y el de invariante de la representacin. o o Supongamos que queremos trabajar con conjuntos como se muestra en el siguiente ejemplo: var C : CONJUNTO NAT fvar / C=0 para i = 1 hasta 100 hacer C = ins(C, i) fpara ... Para representar un conjunto podemos utilizar una tupla con dos campos: por un lado, un vector (digamos de 100 componentes de tipo entero) en cuyos componentes almacenaremos los elementos del conjunto y por otro un entero que nos da el nmero de elementos en el conjunto. u

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

44

La funcin de abstraccin es una aplicacin que o o o va del conjunto de representaciones (el conjunto de todas las tuplas formadas por un vector de 100 enteros y un entero) al conjunto de valores del TAD. nos dice cmo una determinada tupla representa o a un cierto conjunto. Tal como muestra el ejemplo de la gura, dos representaciones distintas pueden representar a un mismo conjunto; pero no todas las posibles representaciones son vlidas. Por ejemplo, a si el campo del nmero de elementos es negativo, o u hay valores repetidos entre los componentes 1 y n, donde n es el nmero de elementos. u Cuando elegimos una determinada representacin o debemos justamente explicar cul es la funcin de a o abstraccin y argumentar que efectivamente todo o valor del TAD es representable (lo que en trminos e matemticos se expresa diciendo que la funcin de a o abstraccin debe ser suprayectiva). Por otro lado o debe describirse qu representaciones son vlidas y e a cules no (la funcin puede y suele ser parcial y a o hay que describir con exactitud su dominio).

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

45

Para que un cierto valor del tipo que estamos usando para la representacin efectivamente o represente algn valor del TAD ha de cumplir el u invariante de representacin Invrep. Dicho de otro o modo, el invariante de representacin determina el o dominio de . Cmo demostramos la correccin de una o o implementacin? Formalmente, para demostrar que o la implementacin imp de una operacin , hay que o o demostrar que que conmuta con e Invrep se preserva.

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

46

Supongamos que tenemos un valor de un TAD x representado por medio de un valor y; es decir, y satisface Invrep y (y) = x. Por ejemplo, la tupla y = [1, 3, 2, 5], 4 podr representar al conjunto a x = {1, 2, 3, 5}. El invariente de la representacin o establece que el segundo atributo de la tupla y es un nmero n 0 y que los primeros n componentes u del primer atributo (una tabla de elementos) son distintos. Para demostrar que nuestra implementacin de la o operacin de insercin es correcta, tenemos que o o justicar dado un y que representa al conjunto x y un z que representa al elemento a insertar, nuestra operacin devuelve un nuevo objeto y que cumple o el invariante de representacin y que adems o a representa al conjunto resultante de insertar z en el conjunto x (mejor dicho el elemento representado por z).

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

47

En general, sea una operacin con parmetros de o a los tipos T1, . . . , Tk que devuelve un resultado del tipo T . Utilizaremos los sub ndices 1, . . . , k para referirnos a las funciones de abstraccin e o invariantes de representacin de cada tipo. Entonces o tenemos: I1(y1) . . . Ik (yk ) y = impl sigma(y1, . . . , yk ) I(y) (y) = (1(y1), . . . , k (yk ))

Tipos Abstractos de Datos y Programacin Orientada a Objetos o

48

II Anlisis de Algoritmos a

Una cualidad importante de un algoritmo o estructura de datos (implementacin de un TAD) es o su eciencia. Naturalmente, stos deben ser e correctos, es decir, satisfacer su especicacin. Pero o por lo general van a existir muchas soluciones satisfactorias al problema planteado y de entre ellas hay que seleccionar la ms adecuada atendiendo a a una serie de criterios. Uno de ellos, y que generalmente tiene gran importancia, es la eciencia de la solucin por lo que respecta a su consumo de o recursos de cmputo: tiempo de ejecucin y espacio o o de memoria. El anlisis de algoritmos tiene como objetivo a establecer propiedades sobre la eciencia permitiendo la comparacin entre soluciones o alternativas y predecir los recursos que usar un a algoritmo o ED.

Anlisis de Algoritmos a

50

Consideremos el siguiente algoritmo para determinar el m nimo de un vector A de n elementos: i = 1; min = 0; mientras i = n + 1 hacer si A[i] < A[min] entonces min = i fsi i = i+1 fmientras (n = 0 y min = 0) o (n > 0 y A[min] = mn{A[i] : 1 i n} El nmero de operaciones elementales u (comparaciones, asignaciones, sumas, etc.) que realiza es bsicamente proporcional al nmero de a u elementos n y el tiempo de ejecucin ser, o a consecuentemente, de la forma a n + b.

Anlisis de Algoritmos a

51

Esto es as tanto si implementamos este algoritmo en C sobre una estacin de trabajo Sun con Solaris, o en Pascal en un PC con procesador Pentium III bajo Windows 98 o en cdigo mquina en una Alpha de o a Digital Eq. Co. con DEC-Unix. De una implementacin a otra variarn las constantes a y b. o a Pero el tiempo de ejecucin ser en todos los casos o a una funcin lineal del nmero de elementos (= o u tamao de la entrada del algoritmo). n Para analizar algoritmos adoptaremos un modelo de cmputo idealizado en el que slo contaremos el o o nmero de operaciones elementales (cada operacin u o cuesta una unidad de tiempo). Eventualmente podemos restringir nuestra atencin a una operacin o o elemental concreta, siempre que sta se haga, ms o e a menos, tantas veces como cualquier otra.

Anlisis de Algoritmos a

52

Por ejemplo, en el algoritmo del m nimo la comparacin A[i] < A[min] se hace tantas veces o como i = i + 1 y slo una vez menos que la o comparacin i = n + 1. Por otra parte si una o comparacin entre elementos del vector fuera algo o o bastante ms costosa que una comparacin entre a o nmeros enteros o el incremento, cosa u perfectamente posible, el coste del algoritmo es proporcional al nmero de comparaciones entre u elementos y ste es a su vez exactamente igual a n. e Sin embargo, la eciencia (en tiempo o en espacio) de un algoritmo no depender exclusivamente del a tamao de la entrada, y en general depender de la n a propia entrada. Siguiendo con nuestro ejemplo del clculo del m a nimo la asignacin min = i puede o llegar a efectuarse hasta n veces (si la entrada est en orden creciente) o tan solo una vez (si hay a un m nimo en la primera posicin). o

Anlisis de Algoritmos a

53

En general, dado un algoritmo A cuyo conjunto de entradas es A su eciencia o coste (en tiempo, en espacio, en nmero de operaciones de E/S, etc.) es u una funcin T de A en N (o Q o R, segn el caso): o u T :A N T ()

Ahora bien, caracterizar la funcin T puede ser muy o complicado y adems proporciona informacin a o inmanejable, dif cilmente utilizable en la prctica. a

Anlisis de Algoritmos a

54

Por ello se denen tres funciones que dependen exclusivamente del tamao de las entradas y n describen de manera resumida las caracter sticas de T . Sea An el conjunto de entradas de tamao n y n Tn : An N la funcin T restringida a An. o Coste en caso mejor : Tmejor(n) = mn{Tn() | An}. Coste en caso peor : Tpeor(n) = m x{Tn() | An}. a Coste promedio: Tavg(n) = =

An

Pr() Tn()

k Pr(Tn = k).
k0

Anlisis de Algoritmos a

55

1. Para todo n 0 y para cualquier An Tmejor(n) Tn() Tpeor(n). 2. Para todo n 0 Tmejor(n) Tavg(n) Tpeor(n).

Anlisis de Algoritmos a

56

Anlisis de Algoritmos a

57

Por lo general, estudiaremos el coste en caso peor de los algoritmos por dos razones: 1) proporciona garant sobre la eciencia del algoritmo ya que el as coste nunca exceder el coste en caso peor; 2) es a ms fcil de calcular que el coste promedio. a a Una caracter stica esencial del coste (en caso peor, en caso mejor, promedio) es su tasa de crecimiento. Por ejemplo, toda funcin lineal de la forma o f (n) = a n + b cumple f (2n) 2 f (n) mientras que para funciones cuadrticas q(n) = a n2 + b n + c a tenemos q(2n) 4 q(n). Se dice que las funciones lineales y las cuadrticas tienen tasas de crecimiento a distintas. Tambin se dice que son de rdenes de e o magnitud distintos.

Anlisis de Algoritmos a

58

log2 n 1 2 3 4 5 6

n 2 4 8 16 32 64 N 2N

+1

n2 4 16 64 256 1024 4096 L C 2(L + N) 4C n log2 n 2 8 24 64 160 384

n3 8 64 512 4096 32768 262144 Q 8Q

2n 4 16 256 262144 6,87 1010 4,72 1021 E E2

Anlisis de Algoritmos a

59

La tasa de crecimiento de una funcin marca una o diferencia importante con las funciones que tengan una tasa distinta. Por ejemplo, f (n) = 100 n es mayor que g(n) = 0,1 n2 si n 1000; pero si n > 1000 entonces g(n) > f (n). Los factores constantes 100 y 0,1 no son relevantes en este aspecto; cualquier funcin cuadrtica (como g) o a superar, ms tarde o ms temprano, a a a a cualquierfuncin lineal (como f ). Este hecho, junto o con nuestra incertidumbre respecto a los factores constantes involucrados en el coste de un algoritmo, motivan la introduccin de la notacin asinttica. o o o Denicin 2. Dada una funcin f : R+ R+ la o o clase O ( f ) (O-grande de f ) es
O ( f )={g:R+ R+ | n0 c nn0 :g(n)c f (n)}.

En palabras, una funcin g est en O ( f ) si existe o a una constante c tal que g < c f para toda n a partir de un cierto punto (n0).

Anlisis de Algoritmos a

60

Aunque O ( f ) es un conjunto de funciones por tradicin se escribe g = O ( f ) en vez de g O ( f ). o Sin embargo, O ( f ) = g no tiene sentido. Algunas propiedades bsicas de la notacin O : a o 1. Si lmn g(n)/ f (n) < + entonces g = O ( f ). 2. Es reexiva: para toda funcin f : R+ R+, f = o O ( f ). 3. Es transitiva: si f = O (g) y g = O (h) entonces f = O (h). 4. Para toda constante c > 0, O ( f ) = O (c f ). La ultima propiedad justica nuestra preferencia por omitir factores constantes (p.e. hablaremos de O (n) y no de O (4 n)) y no expresar la base de logaritmos (O (log n), ya que podemos pasar de una base a otra multiplicando por el factor apropiado: logc x = logb x . logb c

Anlisis de Algoritmos a

61

Adems de la notacin O-grande se utilizan las a o notaciones (omega) y (zita). La primera dene un conjunto de funciones acotada inferiormente por una dada:
( f )={g:R+ R+ | n0 c>0 nn0 :g(n)c f (n)}.

La notacin es reexiva y transitiva; si o lmn g(n)/ f (n) > 0 entonces g = ( f ). Por otra parte, si f = O (g) entonces g = ( f ) y viceversa. Se dice que O ( f ) es la clase de las funciones que crecen no ms rpido que f . Anlogamente, ( f ) a a a es la clase de las funciones que crecen no ms a despacio que f . Finalmente, ( f ) = ( f ) O ( f ) es la clase de la funciones con la misma tasa de crecimiento que f .

Anlisis de Algoritmos a

62

La notacin es reexiva y transitiva, como las o otras. Es adems simtrica: f = (g) si y slo si a e o g = ( f ). Si lmn g(n)/ f (n) = c donde 0 < c < entonces g = ( f ). Otras propiedades adicionales de las notaciones asintticas son (las inclusiones son o estrictas): 1. Para cualesquiera constantes < , si f es una funcin creciente entonces O ( f ) O ( f ). o 2. Para cualesquiera constantes a y b > 0, si f es creciente, O ((log f )a) O ( f b). 3. Para cualquier constante c > 0, si f es creciente, O ( f ) O (c f ).

Anlisis de Algoritmos a

63

Los operadores convencionales (sumas, restas, divisiones, etc.) sobre clases de funciones denidas mediante una notacin asinttica se extienden de la o o siguiente manera: A B = {h | f A g B : h = f g}, donde A y B son conjuntos de funciones. Expresiones de la forma f A donde f es una funcin se entender como { f } A. o a Este convenio nos permite escribir de manera cmoda expresiones como n + O (log n), nO (1), o o (1) + O (1/n).

Anlisis de Algoritmos a

64

Regla de las sumas: ( f ) + (g) = ( f + g) = (m x{ f , g}). a Regla de los productos: ( f ) (g) = ( f g). Reglas similares se cumplen para las notaciones O y .

Anlisis de Algoritmos a

65

Las dos ultimas reglas facilitan el anlisis del coste a en caso peor de algoritmos iterativos. 1. El coste de una operacin elemental es (1). o 2. Si el coste de un fragmento S1 es f y el de S2 es g entonces el coste de S1; S2 es f + g. 3. Si el coste de S1 es f , el de S2 es g y el coste de evaluar B es h entonces el coste en caso peor de si B entonces S1 sino S2 fsi es m x{ f + h, g + h}. a

Anlisis de Algoritmos a

66

4. Si el coste de S durante la i-sima iteracin es e o fi, el coste de evaluar B es hi y el nmero de u iteraciones es g entonces el coste T de mientras B hacer S fmientras es
i=g(n)

T (n) =

i=1

fi(n) + hi(n).

Si f = m x{ fi + hi} entonces T = O ( f g). a

Anlisis de Algoritmos a

67

Anlisis de algoritmos recursivos a


 

El coste (en caso peor, medio, . . . ) de un algoritmo recursivo T (n) satisface, dada la naturaleza del algoritmo, una ecuacin recurrente: o esto es, T (n) depender del valor de T para a tamaos menores. Frecuentemente, la recurrencia n adopta una de las dos siguientes formas: T (n) = a T (n c) + g(n), T (n) = a T (n/b) + g(n). La primera corresponde a algoritmos que tiene una parte no recursiva con coste g(n) y hacen a llamadas recursivas con subproblemas de tamao n n c, donde c es una constante. La segunda corresponde a algoritmos que tienen una parte no recursiva con coste g(n) y hacen a llamadas recursivas con subproblemas de tamao n (aproximadamente) n/b, donde b > 1.

Anlisis de Algoritmos a

68

Teorema 1. Sea T (n) el coste (en caso peor, en caso medio, ...) de un algoritmo recursivo que satisface la recurrencia T (n) = f (n) a T (n c) + g(n) si 0 n < n0 si n n0,

donde n0 es una constante, c 1, f (n) es una funcin arbitraria y g(n) = (nk ) para una cierta conso tante k 0. Entonces (nk ) T (n) = (nk+1) (an/c) si a < 1 si a = 1 si a > 1.

Anlisis de Algoritmos a

69

Teorema 2. Sea T (n) el coste (en caso peor, en caso medio, . . . ) de un algoritmo recursivo que satisface la recurrencia T (n) = f (n) a T (n/b) + g(n) si 0 n < n0 si n n0,

donde n0 es una constante, b > 1, f (n) es una funcin arbitraria y g(n) = (nk ) para una cierta conso tante k 0. Sea = logb a. Entonces (nk ) T (n) = (nk log n) (n) si < k si = k si > k.

Anlisis de Algoritmos a

70

III Secuencias

 

Introduccin o

El modelo matemtico subyacente a pilas, colas y a listas es el de secuencia: una sucesin de elementos o a1, . . . , an. Como estructuras de datos, todas las secuencias son nitas. Al nmero de elementos de u una secuencia se le denomina su longitud. Por lo general, slo consideraremos secuencias o homogneas, es decir, constitu e das por elementos del mismo tipo. Las operaciones generadoras bsicas de a las secuencias son vacia y aade. La primera genera n la secuencia vac de longitud 0. La operacin a, o aade sita un nuevo elemento al nal de una n u secuencia dada y devuelve la secuencia resultante (de hecho, es perfectamente vlido considerar una a operacin aade cuyo comportamiento fuese el o n contrario: es decir, aade por el principio). n

Secuencias

72

 

Listas

El estudiante ya est familiarizado con las a secuencias y en concreto con pilas y colas. Trataremos aqu en primer lugar las listas, dedicando la parte nal del cap tulo a pilas y colas. Hablar de un TAD lista resulta inapropiado ya que existen mltiples variantes. No obstante, hay dos u tipos de operaciones que caracterizan a las listas y que las distinguen de otros TADs secuenciales: Operaciones de recorrido: el TAD ofrece operaciones que permiten recorrer y examinar todos los elementos de la lista sin modicar sta. e Operaciones de insercin y eliminacin en puntos o o arbitrarios de la lista.

Secuencias

73

Existen dos mecanismos bsicos que permiten las a operaciones mencionadas previamente: Lista con punto de inters: podemos imaginar la e lista equipada con una ventana o marca que designa un elemento concreto de la misma; para el recorrido disponemos de operaciones que nos permiten mover o desplazar el punto de inters y e una operacin para consultar el elemento designao do; para las operaciones de insercin y eliminacin o o disponemos de operaciones de insercin justo ano tes o justo despus del elemento designado o de e eliminacin del elemento designado. o Iteradores: este mecanismo es muy util aunque ms complejo y peligroso que el anterior; el a TAD lista ofrece el tipo y adems un nuevo tipo, a los iteradores, que son conceptualmente parecidos al punto de inters, pero independientes de la lise ta. Cada iterador es como un punto de lectura que puede ser movido dentro de la lista y permite recorrerla; y las operaciones de insercin y o eliminacin se hacen con referencia a un iterador. o
Secuencias 74

Los iteradores (tambin llamados items o atajos) se e comprenden mejor en el marco de la implementacin de los TADs en forma de objetos. o Un objeto lista es una zona de memoria que contiene un valor del tipo lista. A su vez consiste en una coleccin de objetos cada uno de los cuales o almacena el valor de un elemento de la lista. Un iterador marca o designa uno de estos objetos. As pues el iterador nos da acceso al contenedor de un elemento y no directamente al continente, es decir, el elemento propiamente dicho.

Secuencias

75

Por lo general, ser responsabilidad del programador a emplear los iteradores disciplinadamente y, por ejemplo, no usar un iterador para acceder a un elemento inexistente por haber sido eliminado (la operacin de eliminar destruye el contenedor y con o ello al elemento) o a un elemento de un objeto lista que ha dejado de existir. Otras complicaciones adicionales surgen del hecho de que un objeto de tipo iterador puede contener en un momento dado un iterador que designa a un elemento de una lista y al cabo de un rato otro iterador que designa a un elemento de una lista completamente diferente.

Secuencias

76

Las listas con punto de inters son ms simples e a desde un punto de vista conceptual (slo hay un o punto), relativamente fciles de especicar en el a marco puramente funcional de los TADs y ms a seguras: el punto de inters est ligado a una lista y e a no tiene existencia independiente. No obstante, los iteradores aumentan el repertorio de posibilidades, facilitan operaciones complicadas con listas sin incrementar el coste, y dan mayor versatilidad a un TAD lista. Por esta razn o bibliotecas como la STL o LEDA incluyen con el TAD list un TAD iterador auxiliar.

Secuencias

77

Conceptos generales
 

En una lista enlazada (cat: llista enllaada; ing: c linked lists) se utilizan apuntadores para explicitar la relacin de sucesin entre elementos. Recordemos o o que dado un tipo T , T es el tipo de los apuntadores a T . Una variable u objeto del tipo T puede almacenar la direccin de memoria de una o variable u objeto de tipo T . Un apuntador con el valor especial NULL (se lee nulo) no apunta a ningn u objeto. En C++ se denota NULL. Dado un apuntador p, p denota el objeto apuntado por p. Frecuentemente, el objeto en cuestin es una tupla y nuestro objetivo es consultar o y/o modicar uno de sus campos. Utilizaremos la notacin abreviada p campo en vez de la o notacin (p).campo; por ejemplo, o
p -> info = x ; p -> sig = q -> sig ;

Secuencias

78

Para la creacin y manipulacin de estructuras de o o datos dinmicas, esto es, que crecen o decrecen a segn las necesidades, contaremos con las siguientes u dos operaciones: new tipo delete apuntador La primera reclama una porcin de la memoria o dinmica, capaz de almacenar un valor del tipo a indicado, aplica la constructora de la clase y devuelve un apuntador al objeto creado. La segunda operacin aplica la destructora de la o clase al objeto apuntado y libera la memoria ocupada por el objeto, quedando disponible para un posterior uso.

Secuencias

79

Para lanzar un error de tipo X hacia los procedimientos superiores, nalizando de manera inmediata la ejecucin del procedimiento que hace la o llamada, se utiliza throw X. Emplearemos en estos apuntes el convenio siguiente: si no hay suciente memoria disponible, la operacin new lanza una o excepcin, tal como sucede en el lenguaje C++. o Salvo en algn caso infrecuente, los algoritmos que u guran en estos apuntes no detectarn ni tratarn a a estas excepciones; consideraremos que son capturadas y tratadas en mdulos superiores. o
static const int ErrorMaxElems = ...; // constructor de la clase lista ; // crea una lista vacia que a lo sumo // contendra n elementos lista :: lista ( int n ) { if ( n <= 0) // si n <= 0 entonces lanzamos un error throw error ( ErrorMaxElems ); // este codigo no se ejecuta si se ha lanzado la excepcion head = new nodo ; // crea un nodo o lanza una excepcion num_max_elems = n ; num_elems = 0; ... }

Secuencias

80

Listas con punto de inters e


 

Veremos ahora la implementacin de una clase o genrica Lista<T> con recorrido mediante punto de e inters. Comenzamos con una especicacin t e o pica para una lista con punto de inters; existen e mltiples variaciones posibles, pero no cambian la u esencia de lo que es una lista con punto de inters. e
template < typename T > class Lista { public : // Constructora , ctora por copia , asignacion , dtora // La constructora devuelve una lista vacia // con punto de interes indefinido Lista () throw ( error ); Lista ( const Lista <T >& l ) throw ( error ); Lista <T >& operator =( const Lista <T >& l ) throw ( error ); ~ Lista () throw (); // inserta el elemento x detras del elemento apuntado // por el PI ; si el PI es indefinido , inserta x como // primer elemento de la lista void inserta ( const T & x ) throw ( error ); // elimina el elemento apuntado por el PI ; no hace nada // si el PI es indefinido ; el PI pasa a apuntar al sucesor // del elemento eliminado o queda indefinido si el elemento // eliminado no tenia sucesor void elimina () throw (); ... // continua en la pagina siguiente

Secuencias

81

... // continua de la pagina anterior // longitud de la lista nat longitud () const throw (); // devuelve cierto si y solo si la lista es vacia bool es_vacia () const throw (); // situa el PI en el primer elemento de la lista // o queda indefinido si la lista esta vacia void primero () const throw (); // mueve el PI al sucesor del elemento apuntado por // el PI , quedando el PI indefinido si apuntaba al // ultimo de la lista ; no hace nada si el PI estaba // indefinido void siguiente () const throw ( error ); // devuelve el elemento apuntado por el PI ; lanza // una excepcion si el PI estaba indefinido const T & actual () const throw ( error ); // devuelve cierto si y solo si el PI esta indefinido // ( apunta al elemento " final " ficticio ) bool final () const throw (); static const int PuntoInteresIndef = 201; ... };

Secuencias

82

Para representar el tipo Lista usaremos una lista enlazada simple. Mantendremos un apuntador al primer elemento y otro apuntador al elemento marcado por el punto de inters. Para posibilitar la e eliminacin de elementos y simplicar la o implementacin, el nodo apuntado por head es un o elemento cticio o fantasma, y pi apunta al predecesor del elemento que realmente nos interesa. Si pi == NULL pi -> next == NULL signica o que no tenemos punto de inters, es decir, que es e indenido. Tambin tendremos un atributo sz para saber la e longitud de la lista en todo momento.

Secuencias

83

La representacin adoptada, y en concreto el o apuntador al elemento precedente al elemento marcado por el punto de inters, responde a la e necesidad de implementar ecientemente todas las operaciones. Si no incluysemos la operacin e o eliminar podr adoptarse una representacin ms a o a natural, con pi apuntando al elemento de inters. e El elemento fantasma, si bien no estrictamente necesario, simplica notablemente la implementacin de diversas operaciones, evitando o complejos anlisis por casos. a Por otra parte, si se necesitasen operaciones de insercin por el extremo nal ser conveniente o a disponer de un apuntador al ultimo elemento. El coste en espacio es (sT + s p) n + 2s p, donde sT es el tamao de un objeto del tipo T y s p el tamao de n n un apuntador (p.e., 4 bytes).

Secuencias

84

... private : struct nodo { T info ; nodo * next ; }; nodo * head ; nodo * pi ; nat sz ; // operaciones privadas nodo * copia_lista ( nodo * orig ) throw ( error ); void destruye_lista ( nodo * p ) throw (); void swap ( Lista <T >& l ) throw (); }; // puesto que es una clase generica , el fichero . hpp // incluye al fichero de implementacion . t # include " lista_pi . t "

// Coste : (1) template < typename T > Lista <T >:: Lista () throw ( error ) { head = new nodo ; head -> next = NULL ; pi = NULL ; sz = 0; }

Secuencias

85

// Coste : (1) template < typename T > void Lista <T >:: inserta ( const T & x ) throw ( error ) { nodo * p = new nodo ; try { p -> info = x ; // (1) } catch ( const error & e ) { // si la asignacion falla delete p ; throw ; // eliminamos el nuevo nodo } if ( pi != NULL and pi -> next != NULL ) { p -> next = pi -> next -> next ; // (2) pi -> next -> next = p ; // (3) } else { p -> next = head -> next ; head -> next = p ; } ++ sz ; }

pi p

(3) x(1)

(2)

Secuencias

86

// Coste : (1) template < typename T > void Lista <T >:: elimina () throw () { if ( pi == NULL or pi -> next == NULL ) return ; nodo * p = pi -> next ; // (1) pi -> next = p -> next ; // (2) delete p ; // (3) -- sz ; }

(2)

(3)

pi

p (1)

hacia la memoria dinamica libre

Secuencias

87

// Coste : (1) template < typename T > nat Lista <T >:: longitud () const throw () { return sz ; } // Coste : (1) template < typename T > bool Lista <T >:: es_vacia () const throw () { return head -> next == NULL ; // equivalentemente : return sz == 0 }

Secuencias

88

// Coste : (1) template < typename T > void Lista <T >:: primero () const throw () { pi = head ; } // Coste : (1) template < typename T > void Lista <T >:: siguiente () const throw ( error ) { if ( pi == NULL or pi -> next == NULL ) return ; pi = pi -> next ; } // Coste : (1) template < typename T > const T & Lista <T >:: actual () const throw ( error ) { if ( pi == NULL or pi -> next == NULL ) throw error ( PuntoInteresIndef ); return pi -> next -> info ; } // Coste : (1) template < typename T > bool Lista <T >:: final () const throw () { return ( pi == NULL ) or ( pi -> next == NULL ); }

Secuencias

89

// Coste : (N) // N = numero de nodos accesibles // desde el puntero orig template < typename T > typename Lista <T >:: nodo * Lista <T >:: copia_lista ( nodo * orig ) throw ( error ) { if ( orig == NULL ) return NULL ; nodo * dst = new nodo ; try { dst -> info = orig -> info ; dst -> next = copia_lista ( orig -> next ); return dst ; } catch ( const error & e ) { delete dst ; throw ; } } // Coste : (l.sz) template < typename T > Lista <T >:: Lista ( const Lista <T >& l ) throw ( error ) { head = copia_lista ( l . head ); sz = l . sz ; pi = head ; nodo * p = l . head ; while ( p != l . pi ) { // situa el pi de la nueva pi = pi -> next ; // lista en el lugar que le p = p -> next ; // corresponde } }

Secuencias

90

// Coste : (1) template < typename T > void Lista <T >:: swap ( Lista <T >& L ) throw () { nodo * auxn = head ; head = L . head ; L . head = auxn ; auxn = pi ; pi = L . pi ; L . pi = auxn ; int auxs = sz ; sz = L . sz ; L . sz = auxs ; } // Coste : (sz + l.sz) template < typename T > Lista <T >& Lista <T >:: operator =( const Lista <T >& l ) throw ( error ) { Lista <T > aux = l ; // usamos ctor por copia swap ( aux ); return * this ; } // Coste : (N) // N = numero de nodos accesibles // desde el puntero p template < typename T > void Lista <T >:: destruye_lista ( nodo * p ) throw () { if ( p != NULL ) { destruye_lista ( p -> next ); delete p ; } } // Coste : (sz) template < typename T > Lista <T >::~ Lista () throw () { destruye_lista ( head ); }

Secuencias

91

Ejemplo de recorrido (unidireccional) para una lista simple con punto de inters: e
// Coste : (N) Lista < int > L ; int x ; while ( cin >> x ) { L . inserta ( x ); } // L contiene los elementos introducidos // en orden inverso int sum = 0; L . primero (); while ( not L . final ()) { sum = sum + L . actual (); L . siguiente (); } cout << " Promedio = " << double ( sum ) / L . longitud () << endl ;

Secuencias

92

 

Listas con iteradores

En una lista con recorridos mediante iteradores, la clase lista dene una subclase iterador. Ambas clases deben cooperar estrechamente. Cada una de ellas autoriza a la otra el acceso a su parte privada mediante la declaracin friend. Los iteradores o necesitan conocer la implementacin de la Lista, o por eso la parte privada de la clase Lista se pone al principio. La estructura general de la denicin tendr el o a siguiente aspecto:
template < typename T > class Lista { private : ... // atributos y metodos privados de Lista public : friend class iterador ; // declaracion de la clase // iterador ( como amiga ) ... // metodos de la clase Lista class iterador { public : friend class Lista ; // la clase Lista es amiga ... // metodos de la clase iterador private : ... // atributos y metodos privados de iterador }; };

Secuencias

93

template < typename T > class Lista { private : ... // atributos y metodos privados de Lista public : friend class iterador ; // Constructora , ctora por copia , asignacion , dtora // La constructora devuelve una lista vacia Lista () throw ( error ); Lista ( const Lista <T >& l ) throw ( error ); Lista <T >& operator =( const Lista <T >& l ) throw ( error ); ~ Lista () throw (); // inserta el elemento x detras / delante // del elemento apuntado por el iterador ; // si el iterador it es indefinido , inserta_detras // a~ ade a x como primero de la lista , e inserta_delante n // a~ ade x como ultimo de la lista ; en abstracto un n // iterador indefinido " apunta " a un elemento ficticio // que es a la vez predecesor del primero y sucesor del // ultimo void inserta_detras ( const T & x , iterador it ) throw ( error ); void inserta_delante ( const T & x , iterador it ) throw ( error ); // ... continua en la pagina siguiente

Secuencias

94

// ... continua de la pagina anterior // tanto elimina_avz como elimina_rtr eliminan // al elemento apuntado por el iterador , // si el it es indefinido no hacen nada ; // con elimina_avz el iterador pasa a apuntar al // sucesor del elemento eliminado o queda indefinido // si el elemento eliminado es el ultimo ; con elimina_rtr // el iterador pasa a apuntar al predecesor del elemento // eliminado o queda indefinido si el eliminado es el // primer elemento void elimina_avz ( iterador & it ) throw (); void elimina_rtr ( iterador & it ) throw (); // longitud de la lista nat longitud () const throw (); // devuelve cierto si y solo si la lista es vacia bool es_vacia () const throw (); // devuelve un iterador al primer / ultimo elemento o // un iterador indefinido si la lista es vacia iterador primero () const throw (); iterador ultimo () const throw (); // devuelve un iterador indefinido iterador indef () const throw (); // ... continua en la pagina siguiente

Secuencias

95

// ... continua de la pagina anterior // Un objeto iterador siempre esta asociado a una lista // particular ; solo pueden ser creados mediante los // metodos Lista <T >:: primero , Lista <T >:: ultimo y // Lista <T >:: indef . // Cada lista tiene su iterador indefinido propio : // L1 . indef () != L2 . indef () class iterador { public : friend class Lista ; // Ctora por copia , asignacion , dtora iterador ( const iterador & it ) throw (); iterador & operator =( const iterador & it ) throw (); ~ iterador () throw (); // Accede al elemento apuntado por el iterador o lanza // una excepcion si el iterador es indefinido const T & operator *() const throw ( error ); // Operadores de avance ( pre - y postincremento ) y // de retroceso ( pre - y postdecremento ); // no hacen nada si el iterador es indefinido iterador & operator ++() throw (); iterador operator ++( int ) throw (); iterador & operator - -() throw (); iterador operator - -( int ) throw (); // Operadores de igualdad y desigualdad entre iteradores bool operator ==( iterador it ) const throw (); bool operator !=( iterador it ) const throw (); static const int IteradorIndef = 301; private : ... // atributos y metodos privados de iterador }; };

Secuencias

96

Ejemplo de recorridos para una lista con iteradores:


// Coste : (N) Lista < int > L ; int x ; while ( cin >> x ) { L . inserta_detras (x , L . indef ()); } // L contiene los N elementos introducidos // en orden inverso int sum = 0; Lista < int >:: iterador it1 = L . primero (); while ( it1 != L . indef ()) { sum = sum + * it1 ; ++ it1 ; } double prom = double ( sum ) / L . longitud (); // Buscamos el ultimo elemento en la lista // que es mayor o igual que el promedio ; // recorremos la lista de atr \ as hacia // adelante for ( Lista < int >:: iterador it2 = L . ultimo (); it2 != L . indef () and * it2 < prom ; -- it2 ) ; // aqui it2 == L . indef () or * it2 >= prom

Secuencias

97

Para la representacin de la clase Lista, usaremos o una lista doblemente encadenada. Cada nodo tiene dos apuntadores: uno a su sucesor y otro a su predecesor. La implementacin de los diversos o mtodos resulta mucho ms sencilla y elegante si e a tenemos un elemento cticio o fantasma y cerramos la lista circularmente. Es decir, el primer elemento real de la lista tendr como predecesor al fantasma; a el ultimo elemento real de la lista tendr como a sucesor al fantasma.
template < typename T > class Lista { private : struct nodo { T info ; nodo * next ; nodo * prev ; }; nodo * head ; nat sz ; nodo * copia_lista ( nodo * orig , nodo * orighead , nodo * h ) throw ( error ); void destruye_lista ( nodo * p , nodo * h ) throw (); void swap ( Lista <T >& l ) throw (); void inserta ( const T & x , nodo * pins ) throw ( error ); void elimina ( nodo * pdel ) throw (); public : ... };

Secuencias

98

Un iterador en una Lista es simplemente un apuntador a un nodo; pero tambin contiene un e apuntador al nodo fantasma de la lista, de ese modo podemos saber si dos iteradores dados apuntan a elementos de una misma lista o apuntan a elementos de listas diferentes. Un iterador indenido es un iterador que apunta al elemento fantasma de su lista. Como la clase iterador es amiga de Lista, sabe de la existencia del tipo privado nodo. Por otro lado queremos evitar que los iteradores puedan ser constru dos de manera independiente de una lista, hacemos que la constructora de iteradores sea privada.
class iterador { ... private : // Constructora . Devuelve un iterador indefinido // No puede usarse fuera de la clase iterador . iterador () throw (); nodo * curr ; // apuntador al elemento nodo * h ; // apuntador al header de la lista };

Secuencias

99

// Constructora por defecto // Coste : (1) template < typename T > Lista <T >:: Lista () throw ( error ) { head = new nodo ; head -> next = head -> prev = head ; sz = 0; } // Consultoras simples // Coste : (1) template < typename T > nat Lista <T >:: longitud () const throw () { return sz ; } // Coste : (1) template < typename T > bool Lista <T >:: es_vacia () const throw () { return sz == 0; }

Secuencias

100

// Inserciones . La figura ilustra la funcion // privada inserta (x , pins ). Las tres tienen coste : (1) template < typename T > void Lista <T >:: inserta_detras ( const T & x , iterador it ) throw ( error ) { inserta (x , it . curr ); } template < typename T > void Lista <T >:: inserta_delante ( const T & x , iterador it ) throw ( error ) { inserta (x , it . curr -> prev ); } template < typename T > void Lista <T >:: inserta ( const T & x , nodo * pins ) throw ( error ) { nodo * p = new nodo ; try { p -> info = x ; } // (1) catch ( const error & e ) { delete p ; throw ; } p -> next = pins -> next ; // (2) p -> prev = pins ; // (3) pins -> next = p ; // (4) p -> next -> prev = p ; // (5) ++ sz ; }

(4)

(5)

(2)

pins

(3) x(1)

Secuencias

101

// Borrados . La figura ilustra la funcion // privada elimina ( pdel ). Las tres tienen coste : (1) template < typename T > void Lista <T >:: elimina_avz ( iterador & it ) throw () { if ( it . curr == head ) return ; it . curr = it . curr -> next ; elimina ( it . curr -> prev ); } template < typename T > void Lista <T >:: elimina_rtr ( iterador & it ) throw () { if ( it . curr == head ) return ; it . curr = it . curr -> prev ; elimina ( it . curr -> next ); } template < typename T > void Lista <T >:: elimina ( nodo * pdel ) throw () { pdel -> prev -> next = pdel -> next ; // (1) pdel -> next -> prev = pdel -> prev ; // (2) delete pdel ; // (3) -- sz ; }

(1)

(3)

(2)

hacia la memoria dinamica libre

pdel

Secuencias

102

// Metodos para los recorridos // Coste : (1) template < typename T > typename Lista <T >:: iterador Lista <T >:: primero () const throw () { iterador it ; it . curr = head -> next ; it . h = head ; return it ; } // Coste : (1) template < typename T > typename Lista <T >:: iterador Lista <T >:: ultimo () const throw () { iterador it ; it . curr = head -> prev ; it . h = head ; return it ; } // Coste : (1) template < typename T > typename Lista <T >:: iterador Lista <T >:: indef () const throw () { iterador it ; it . curr = head ; it . h = head ; return it ; }

Secuencias

103

// Constructor por defecto , por copia , asignacion y // destructora de la clase Lista <T >:: iterador // Coste : (1) template < typename T > Lista <T >:: iterador :: iterador () throw () : curr ( NULL ) , h ( NULL ) {} // Coste : (1) template < typename T > Lista <T >:: iterador :: iterador ( const iterador & it ) throw () { curr = it . curr ; h = it . h ; } // Coste : (1) template < typename T > typename Lista <T >:: iterador & Lista <T >:: iterador :: operator =( const iterador & it ) throw () { curr = it . curr ; h = it . h ; return * this ; } // Coste : (1) template < typename T > Lista <T >:: iterador ::~ iterador () throw () {}

Secuencias

104

// // // //

Operadores de pre - y postincremento , y de pre - y postdecremento de iteradores . Los incrementos avanzan el iterador , los decrementos lo hacen retroceder . Todos tienen coste : (1)

template < typename T > typename Lista <T >:: iterador & Lista <T >:: iterador :: operator ++() throw () { if ( curr != h ) curr = curr -> next ; return * this ; } template < typename T > typename Lista <T >:: iterador Lista <T >:: iterador :: operator ++( int ) throw () { iterador tmp (* this ); ++(* this ); return tmp ; } template < typename T > typename Lista <T >:: iterador & Lista <T >:: iterador :: operator - -() throw () { if ( curr != h ) curr = curr -> prev ; return * this ; } template < typename T > typename Lista <T >:: iterador Lista <T >:: iterador :: operator - -( int ) throw () { iterador tmp (* this ); - -(* this ); return tmp ; }

Secuencias

105

// Operador de dereferencia y operadores relacionales // Coste : (1) template < class T > // con < typename T > falla !!! const T & Lista <T >:: iterador :: operator *() const throw ( error ) { if ( curr == h ) throw error ( IteradorIndef ); return curr -> info ; } // Coste : (1) template < typename T > bool Lista <T >:: iterador :: operator ==( iterador it ) const throw () { return ( curr == it . curr ) and ( h == it . h ); } // Coste : (1) template < typename T > bool Lista <T >:: iterador :: operator !=( iterador it ) const throw () { return not (* this == it ); }

Secuencias

106

// Constructora por copia y su privada // Coste : (N) // N = Numero de nodos accesibles desde el puntero orig template < typename T > typename Lista <T >:: nodo * Lista <T >:: copia_lista ( nodo * orig , nodo * orig_head , nodo * h ) throw ( error ) { if ( orig == orig_head ) return h ; nodo * dst = new nodo ; try { dst -> info = orig -> info ; dst -> next = copia_lista ( orig -> next , orig_head , h ); dst -> next -> prev = dst ; return dst ; } catch ( const error & e ) { delete dst ; throw ; } } // Coste : (l.sz) template < typename T > Lista <T >:: Lista ( const Lista <T >& l ) throw ( error ) { head = new nodo ; head -> next = copia_lista ( l . head -> next , l . head , head ); head -> next -> prev = head ; sz = l . sz ; }

Secuencias

107

// Destructora , asignacion y sus privadas // Coste : (1) template < typename T > void Lista <T >:: swap ( Lista <T >& L ) throw () { nodo * auxn = head ; head = L . head ; L . head = auxn ; int auxs = sz ; sz = L . sz ; L . sz = auxs ; } // Coste : (sz + l.sz) template < typename T > Lista <T >& Lista <T >:: operator =( const Lista <T >& l ) throw ( error ) { Lista <T > aux = l ; // usamos ctor por copia swap ( aux ); return * this ; } // Coste : (N) // N = Numero de nodos accesibles desde el puntero p template < typename T > void Lista <T >:: destruye_lista ( nodo * p , nodo * h ) throw () { if ( p != h ) { destruye_lista ( p -> next , h ); delete p ; } } // Coste : (sz) template < typename T > Lista <T >::~ Lista () throw () { destruye_lista ( head -> next , head ); delete head ; }

Secuencias

108

Ordenacin por fusin o o


 

El algoritmo de ordenacin por fusin (mergesort) o o fue uno de los primeros algoritmos ecientes de ordenacin jams propuestos. Diversas variantes de o a este algoritmo son particularmente utiles para la ordenacin de datos residentes en memoria externa. o El propio mergesort es un mtodo muy ecaz para e la ordenacin de listas enlazadas. o La idea bsica es simple: se divide la secuencia de a datos a ordenar en dos subsecuencias de igual o similar tamao, se ordena recursivamente cada una n de ellas, y nalmente se obtiene una secuencia ordenada fusionando las dos subsecuencias ordenadas. Si la secuencia es sucientemente pequea puede emplearse un mtodo ms simple (y n e a ecaz para entradas de tamao reducido). n

Secuencias

109

Supondremos que nuestra entrada es una lista enlazada L conteniendo una secuencia de elementos x1, . . . , xn. Cada elemento se almacena en un nodo con dos campos: info contiene el elemento y next es un apuntador al siguiente nodo de la lista. La propia lista L es, de hecho, un apuntador al primer nodo.
struct nodo { T info ; nodo * next ; }; typedef nodo * lista ; void mergesort ( lista & L , int n ) { if ( n <= 1) return ; int m = n / 2; lista R = NULL ; // dividir la lista L en dos sublistas // L y R , con m y n - m elementos resp . split (L , R , m ); mergesort (L , m ); mergesort (R , n - m ); // fusionar las listas L y R L = merge (L , R ); }

Secuencias

110

Veamos ahora la accin split que divide una lista o de longitud n > 1 en dos sublistas conteniendo, respectivamente, los primeros m n elementos de la lista y los restantes n m elementos. Es destructiva.
void split ( lista & L1 , lista & L2 , int m ) { nodo * p = L1 ; while ( m > 1) { p = p -> next ; --m ; }; L2 = p -> next ; p -> next = NULL ; }

Secuencias

111

Para fusionar dos listas L1 y L2 razonamos de la siguiente forma: Si L1 L2 es vac la lista o a resultante es la otra lista, es decir, la que eventualmente es no vac Si ambas son no vac a. as comparamos sus respectivos primeros elementos: el menor de los dos ser el primer elemento de la lista a fusionada resultante y a continuacin vendr la lista o a resultado de fusionar la sublista que sucede a ese primer elemento con la otra lista.
lista merge ( lista L1 , lista L2 ) { if ( L1 == NULL ) return L2 ; if ( L2 == NULL ) return L1 ; if ( L1 -> info <= L2 -> info ) { L1 -> sig = merge ( L1 -> sig , L2 ); return L1 ; } else { L2 -> sig = merge ( L1 , L2 -> sig ); return L2 ; } }

Secuencias

112

Secuencias

113

Cada elemento de L1 y L2 es visitado exactamente una vez, por lo que el coste de la operacin merge es proporcional a la suma de los o tamaos de las listas L1 y L2; es decir, su coste es n (n). La funcin denida es recursiva nal y o podemos obtener con poco esfuerzo una versin o iterativa algo ms eciente. Observese que si el a primer elemento de la lista L1 es igual al primer elemento de L2 se pone en primer lugar al que proviene de L1, por lo que mergesort es un algoritmo estable. El coste de mergesort viene descrito por la recurrencia M(n) = (n) + M n 2 +M n 2

n = (n) + 2 M 2 cuya solucin es M(n) = (n log n), aplicando el o teorema de resolucin de recurrencias o divide-y-vencers. a

Secuencias

114

 

Pilas

Una pila (en ingls, stack, LIFO) es una secuencia e en la que inserciones y borrados se producen en un extremo, la cima de la pila. Cualquier pila puede obtenerse a partir de la pila vac aplicando a exclusivamente la operacin de apilar. o
template < typename T > class Pila { public : Pila () throw ( error ); ... void apilar ( const T & x ) throw ( error ); T cima () const throw ( error ); void desapilar () throw ( error ); bool es_vacia () const throw (); ... };

Secuencias

115

Nuestra primera implementacin de la clase Pila o ser una implementacin en vector, ventajosa en a o tiempo ya que todas las operaciones se producen en un extremo. El principal inconveniente de esta implementacin reside en que el espacio de memoria o destinado a almacenar los elementos de una pila o es independiente del tamao de sta y puede n e producirse un error si la pila se llena, o bien requiere ser ampliado cada vez que la pila se llene (redimensionado) con un considerable coste en tiempo. En cualquiera de los dos casos puede haber un considerable desperdicio de espacio de memoria. Aqu presentaremos la primera alternativa, empleando un vector de tamao prejado. n
template < typename T > class Pila { public : ... static const int MAXELEMS = ...; private : T cont [ MAXELEMS ]; int cim ; };

Secuencias

116

El invariante de la representacin establece que los o n elementos de la pila se ubican en las primeras n componentes del vector, ocupando el ultimo en ser apilado la componente n 1. El campo cima indica el nmero de elementos en la pila y, eventualmente, u la posicin de la primera componente libre en el o vector.

Secuencias

117

template < typename T > Pila :: Pila () : cim (0) {}; template < typename T > void Pila :: apilar ( const T & x ) throw ( error ) { if ( cim == MAXELEMS ) throw error ( PilaLlena ); cont [ cim ++] = x ; } template < typename T > void Pila :: desapilar () throw ( error ) { if ( cim == 0) throw error ( PilaVacia ); -- cim ; } template < typename T > T Pila :: cima () throw ( error ) { if ( cim == 0) throw error ( PilaVacia ); return cont [ cim - 1]; } template < typename T > bool Pila :: es_vacia () const throw () { return cim == 0; }

Secuencias

118

Los inconvenientes bsicos de la implementacin de a o pilas mediantes vectores se pueden subsanar mediante una implementacin enlazada con o memoria dinmica. a Cada nodo contendr un elemento y un apuntador a al nodo insertado inmediatamente antes. La pila consistir en un apuntador al nodo de la cima (NULL a si la pila es vac a).
template < typename T > class Pila { public : ... private : struct nodo { T info ; nodo * sig ; }; nodo * cim ; };

Todas las operaciones pueden implementarse de modo que su coste sea (1). El coste en espacio es proporcional al nmero de elementos en la pila u ((n)); su desventaja en comparacin con la o implementacin en vector reside en el overhead en o espacio de los apuntadores y el coste levemente mayor del acceso a travs de apuntadores frente el e acceso indexado.
Secuencias 119

Los usos principales de las pilas son la transformacin recursivo-iterativa de programas o la implementacin a bajo nivel de funciones y proo cedimientos: paso de parmetros, variables locales, a ...

Secuencias

120

 

Colas

Una cola (cat: cua; ing: queue, FIFO) es una secuencia en la que las inserciones se producen en un extremo (el nal de la cola) y los borrados y consultas en el extremo opuesto (el frente de la cola). Cualquier cola puede obtenerse a partir de la cola vac aplicando exclusivamente la operacin de a o encolar.
template < typename T > class Cola { public : Cola () throw ( error ); ... void encolar ( const T & x ) throw ( error ); T frente () const throw ( error ); void desencolar () throw ( error ); bool es_vacia () const throw (); ... };

Secuencias

121

Comenzamos examinando una implementacin en o vector de la clase. Las ventajas e inconvenientes de la implementacin en vector son las mismas que en o el caso de las pilas. No obstante, mantener como invariante de la representacin que los n elementos o de una cola se siten en las primeras n componentes u del vector dar origen a que la operacin a o desencolar (o la operacin encolar, si los o elementos se ubican en orden inverso) sea ineciente, con coste (n). Por lo tanto, mantendremos los elementos de la cola en un segmento del vector, delimitado por dos ndices, prim y ult. Si prim > ult entonces la cola est vac a a.

Secuencias

122

Ahora bien, podr suceder que ult apunte a la a ultima componente del vector, no quedando espacio libre a continuacin, pero que haya varias o componentes libres previas a la indicada por prim. Una opcin ser desplazar el segmento o a cont[prim..ult] a cont[0..ult prim] cuando tuvieramos que encolar un nuevo elemento y ult = MAXELEMS, pero entonces incurrir amos de vez en cuando en un elevado coste ((n)).

Secuencias

123

Pero cerrando circularmente el vector soslayamos el problema y conseguimos que todas las operaciones tengan coste (1). Los elementos de la cola se sitan en las componentes indicadas por u prim y ult, pero consideramos que a la ultima componente cont[MAXELEMS 1] le sucede la primera cont[0]. Un problema que se nos presenta con esta representacin es la dicultad de distinguir o entre una cola vac y una completamente llena! a Suele emplearse un campo adicional con el nmero u de elementos en la cola, un campo booleano que indique si la cola est vac o dejar la componente a a prim 1 (modular) siempre libre.
template < typename T > class Cola { public : ... static const int MAXELEMS = ...; private : T cont [ MAXELEMS ]; int prim , ult ; int num_elems ; };

Secuencias

124

Secuencias

125

template < typename T > Cola :: Cola () throw ( error ) : prim (0) , ult ( MAXELEMS -1) , num_elems (0) {}; template < typename T > void Cola :: encolar ( const T & x ) throw ( error ) { if ( num_elems == MAXELEMS ) throw error ( ColaLlena ); ult = ( ult + 1) % MAXELEMS ; cont [ ult ] = x ; ++ num_elems ; } template < typename T > void Cola :: desencolar () throw ( error ) { if ( num_elems == 0) throw error ( ColaVacia ); prim = ( prim + 1) % MAXELEMS ; -- num_elems ; } template < typename T > T Cola :: frente () throw ( error ) { if ( num_elems == 0) throw error ( ColaVacia ); return cont [ prim ]; } template < typename T > bool Cola :: es_vacia () const throw () { return num_elems == 0; }

Secuencias

126

Tambin podemos decantarnos por una e implementacin enlazada con memoria dinmica, o a evitando algunos de los problemas principales de las implementaciones en vector. Cada nodo contendr un elemento y un apuntador al nodo a insertado inmediatamente despus. La cola e consistir en sendos apuntadores a los nodos inicial a y nal de la cola (ambos sern NULL si la cola a est vac Puede ser ventajoso emplear un a a). elemento fantasma, pero esta vez no lo haremos para ilustrar una estructura enlazada que no lo usa. Todas las operaciones pueden implementarse de modo que su coste sea (1). El coste en espacio es proporcional al nmero de elementos en la cola u ((n)).
template < typename T > class Cola { public : ... private : struct nodo { T info ; nodo * sig ; }; nodo * prim , ult ; };

Secuencias

127

template < typename T > Cola :: Cola () throw ( error ) : prim ( NULL ) , ult ( NULL ) {} template < typename T > void Cola :: encolar ( const T & x ) throw ( error ) { nodo * p = new nodo ; p -> info = x ; p -> sig = NULL ; if ( ult == NULL ) prim = p ; else ult -> sig = p ; ult = p ; } template < typename T > void Cola :: desencolar () throw ( error ) { if ( prim == NULL ) throw error ( ColaVacia ); nodo * p = prim ; prim = prim -> sig ; if ( prim == NULL ) ult = NULL ; delete p ; } template < typename T > T Cola :: frente () const throw ( error ) { if ( prim == NULL ) throw error ( ColaVacia ); return prim -> info ; } template < typename T > bool Cola :: es_vacia () const throw () { return prim == NULL ; }

Secuencias

128

Algunos usos de las colas incluyen: recorrido por niveles de rboles y en anchura de a grafos simulacin o colas de impresin, de trabajos en batch, de meno sajes, de eventos, . . . buers

Secuencias

129

IV Arboles

Los rboles son objetos matemticos que aparecen a a muy frecuentemente en la Informtica, en todos sus a mbitos. La denicin ms general de rbol es la a o a a que se da en teor de grafos. a Denicin 3. Un rbol (libre) T es un grafo no o a dirigido conexo y ac clico. Pero en la mayor parte de las ocasiones se emplean rboles enraizados y orientados: enraizados, porque a se distingue uno de los nodos del rbol como ra a z, induciendo una relacin jerrquica anloga en los o a a subrboles; y orientados, porque se impone un a orden entre los subrboles de cada nodo. a

Arboles

131

La denicin que manejaremos para rboles o a (generales) es la siguiente: Denicin 4. Un rbol T de tamao n, n > 0, es o a n una coleccin de n nodos, uno de los cuales se deo nomina ra y los n 1 nodos restantes forman z, una secuencia de k 0 rboles T1, . . . , Tk de tamaos a n n1, . . . , nk , conectados a la ra y tales que z, ni > 0, para 1 i k; n 1 = n1 + n2 + + nk .

Arboles

132

En un ligero abuso del lenguaje, se dice que los k subrboles o hijos de un rbol T de ra r son a a z subrboles o hijos de r, confundiendo un rbol con a a el nodo que es su ra Anlogamente, se dice que z. a las ra r1, . . . , rk de los subrboles de r son hijos ces a de r y que r es el padre de los ris. Los nodos ri son hermanos ya que tienen el mismo padre. Todo nodo de T , excepto la ra tiene exactamente un padre. z, El grado de un nodo x es el nmero de hijos de x. u Dado un rbol T y cualquier nodo x de T existe un a camino unico desde la ra hasta x. Se dice que un z nodo x es un descendiente de un nodo y si y est en a el camino unico de la ra a x. A la inversa, y es un z antecesor de x.

Arboles

133

Si un nodo x de un rbol T no tiene descendientes a (es decir, su grado es 0) se dice que x es una hoja. La profundidad de un nodo x es la longitud del camino entre la ra y el nodo x. Los nodos de T z cuya profundidad es k constituyen el nivel k de T . La altura de un nodo x es la longitud mxima entre a x y las hojas que son descedientes de x. La altura de T es la altura de su ra y coincide con el nmero de z u niveles menos uno.

Arboles

134

Arboles

raiz
u

altura = 4 ~ tamano = 18 grado = 4


v

nivel = 2
x y

hojas y es padre de z z es hijo de y

hermanos

135

Otro concepto importante es el de bosque. Un bosque es una secuencia de rboles. Un bosque a puede ser vac o no. o Las operaciones que veremos a continuacin se o denen sobre rboles, si bien en toda propiedad a deber denirse sobre bosques. As la operacin an , o raiz deber devolvernos un iterador sobre el nodo a ra del primer rbol del bosque o un iterador nulo si z a el bosque es vac La operacin primogenito o. o deber devolvernos un iterador a la ra del primer a z rbol del bosque formado por los hijos del iterador a sobre el que se aplica, y la operacin sig herm o deber devolvernos un iterador a la ra del a z siguiente rbol en el bosque (todo iterador apunta a a la ra de un rbol que es parte de un cierto z a bosque). De todas formas, en la especicacin que viene a o continuacin hablaremos simplemente de rboles, o a siguiendo la costumbre, aunque ello conlleva una leve prdida de rigor. e

Arboles

136

template < typename T > class Arbol { public : // construye un rbol consistente en un a // nico nodo que contiene a x u Arbol ( const T & x ) throw ( error ); // ctor por copia , asignaci n y dtor o ~ Arbol () throw (); Arbol ( const Arbol & B ) throw ( error ); Arbol & operator =( const Arbol & B ) throw ( error ); // coloca el rbol dado como primer hijo a // de la ra z del rbol sobre el que se a // aplica el m todo y el rbol A queda e a // invalidado ; despu s de aplicar e // B . plantar ( A ) , A no es un rbol v lido a a void plantar ( Arbol & A ) throw ( error ); // iteradores sobre rboles generales a friend class iterador { ... // VER SIGUIENTE TRANSPARENCIA == > }; // devuelve un iterador al nodo ra z del // rbol ( un iterador no v lido si el a a // rbol es inv lido ) a a iterador raiz () const throw (); // devuelve un iterador no v lido a iterador fin () const throw (); private : ... // constructor por defecto privado Arbl () throw ( error ); };

Arboles

137

template < typename T > class Arbol { ... // iteradores sobre rboles generales a friend class iterador { public : friend class Arbol ; // construye un iterador nulo iterador () throw (); // devuelve el sub rbol al que apunta a // el iterador ; lanza error si no es // v lido a Arbol <T > arbol () const throw ( error ); // devuelve el elemento en el nodo al que // apunta el iterador o lanza un error si // el iterador no es v lido a T operator *() const throw ( error ); // devuelve un iterador al primog nito del e // nodo al que apunta ; lanza un error si // el iterador no es v lido a iterador primogenito () const throw ( error ); // devuelve un iterador al siguiente hermano // del nodo al que apunta ; lanza un error // si el iterador no es v lido a iterador sig_hermano () const throw ( error ); // operadores relacionales bool operador ==( const iterador & it ) const throw (); bool operador !=( const iterador & it ) const throw (); private : ... }; ... };

Arboles

138

 

Recorridos de rboles a

Un recorrido de un rbol (cat: recorregut; ing: a traversal) visita todos los nodos de un rbol de a manera sistemtica, sin repeticiones y con arreglo a a un cierto orden prejado. Para jar ideas, supondremos que el propsito de un recorrido es o crear una lista con todos los elementos del rbol en a el orden deseado. 1. En preorden: se visita la ra y a continuacin los z o nodos en los subrboles respetando el orden de a stos. e 2. En postorden: se visitan los nodos en los subrboa les respetando el orden de stos y nalmente se e visita la ra z. 3. Por niveles: se visitan los nodos por niveles, de menor a mayor nivel, y respetando el orden de los subrboles. a

Arboles

139

b g

h m

Preorden: a, b, f, g, l, m, h, c, d, i, n, o, p, e, j, k Postorden: f, l, m, g, h, b, c, n, o, p, i, d, j, k, e, a Niveles: a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p

Arboles

140

// Postcond : Lpre contiene los elementos // de A en preorden ; Lpost contiene los // elementos de A en postorden template < typename T > void rec_orden ( const Arbol <T >& A , list <T >& Lpre , list <T >& Lpost ) { rec_orden ( A . raiz () , A . fin () , Lpre , Lpost ); } template < typename T > void rec_orden ( Arbol <T >:: iterador it , Arbol <T >:: iterador end , list <T >& Lpre , list <T >& Lpost ) { if ( it != end ) { Lpre . push_back (* it ); rec_orden ( it . primogenito () , end , Lpre , Lpost ); Lpost . push_back (* it ); rec_orden ( it . sig_hermano () , end , Lpre , Lpost ); } }

Arboles

141

Para recorrer un rbol por niveles emplearemos una a cola de rboles, donde mantendremos aquellos a rboles cuya ra no se ha visitado. Inicialmente la a z cola contiene slo el rbol cuyos nodos quieren o a recorrerse. El proceso a seguir es simple: se extrae un nodo de la cola, se visita, se colocan todos sus hijos en la cola y se repite el proceso.
// Postcond : La lista Lniv contiene los // elementos de A por niveles template < typename T > void rec_niveles ( const Arbol <T >& A , list <T >& Lniv ) { queue < Arbol <T >:: iterador > Q ; Q . push_back ( A . raiz ()); while (! Q . empty ()) { Arbol <T >:: iterador it = Q . pop_front (); Lniv . push_back (* it ); it = it . primogenito (); while ( it != A . fin ()) { Q . push_back ( it ); it = it . sig_hermano (); } } }

Arboles

142

1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 visitados 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000 2 1 3 1111111111111111 0000000000000000 1111111111111111 0000000000000000 1111111111111111 0000000000000000
4 n

k k+1

Arboles

143

Una clase particular de rboles de considerable a importancia son los rboles m-arios. a Denicin 5. Un rbol T es m-ario si todos sus o a nodos o son hojas o tienen grado m. Frecuentemente se desea que las hojas de un rbol a m-ario no almacenen informacin, por lo que o modicaremos la signatura de modo que un rbol a que consiste en una hoja (un solo nodo) no contiene ningn elemento y se dice vac u o.

Arboles

144

template < typename T > class Arb_m_Ary { public : // construye un rbol m - ario vac o a Arb_m_Ary ( int m ) throw ( error ); // ctor por copia , asignacion y dtor ~ Arb_m_Ary () throw (); Arb_m_Ary ( const Arb_m_Ary & B ) throw ( error ); Arb_m_Ary & operator =( const Arb_m_Ary & B ) throw ( error ); // constructora : crea un rbol m - ario cuya a // ra z contiene al elemento x , y con // sub rboles a [0] , a [1] , ... , a [m -1]; a // los sub rboles a [ i ] se destruyen , es a // decir , despu s de aplicar la constructora e // a [ i ] es vac o ; se asume que a [] // contiene al menos m sub rboles , en caso a // contrario el comportamiento es indefinido ; // puede propagar un error de memoria din mica a // si falta memoria para el nodo ra z Arb_m_Ary ( const T & x , Arb_m_Ary a []) throw ( error ); // devuelve cierto ssi el rbol es vac o a bool es_vacio () const throw (); // iteradores sobre arboles m - arios friend class iterador { ... }; // devuelve un iterador a la ra z iterador raiz () const throw (); // devuelve un iterador no v lido a iterador fin () const throw (); private : ... };

Arboles

145

template < typename T > class Arb_m_Ary { public : ... friend class iterador { public : friend class Arb_m_Ary ; iterador () throw (); // devuelve el sub rbol al que apunta a // el iterador ; lanza error si no es // v lido a Arb_m_ary <T > arbol () const throw ( error ); // devuelve el elemento en el nodo al // que apunta el iterador , o lanza un // error si el iterador no es v lido a T operator *() const throw ( error ); // devuelve un iterador al hijo i - simo e // del nodo apuntado ; lanza un error si // el iterador no es v lido a // o i est fuera del rango 0.. m -1 a iterador hijo ( int i ) const throw ( error ); // operadores relacionales bool operator ==( const iterador & it ) const throw (); bool operator !=( const iterador & it ) const throw (); private : ... }; ... };

Arboles

146

En el caso de los rboles binarios (m = 2) se utiliza a la nomenclatura hijo izquierdo e hijo derecho para referirse a los subrboles de un rbol no vac En la a a o. representacin grca suele omitirse la o a representacin de las hojas o subrboles vac o se o a os representan mediante un s mbolo distinto. Los recorridos denidos para los rboles generales a tambin se aplican a los rboles m-arios. Adems, e a a en el caso de los rboles binarios se dene el a recorrido en inorden: se visita en primer lugar el subrbol izquierdo, a continuacin la ra y a o z, nalmente el subrbol derecho. a

Arboles

147

// Postcond : Lin contiene los elementos // de A en inorden template < typename T > void rec_inorden ( const ArbBin <T >& A , list <T >& Lin ) { rec_inorden ( A . raiz () , A . fin () , Lin ); } template < typename T > void rec_inorden ( ArbBin <T >:: iterador it , ArbBin <T >:: iterador end , list <T >& Lin ) { if ( it != end ) { rec_inorden ( it . hijo_izq () , end , Lin ); Lin . push_back (* it ); rec_inorden ( it . hijo_der () , end , Lin ); } }

b d g

e h i

j Inorden: g, d, j, h, b, e, i, a, c, f

Arboles

148

El tamao de un rbol m es el nmero de nodos que n a u contiene (las hojas o subrboles vac no se a os contabilizan. Lema 1. La altura h(T ) de un rbol m-ario de taa mao n satisface n logm((m 1)n + 1) h(T ) n. En particular, si m = 2, h(T ) log2(n + 1) . Demostracin. Sea h la altura de T . Como mximo o a T tendr n = 1 + m + m2 + m3 + + mh1 = (mh a 1)/(m 1) nodos. Luego h logm((m 1)n + 1).

Arboles

149

Lema 2. El nmero de hojas (subrboles vac u a os) de un rbol m-ario de tamao n es (m 1)n + 1. a n Demostracin. Por induccin estructural. Si n = 0, o o efectivamente hay 1 hoja. Si n > 0, cada uno de los m subrboles contribuye (m 1)ni + 1 hojas. Sumando a para i = 1 a i = m, obtenemos (m 1)(n1 + + nm) + m = (m 1)(n 1) + m = (m 1)n + 1. Veamos una demostracin alternativa. Para repreo sentar un rbol m-ario con n nodos internos se nea cesitan m n + 1 apuntadores: m por cada uno de los n nodos, ms uno para la ra De esos m n + 1 a z. apuntadores slo n son no nulos ya que apuntan a o nodos (para cada nodo slo hay una echa entrano te). Los restantes punteros son nulos; en total son m n + 1 n = (m 1) n + 1. Cada apuntador nulo representa un hoja, por lo que queda demostrado el lema.

Arboles

150

Existe una correspondencia biun voca entre bosques generales y rboles binarios. Esta correspondencia a saltar a la vista cuando estudiemos las respectivas a implementaciones. Dado un rbol binario T , la ra de T se a z corresponde con la ra del primer rbol del bosque z a B que corresponde a T . El subrbol izquierdo de T a corresponde al bosque de hijos del primer rbol de a B. El subrbol derecho de T corresponde al resto del a bosque B.

Arboles

151

T1

T2

T3

T4

B1

B2

B3

B4

Arboles

152

Los rboles tienen muchos usos. Entre ellos a destacamos los siguientes: Representacin de expresiones y rboles sintctio a a cos (parse trees) Sistemas de cheros (jerarqu de subdirectorioa s/carpetas y cheros) Implementacin de colas de prioridad y de partio ciones (mfsets) Implementacin de diccionarios e o ndices Arboles binarios de bsqueda u Tries B-trees ...

Arboles

153

La implementacin general de rboles consiste en o a almacenar los elementos en nodos cada uno de los cuales contiene una lista de apuntadores (o cursores) a las ra de sus subrboles. Si el grado ces a mximo de los nodos est acotado y es pequeo a a n entonces es razonable que la lista est implementada en vector, posibilitando una e implementacin eciente del acceso a los hijos por o posicin. Este tipo de implementacin es o o particularmente atractiva para rboles m-arios con a m pequeo. Un rbol se representa de hecho como n a un apuntador al nodo ra z.
template < typename T > class Arbol { ... private : Arbol () throw ( error ); static const MAXHIJOS = ...; struct nodo { T info ; nodo * hijo [ MAXHIJOS ]; }; nodo * raiz ; };

Arboles

154

Si el grado mximo no est acotado, es grande o a a hay mucha variacin posible, entonces o convendr representar la lista de hijos mediante una a lista enlazada. Esta forma de representacin se o denomina primognito-siguiente hermano por e razones obvias. Cada nodo contiene un elemento y sendos apuntadores, a su primer hijo y a su siguiente hermano, respectivamente. Tanto un rbol como a una secuencia de rboles (bosque) tienen la misma a representacin: un apuntador a un nodo ra o z.
template < typename T > class Arbol { ... private : Arbol () throw ( error ); struct nodo { T info ; nodo * primg ; nodo * sigherm ; }; nodo * raiz ; };

Arboles

155

La implementacin de los iteradores sobre rboles es o a trivial: simplemente basta un atributo que es un apuntador a un nodo del rbol; si el apuntador es a nulo, el iterador no es vlido. a
template < typename T > class Arbol { ... friend class iterador { friend class Arbol ; ... private : Arbol <T >:: nodo * p ; } ... }

Arboles

156

template < typename T > Arbol <T >:: Arbol () throw ( error ) : raiz ( NULL ) { } template < typename T > void Arbol <T >:: plantar ( Arbol & A ) throw ( error ) { if ( raiz == NULL ) throw error ( ArbolInvalido ); if ( A . raiz == NULL A . raiz -> sigherm != NULL ) throw error ( ArbolInvalido ) A . raiz -> sigherm = raiz -> primg ; raiz -> primg = A . raiz ; A . raiz = NULL ; } template < typename T > Arbol <T >:: iterador Arbol <T >:: raiz () const throw () { iterador it ; it . p = raiz ; return it ; } template < typename T > Arbol <T >:: iterador Arbol <T >:: fin () const throw () { iterador it ; it . p = NULL ; return it ; }

Arboles

157

template < typename T > T Arbol <T >:: iterador :: operator *() const throw ( error ) { if ( p == NULL ) throw error ( IteradorInvalido ); return p -> info ; } template < typename T > Arbol <T >:: iterador Arbol <T >:: iterador :: primogenito () const throw ( error ) { if ( p == NULL ) throw error ( IteradorInvalido ); iterador it ; it . p = p -> primg ; return it ; } template < typename T > Arbol <T >:: iterador Arbol <T >:: iterador :: sig_hermano () const throw ( error ) { if ( p == NULL ) throw error ( IteradorInvalido ); iterador it ; it . p = p -> sigherm ; return it ; }

Arboles

158

// Constructora por copia , destructora // y asignaci n o template < typename T > Arbol <T >:: Arbol ( const Arbol & A ) throw ( error ) { raiz = copia_arbol ( A . raiz ); } template < typename T > Arbol <T >::~ Arbol () throw () { destruye_arbol ( raiz ); } template < typename T > Arbol <T >& Arbol <T >:: operator =( const Arbol & A ) throw ( error ) { Arbol <T > tmp ( A ); nodo * aux = raiz ; raiz = tmp . raiz ; tmp . raiz = aux ; return * this ; }

Arboles

159

// la destrucci n es un recorrido postorden o template < typename T > void Arbol <T >:: destruye_arbol ( nodo * p ) throw () { if ( p != NULL ) { destruye_arbol ( p -> primg ); destruye_arbol ( p -> sigherm ); delete p ; } } // la copia es un recorrido preorden template < typename T > Arbol <T >:: nodo * Arbol <T >:: copia_arbol ( nodo * p ) throw ( error ) { if ( p == NULL ) return NULL ; nodo * aux = new nodo ; try { aux -> primg = aux -> sigherm = NULL ; aux -> info = p -> info ; aux -> primg = copia_arbol ( p -> primg ); aux -> sigherm = copia_arbol ( p -> sigherm ); } catch ( error ) { destruye_arbol ( aux ); throw ; } return aux ; }

Arboles

160

Arboles

161

Hay algunas tcnicas que se aplican slo en casos e o particulares. Por ejemplo Para rboles m-arios casi-completos puede utilia zarse una representacin en vector: A[1] contiene o la ra y en general, los hijos del nodo A[i] se z, encuentran en las componentes A[m i], A[m i + 1], . . . , A[m i + m 1]. Ocasionalmente, se requiere acceso al padre, para lo cual cada nodo incluir un apuntador a su a padre.

Arboles

162

V Diccionarios

Un diccionario (tabla asociativa, tabla de s mbolos, estructura funcional) es una estructura de datos que contiene un conjunto de elementos, cada uno de los cuales tiene una clave que lo identica un vocamente, y ofrece operaciones que permiten localizar un elemento dada su clave (bsqueda por u clave). Por ejemplo, un diccionario podr contener a informacin sobre los estudiantes de una facultad y o permitir el acceso a la informacin de un estudiante o en particular dado su nombre completo o dado su DNI (dependiendo de qu se haya escogido como e clave de identicacin). o

Diccionarios

164

Existen varias caracterizaciones equivalentes del modelo matemtico de los diccionarios: a 1. Un diccionario D es una funcin parcial nita que o va de un conjunto de claves K a un conjunto de valores V . 2. Un diccionario D es una funcin total del conjunto o de claves K en el conjunto V {}, donde V es el conjunto de valores y V es un valor especial, y el subconjunto de claves {k K | D(k) V } es nito. 3. Un diccionario D es un conjunto nito de pares (k, v) con k K y v V tal que no hay dos pares distintos con idntica clave. e 4. Un diccionario D es un conjunto nito de elementos, cada uno de los cuales tiene una clave (que denotaremos clave(x)), y tales que no hay dos elementos en el conjunto con igual clave.
Diccionarios 165

Un caso particular de diccionario lo constituyen los conjuntos (en el sentido clsico del trmino). a e Para ello basta considerar un conjunto como subconjunto nito de K y el diccionario como la funcin caracter o stica del conjunto (V = {cierto} y falso). Equivalentemente podemos tomar la ultima de las caracterizaciones vista anteriormente con clave(x) = x. Obviamente, en un conjunto la operacin que o obtiene el valor asociado a una clave dada no es ms que una operacin de pertenencia. a o

Diccionarios

166

Una primera clasicacin de los TADs diccionario se o hace con arreglo a las operaciones de actualizacin o permitidas: En un diccionario esttico los n elementos que lo a formarn se conocen de antemano; no se permiten a inserciones o eliminaciones posteriores. El TAD ofrece una operacin para crear un diccionario a o partir de un vector o lista de elementos. En un diccionario semidinmico se permiten insera ciones de nuevos elementos (o pares clave-valor), as como la modicacin del valor asociado a una o clave dada, pero no eliminaciones. T picamente, el TAD ofrece una operacin de creacin de un o o diccionario vac o. En un diccionario dinmico es posible tanto la a insercin de nuevos elementos como la eliminacin o o de elementos presentes en un diccionario, dada su clave.

Diccionarios

167

template < typename Clave , typename Valor > class Diccionario { public : // Constructora . Crea un diccionario vac o . Diccionario () throw ( error ); // Destructora , constr . por copia y asignaci n . o ~ Diccionario () throw (); Diccionario ( const Diccionario & src ) throw ( error ); Diccionario & operator =( const Diccionario & src ) throw ( error ); // A~ ade el par <k , v > al diccionario si n // no hab a ning n par con la clave k ; no u // hace nada en caso contrario . void inserta ( const Clave & k , const Valor & v ) throw ( error ); // Elimina el par <k , v > si existe un par // con la clave k presente ; no hace nada // en caso contrario . void elimina ( const Clave & k ) throw (); // Devuelve cierto si y s lo si el diccionario o // contiene un par con la clave dada . bool contiene ( const Clave & k ) const throw (); // Devuelve el valor asociado a la clave // dada si existe un par con la clave k , // y lanza una excepci n si la clave no o // existe . Valor valor_asociado ( const Clave & k ) const throw ( error ); ... };

Diccionarios

168

Una familia importante y de considerable inters la e constituyen los llamados diccionarios recorribles u ordenados. Si las claves admiten una relacin de o orden total (esto es, tenemos una operacin de o comparacin < entre claves), entonces puede ser o util que el TAD ofrezca operaciones que permiten examinar los elementos (o pares clave-valor) por orden (de)creciente de sus claves. Se dice que el diccionario es recorrible. Por ejemplo:
template < typename Clave , typename Valor > class DiccionarioRecorr { public : typedef pair < Clave , Valor > pair_cv ; ... // Devuelve una lista con todos los // pares ( clave , valor ) del diccionario // en orden ascendente . void li sta_diccionario ( list < pair_cv >& L ) const throw ( error ); // Funciones para el recorrido en orden // ascedente de claves de todas las // claves mediante un punto de // inter s . e pair_cv primero () throw ( error ); pair_cv siguiente () throw ( error ); bool final () const throw (); ...

Diccionarios

169

Los recorridos en un diccionario tambin pueden e realizarse mediante iteradores.


template < typename Clave , typename Valor > class DiccionarioRecorr { public : typedef pair < Clave , Valor > pair_cv ; friend class iterador { public : friend class DiccionarioRecorr ; iterador (); ... // Dereferencia del iterador . pair_cv operator *() const throw ( error ); // Pre - y postincremento ; avanzan // el iterador iterador & operator ++() throw (); iterador operator ++( int ) throw (); // Operadores relacionales . bool operator ==( const iterador & b ) const throw (); ... }: ... // Iteradores al inicio y final ( centinela ) // del diccionario . iterador inicio () const throw (); iterador final () const throw (); };

Diccionarios

170

Es frecuente que un TAD diccionario incluya operaciones adicionales a las ya mencionadas. Por ejemplo: Operaciones conjuntistas entre unin, interseccin, diferencia, . . . o o diccionarios:

Operaciones por rango: localizar el i-simo elee mento, eliminar el i-simo elemento, . . . e Otras: eliminar todos los elementos cuya clave est comprendida entre dos claves k1 y k2 dadas, e determinar el rango (cuntos elementos con una a clave menor hay) de una clave dada, etc. Determinados tipos de claves (p.e. strings) permiten el uso de ciertas tcnicas de implementacin con e o costes de espacio y tiempo interesantes, y motivan la existencia de operaciones espec cas: p.e. hallar todos los strings de un conjunto de strings que comienzan con un prejo dado.

Diccionarios

171

En circunstancias especiales, por ejemplo, si hemos de implementar un conjunto y K {0, . . . , M 1} para un valor M relativamente pequeo, podemos utilizar representar el conjunto n mediante un vector de M bits. El coste de las operaciones de bsqueda, insercin y eliminacin u o o ser en este caso constante. a Podemos usar una lista enlazada simple desordenada para implementar el diccionario, de modo que cada nodo contenga un par clave-valor. El coste en caso peor de inserciones, borrados y bsquedas es (n) siendo n el nmero de eleu u mentos (o claves) en el diccionario, pero puede ser aceptable si los diccionarios son pequeos. Su n coste en espacio es (n) y todos los algoritmos son muy simples, por lo que puede ser aconsejable en determinadas circunstancias. Si se necesita soportar operaciones de recorrido ordenado no ser a recomendable esta opcin. o

Diccionarios

172

Una lista enlazada desordenada, en combinacin o con una estrategia de autoorganizacin, puede o dar buenos resultados en la prctica, si existe un a grado elevado de localidad de referencia en las bsquedas por clave. u Si el diccionario es esttico o las actualizacioa nes (inserciones y borrados) se producen muy raramente entonces puede convenir mantener el diccionario en un vector ordenado por clave. Las bsquedas por clave se pueden realizar mediante u el algoritmo de bsqueda dicotmica o binaria, u o con coste (log n). Soporta de manera eciente las operaciones de recorrido ordenado.

Diccionarios

173

Las listas enlazadas ordenadas por clave combinan algunas de las ventajas de las listas enlazadas desordenadas y de los vectores ordenados; tambin e heredan algunos de sus inconvenientes. Soportan bien los recorridos ordenados y las operaciones de tipo conjuntista. De todos modos las operaciones de bsqueda y actualizacin tienen coste (n) u o tanto en el caso peor como en el caso medio. Otras implementaciones que examinaremos: Arboles de bsqueda u Tablas de dispersin o Skip lists Arboles digitales (tries y variantes)

Diccionarios

174

Arboles binarios de bsqueda u


 

Denicin 6. Un rbol binario de bsqueda T es un o a u rbol binario tal que o es vac o bien contiene un a o elemento x y satisface 1. Los subrboles izquierdo y derecho, L y R, resa pectivamente, son rboles binarios de bsqueda. a u 2. Para todo elemento y de L, clave(y) < clave(x), y para todo elemento z de R, clave(z) > clave(x).

BSTs

175

BSTs

gato

"morsa" es mayor que "leon" y "jirafa"; y menor que "pato", "perro", etc

cerdo

morsa

boa

elefante

leon

perro

jirafa

pato

raton

toro

176

Lema 3. Un recorrido en inorden de un rbol bia nario de bsqueda T visita los elementos de T por u orden creciente de clave. Vamos a considerar ahora el diseo del algoritmo de n bsqueda en rboles binarios de bsqueda (BSTs, u a u en lo sucesivo). Dada la naturaleza recursiva de la denicin de BST es razonable abordar el diseo de o n este algoritmo de manera recursiva. Sea T el BST que representa al diccionario y k la clave buscada. Si T = vacio entonces k no se encuentra el diccionario y ello se habr de indicar de a algn modo conveniente. Si T no es vac entonces u o tendremos que considerar la relacin que existe o entre la clave del elemento x que ocupa la ra de T z y la clave dada k.

BSTs

177

Si k = clave(x) la bsqueda ha tenido xito y u e nalizamos, retornando el elemento x (o la informacin asociada a x que nos interesa). Si o k < clave(x), se sigue de la denicin de los BSTs, o que si hay un elemento en T cuya clave es k entonces dicho elemento se habr de encontrar en el a subrbol izquierdo de T , por lo que habr que a a efectuar una llamada recursiva sobre el hijo izquierdo de T . Anlogamente, si k > clave(x) a entonces la bsqueda habr de continuar u a recursivamente en el subrbol derecho de T . a

BSTs

178

template < typename Clave , typename Valor > class Diccionario { public : ... void busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ); void inserta ( const Clave & k , const Valor & v ) throw ( error ); void elimina ( const Clave & k ) throw (); ... private : struct nodo_bst { Clave _k ; Valor _v ; nodo_bst * _izq ; nodo_bst * _der ; // constructora de la clase nodo_bst nodo_bst ( const Clave & k , const Valor & v , nodo_bst * izq = NULL , nodo_bst * der = NULL ); }; nodo_bst * raiz ; static nodo_bst * busca_en_bst ( nodo_bst * p , const Clave & k ) throw (); static nodo_bst * inserta_en_bst ( nodo_bst * p , const Clave & k , const Valor & v ) throw ( error ); static nodo_bst * elimina_en_bst ( nodo_bst * p , const Clave & k ) throw (); static nodo_bst * juntar ( nodo_bst * t1 , nodo_bst * t2 ) throw (); static nodo_bst * reubicar_max ( nodo_bst * p ) throw (); ... }

BSTs

179

El mtodo busca emplea el mtodo privado e e busca en bst. Este ultimo recibe un apuntador p a la ra del BST en el que se ha de hacer la bsqueda z u y una clave k. Devuelve un apuntador, o bien nulo, si k no est presente en el BST, o bien que apunta a al nodo del BST que contiene la clave k.
template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ) { nodo_bst * p = busca_en_bst ( raiz , k ); if ( p == NULL ) esta = false ; else { esta = true ; v = p -> _v ; } }

BSTs

180

La implementacin recursiva del mtodo privado o e busca en bst es casi inmediata a partir de la denicin de BST. Si el rbol es vac su ra o a o o z contiene la clave k, devolvemos el apuntador p ya que apunta al lugar correcto (a nulo o al nodo con la clave). En caso contrario, se compara k con la clave almacenada en el nodo ra al que apunta p y z se prosigue recursivamente la bsqueda en el u subrbol izquierdo o derecho, segn toque. a u
// implementaci n recursiva o template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: busca_en_bst ( nodo_bst * p , const Clave & k ) throw () { if ( p == NULL || k == p -> _k ) return p ; // p != NULL and k != p -> _k if ( k < p -> _k ) return busca_en_bst ( p -> _izq , k ); else // p -> _k < k return busca_en_bst ( p -> _der , k ); }

BSTs

181

Puesto que el algoritmo de buqueda recursivo es recursivo nal es inmediato obtener una versin o iterativa.
// implementaci n iterativa o template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: busca_en_bst ( nodo_bst * p , const Clave & k ) throw () { while ( p != NULL && k != p -> _k ) { if ( k < p -> _k ) p = p -> _izq ; else // p -> _k < k p = p -> _der ; } return p ; }

BSTs

182

El algoritmo de insercin es tambin o e extremadamente simple y se obtiene a partir de un razonamiento similar al utilizado para desarrollar el algoritmo de bsqueda: si la nueva clave es menor u que la clave en la ra entonces el nuevo elemento z se ha de insertar (recursivamente) en el subrbol a izquierda; si es mayor, la insercin se realiza en el o subrbol derecho. a El mtodo pblico inserta se apoya en otro e u mtodo privado de clase llamado inserta en bst. e Este recibe un apuntador a la ra del BST donde se z debe insertar el par k, v y nos devuelve un apuntador a la ra del BST resultante de la z insercin. Si k no aparece en el BST, se aade un o n nodo con el par k, v en el lugar apropiado. Si k ya exist entonces el mtodo modica el valor a, e asociado a k, reemplazando el valor anterior por v.

BSTs

183

Antes de pasar a la implementacin de las o inserciones, veamos la implementacin (trivial) de la o constructora de la clase nodo bst:
template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst ( const Clave & k , Valor & v , nodo_bst * izq , nodo_bst * der ) throw ( error ) : _k ( k ) , _v ( v ) , _izq ( izq ) , _der ( der ) { }

BSTs

184

template < typename Clave , typename Valor > void Diccionario :: inserta ( const Clave & k , const Valor & v ) throw ( error ) { raiz = inserta_en_bst ( raiz , k , v ); } // implementaci n recursiva o template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo * Diccionario < Clave , Valor >:: inserta_en_bst ( nodo_bst * p , const Clave & k , const Valor & v ) throw ( error ) { if ( p == NULL ) return new nodo_bst (k , v ); // // // if p != NULL , contin a la inserci n en el u o sub rbol apropiado o reemplaza el valor a asociado si p -> _k == k ( k < p -> _k ) p -> _izq = inserta_en_bst ( p -> _izq , k , v ); else if ( p -> _k < k ) p -> _der = inserta_en_bst ( p -> _der , k , v ); else // p -> _k == k p -> _v = v ; return p ;

BSTs

185

La versin iterativa es ms compleja, ya que adems o a a de localizar la hoja en la que se ha de realizar la insercin, deber mantenerse un apuntador padre al o a que ser padre del nuevo nodo. a
g < m

< g

g <

a padre

< g

BSTs

186

// implementaci n iterativa o template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: inserta_en_bst ( nodo_bst * p , const Clave & k , const Valor & v ) throw ( error ) { // el BST est vac o a if ( p == NULL ) return new nodo_bst (k , v ); // el BST no est vac o a nodo_bst * padre = NULL ; nodo_bst * p_orig = p ; // buscamos el punto de inserci n o while ( p != NULL && k != p -> _k ) { padre = p ; if ( k < p -> _k ) p = p -> _izq ; else // p -> _k < k p = p -> _der ; } // // // if insertamos el nuevo nodo como hoja o modificamos el valor asociado , si ya hab a un nodo con la clave k dada ( p == NULL ) { if ( k < padre -> _k ) padre -> izq = new nodo_bst (k , v ); else // k > padre -> _k padre -> der = new nodo_bst (k , v );

} else // k == p -> _k p -> _v = v ; return p_orig ; }

BSTs

187

Slo nos queda por considerar la eliminacin de o o elementos en BSTs. Si el elemento a eliminar se encuentra en un nodo cuyos dos subrboles son a vac basta eliminar el nodo en cuestin. Otro os o tanto sucede si el nodo x a eliminar slo tiene un o subrbol no vac basta hacer que la ra del a o: z subrbol no vac quede como hijo del padre de x. a o
z x z

z x

z y

BSTs

188

El problema surge si hay que eliminar un nodo que contiene dos subrboles no vac Podemos a os. reformular el problema de la siguiente forma: dados dos BSTs T1 y T2 tales que todas las claves de T1 son menores que las claves de T2 obtener un nuevo BST que contenga todas las claves: T = juntar(T1, T2). Obviamente: juntar(T, ) = T juntar( , T ) = T En particular, juntar( , ) = .

BSTs

189

template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: elimina ( const Clave & k ) throw () { raiz = elimina_en_bst ( raiz , k ); } template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: elimina_en_bst ( nodo_bst * p , const Clave & k ) throw () { // la clave no est a if ( p == NULL ) return p ; if ( k < p -> _k ) p -> _izq = elimina_en_bst ( p -> _izq , k ); else if ( p -> k < k ) p -> _der = elimina_en_bst ( p -> _der , k ); else { // k == p -> k nodo_bst * to_kill = p ; p = juntar ( p -> _izq , p -> _der ); delete to_kill ; } return p ; }

BSTs

190

Sea z+ la mayor clave de T1. Puesto que es mayor que todas las dems en T1 pero al mismo tiempo a menor que cualquier clave de T2 podemos construir T colocando un nodo ra que contenga al elemento z de clave z+, a T2 como subrbol derecho y al a resultado de eliminar z+ de T1 llammosle T1 e como subrbol izquierdo. Adems puesto que z+ es a a la mayor clave de T1 el correspondiente nodo no tiene subrbol derecho, y es el nodo ms a la a a derecha en T1, lo que nos permite desarrollar un procedimiento ad-hoc para eliminarlo.

T1 z+ z

T2

BSTs

191

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: juntar ( nodo_bst * t1 , nodo_bst * t2 ) throw () { // // if if si uno vac o , ( t1 == ( t2 == de los dos sub rboles es a la implementaci n es trivial o NULL ) return t2 ; NULL ) return t1 ;

// t1 != NULL y tambi n t2 != NULL e nodo_bst * z = reubica_max ( t1 ); z -> _der = t2 ; return z ; // alternativa : z = reubica_min ( t2 ); // z -> _izq = t1 ; // return z ; }

BSTs

192

El mtodo privado reubica max recibe un e apuntador p a la ra de un BST T y nos devuelve z un apuntador a la ra de un nuevo BST T . La ra z z de T es el elemento mximo de T . El subrbol a a derecho de T es nulo y el subrbol izquierdo de T a es el BST que se obtiene al eliminar el elemento mximo de T . a La unica dicultad estriba en tratar adecuadamente la situacin en la que el elemento o mximo del BST se encuentra ya como ra del a z mismodicha situacin debe detectarse y en ese o caso el mtodo no debe hacer nada. e
template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_bst * Diccionario < Clave , Valor >:: reubica_max ( nodo_bst * p ) throw () { nodo_bst * padre = NULL ; nodo_bst * p_orig = p ; while ( p -> _der != NULL ) { padre = p ; p = p -> _der ; } if ( padre != NULL ) { padre -> _der = p -> _izq ; p -> _izq = p_orig ; } return p ; }

BSTs

193

j e r

padre

z+

BSTs

194

Un razonamiento anlogo nos lleva a una versin de a o juntar en la que se emplea la clave m nima z del rbol T2 para que ocupe la ra del resultado, en el a z caso en que T1 y T2 no son vac os. Se ha evidenciado experimentalmente que conviene alternar entre el predecesor z+ y el sucesor z del nodo a eliminar en los borrados (por ejemplo, mediante una decisin aleatoria o con bit que va o pasando alternativamente de 0 a 1 y de 1 a 0) y no utilzar sistemticamente una de las versiones. a Existen otros algoritmos de borrado pero no se estudiarn en esta asignatura. a

BSTs

195

Un BST de n elementos puede ser equivalente a una lista, pues puede contener un nodo sin hijos y n 1 con slo un hijo (p.e. si insertamos una secuencia de o n elementos con claves crecientes en un BST inicialmente vac Un BST con estas o). caracter sticas tiene altura n. En caso peor, el coste de una bsqueda, insercin o borrado en dicho rbol u o a es (n). En general, una bsqueda, insercin o u o borrado en un BST de altura h tendr coste (h) a en caso peor. Como funcin de n, la altura de un o rbol puede llegar a ser n y de ah que el coste de a las diversas operaciones es, en caso peor, (n).

BSTs

196

Pero normalmente el coste de estas operaciones ser menor. Supongamos que cualquier orden de a insercin de los elementos es equiprobable. Para o bsquedas con xito supondremos que buscamos u e cualquiera de las n claves con probabilidad 1/n. Para bsquedas sin xito o inserciones supondremos u e que nalizamos en cualquiera de las n + 1 hojas con igual probabilidad. El coste de una de estas operaciones va a ser proporcional al nmero de comparaciones que u habr que efectuar entre la clave dada y las claves a de los nodos examinados. Sea C(n) el nmero medio u de comparaciones y C(n; k) el nmero medio de u comparaciones si la ra est ocupada por la k-sima z a e clave. C(n) =

1kn

C(n; k) P[ra es k-sima] . z e

BSTs

197

C(n) =

1 C(n; k) n 1kn

= 1+ 1 1 k1 nk 0 + n C(k 1) + n C(n k) n 1kn n = 1+ = 1+ 1 (k C(k) + (n 1 k) C(n 1 k)) 2 n 0k<n 2 k C(k). 2 n 0k<n

Otra forma de plantear esto es calcular I(n), el valor medio de la longitud de caminos internos (IPL). Dado un BST su longitud de caminos internos es la suma de las distancias desde la ra a cada uno de z los nodos. I(n) C(n) = 1 + n

BSTs

198

La longitud media de caminos internos satisface la recurrencia I(n) = n 1 + 2 I(k), n 0k<n I(0) = 0. (1)

Esto es as porque cada nodo que no sea la ra z contribuye al IPL total 1 ms su contribucin al IPL a o del subrbol en que se encuentre. Otra razn por la a o que resulta interesante estudiar el IPL medio es porque el coste de construir un BST de tamao n n mediante n inserciones es proporcional al IPL. Antes de continuar con el estudio del coste examinaremos un algoritmo de ordenacin muy o estrechamente relacionado: el algoritmo de ordenacin rpida (quicksort) inventado por C.A.R. o a Hoare en 1962.

BSTs

199

Quicksort

Quicksort usa el principio de divide y vencers, a pero a diferencia de otros algoritmos divide-y-vencers (p.e. mergesort), no garantiza que a cada subejemplar tendr un tamao que es fraccin a n o del tamao original. n La base de quicksort es el procedimiento de particin: dado un elemento p denominado pivote, o debe reorganizarse un segmento de vector dado de tal modo que todos los elementos menores (o iguales) que el pivote queden a su izquierda y todos los elementos mayores (o iguales) que el pivote queden a su derecha.

BSTs

200

El procedimiento de particin sita a un elemento, o u el pivote, en su lugar apropiado. Luego no queda ms que ordenar los segmentos que quedan a su a izquierda y a su derecha. Mientras que en mergesort la divisin es simple y el trabajo se realiza durante o la fase de combinacin, en quicksort sucede lo o contrario. Para ordenar el segmento A[ ..u] el algoritmo queda as
template < typename Elem > void quicksort ( Elem A [] , int l , int u ) { if ( u - l + 1 <= M ) { // usar aqu un algoritmo de ordenaci n o // simple } else { particion (A , l , u , k ); // A[l..k 1] A[k] A[k + 1..u] quicksort (A , l , k -1); quicksort (A , k +1 , u ); } }

BSTs

201

En vez de usar un algoritmo de ordenacin simple o (p.e. ordenacin por insercin) con cada segmento o o de M o menos elementos, puede ordenarse mediante el algoritmo de insercin al nal: o quicksort(A, 1, n) insercion(A, 1, n) Puesto que el vector A est quasi-ordenado tras a aplicar quicksort, el ultimo paso se hace en tiempo (n), donde n es el nmero de elementos a ordenar. u Se estima que la eleccin ptima para el umbral o o o corte de recursin M oscila entre 20 y 25. o

BSTs

202

Existen muchas formas posibles de realizar la particin. En Bentley & McIlroy (1993) se discute o un procedimiento de particin muy eciente, incluso o si hay elementos repetidos. Aqu examinamos un algoritmo bsico, pero razonablemente ecaz. a Como invariante de la iteracin en todo momento o tendremos el pivote p en A[ ] y el subvector A[ + 1..u] dividido en tres zonas: A[ + 1.. j] contiene elementos menores que p, A[ j + 1..i 1] contiene elementos mayores o iguales que p y A[i..u] los elementos no clasicados todav Consideremos a. A[i]: si es mayor o igual a p, podemos avanzar i, manteniendo el invariante y disminuyendo el tamao de la zona no clasicada; si A[i] es menor n que p podemos intercambiar el primer elemento de A[ j + 1..i] con A[i] e incrementar tanto la i como la j, reestableciendo de este modo el invariante.

BSTs

203

_ < p _ > p 111111 000000 11111111 00000000 111111 000000 11111111 00000000 111111 000000 11111111 00000000 111111 000000 11111111 00000000 111111 11111111 00000000 p 000000 111111 000000 11111111 00000000 111111 000000 11111111 00000000 111111 000000 11111111 00000000 111111 000000 11111111 00000000 l j i _ p >
11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000

_ < p 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000p 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 l j

i= u

// algoritmo de partici n de Lomuto o template < typename Elem > void particion ( Elem A [] , int l , int u , int & k ) { int j = l ; Elem pivot = A [ l ]; for ( int i = l + 1; i <= u ; ++ i ) if ( A [ i ] < pivot ) { ++ j ; swap ( A [ j ] , A [ i ]); } // A[l + 1.. j] pivot A[ j + 1..u] swap ( A [ l ] , A [ j ]); k = j; }

BSTs

204

Otro procedimiento de particin alternativo consiste o en mantener dos ndices i y j de tal modo que A[ + 1..i 1] contiene elementos menores o iguales que el pivote p, y A[ j + 1..u] contiene elementos mayores o iguales. Los ndices barren el segmento (de izquierda a derecha, y de derecha a izquierda, respectivamente), hasta que A[i] > p y A[ j] < p o se cruzan (i = j + 1). El principio de funcionamiento se ilustra en la gura siguiente:
_ < p 111111 000000 111111 000000 111111 000000 111111 000000 111111 p 000000 111111 000000 111111 000000 111111 000000 111111 000000 l i j _ > p
11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000 11111111111111 00000000000000

_ > p 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 1111111 0000000 u

_ < p 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000p 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 1111111111111 0000000000000 l j

BSTs

205

// algoritmo de partici n de Hoare - Sedgewick o template < typename Elem > void particion ( Elem A [] , int l , int u , int & k ) { int i = l + 1; int j = u ; Elem pivot = A [ l ]; do { while ( i < j + 1 && A [ i ] <= pivot ) ++ i ; while ( i < j + 1 && A [ j ] >= pivot ) --j ; if ( i < j + 1) { swap ( A [ i ] , A [ j ]); ++ i ; --j ; } } while ( i < j + 1); // A[l + 1.. j] pivot A[i..u] swap ( A [ l ] , A [ j ]); k = j; }

BSTs

206

El coste de quicksort en caso peor es (n2). Esto ocurre si en todos o en la gran mayor de los casos a uno de los subsegmentos contiene muy pocos elementos y el otro casi todos, p.e. as sucede si el vector est ordenado creciente o decrecientemente. a El coste de la particin es (n) y entonces tenemos o Q(n) = (n) + Q(n 1) + Q(0) = (n) + Q(n 1) = (n) + (n 1) + Q(n 2)
n

= = (i) =
i=0

0in

= (n2).

Sin embargo, en promedio, el pivote quedar ms o a a menos centrado hacia la mitad del segmento, como ser deseable justicando que quicksort sea a considerado un algoritmo de divide y vencers. a

BSTs

207

Para analizar el comportamiento de quicksort slo o importa el orden relativo de los elementos. Tambin e podemos investigar exclusivamente el nmero de u comparaciones entre elementos, ya que el coste total es proporcional a dicho nmero. Supongamos u que cualquiera de los n! ordenes relativos posibles tiene idntica probabilidad, sea Q(n) el nmero e u medio de comparaciones y Q(n; k) el nmero medio u de comparaciones si el pivote de la primera fase es el k-simo. e Q(n) =

1kn

Q(n; k) Pr{pivote es k-simo} e

1 = (n 1 + Q(k 1) + Q(n k)) n 1kn = n1+ = n1+ 1 (Q(k 1) + Q(n k)) n 1kn 2 Q(k) n 0k<n

es decir, Q(n) satisface la misma recurrencia que I(n). No es casual.


BSTs 208

Podemos asociar a cada ejecucin de quicksort un o BST como sigue. En la ra se coloca el pivote de la z fase inicial; los subrboles izquierdo y derecho a corresponden a las ejecuciones recursivas de quicksort sobre los subvectores a la izquierda y a la derecha del pivote. Consideremos un subrbol cualquiera de este BST. a Todos los elementos del subrbol, salvo la ra son a z, comparados con el nodo ra durante la fase a la z que corresponde ese subrbol. Y rec a procamente el pivote de una determinada fase ha sido comparado con los elementos (pivotes) que son sus antecesores en el rbol y ya no se comparar con ningn otro a a u pivote. Por lo tanto, el nmero de comparaciones en u las que interviene un cierto elemento no siendo el pivote es igual a su distancia a la ra del BST. Por z esta razn I(n) = Q(n). o

BSTs

209

Para resolver la recurrencia de I(n) (longitud de caminos internos en BSTs) calculamos (n + 1)I(n + 1) nI(n): (n + 1)I(n + 1) nI(n) = (n + 1)n n(n 1) + 2I(n) = 2n + 2I(n); (n + 1)I(n + 1) = 2n + (n + 2)I(n)

BSTs

210

I(n + 1) =

2n n+2 + I(n) n+1 n+1 2(n 1)(n + 2) n + 2 2n + + I(n 1) = n+1 n(n + 1) n

2n 2(n 1)(n + 2) 2(n 2)(n + 2) + + n+1 n(n + 1) n(n 1) n+2 I(n 2) + n1 =


i=k 2n ni = + 2(n + 2) n+1 i=1 (n i + 1)(n i + 2)

n+2 I(n k) nk+1

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

= O (1) + 2(n + 2) = O (1) + 2(n + 2)(

1in

2 1 i+2 i+1

2 1 + + Hn 2) n+2 n+1 = 2nHn 4n + 4Hn + O (1),


211

BSTs

donde Hn = 1in 1/i. Puesto que Hn = ln n + O (1) se sigue que I(n) = Q(n) = 2n ln n + O (n) = 1,386 . . . n log2 n + O (n) y que C(n) = 2 ln n + O (1).

BSTs

212

Los BSTs permiten otras varias operaciones siendo los algoritmos correspondientes simples y con costes (promedio) razonables. Algunas operaciones como las de bsqueda o borrado por rango (e.g. busca el u 17o elemento) o de clculo del rango de un a elemento dado requieren una ligera modicacin de o la implementacin estndar, de tal forma que cada o a nodo contenga informacin relativa a tamao del o n subrbol en l enraizado. Concluimos esta parte con a e un ejemplo concreto: dadas dos claves k1 y k2, k1 < k2 se precisa una operacin que devuelve una o lista ordenada de todos los elementos cuya clave k est comprendida entre las dos dadas, esto es, a k1 k k2.

BSTs

213

Si el BST es vac la lista a devolver es tambin o, e vac Supongamos que el BST no es vac y que la a. o clave en la ra es k. Si k < k1 entonces todas las z claves buscadas deben encontrarse, si las hay, en el subrbol derecho. Anlogamente, si k2 < k se a a proseguir la bsqueda recursivamente en el a u subrbol izquierdo. Finalmente, si k1 k k2 a entonces puede haber claves que caen dentro del intervalo tanto en el subrbol izquierdo como en el a derecho. Para respetar el orden creciente en la lista, deber buscarse recursivamente a la izquierda, luego a listar la ra y nalmente buscarse recursivamente a z la derecha.

BSTs

214

// El m todo en_rango se ha de declarar como e // m todo p blico de la clase Diccionario . e u // Dicho m todo usa a otro auxiliar , e // en_rango_en_bst , que se habra declarado // como m todo privado de clase ( static ). e template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: en_rango ( const Clave & k1 , const Clave & k2 , list < pair < Clave , Valor > >& result ) const throw ( error ) { en_rango_en_bst ( raiz , k1 , k2 , result ); } template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: en_rango_en_bst ( nodo_bst * p , const Clave & k1 , const Clave & k2 , list < pair < Clave , Valor > >& result ) const throw ( error ) { if ( p == NULL ) return ; if ( k1 <= p -> _k ) en_rango_en_bst ( p -> _izq , k1 , k2 , result ); if ( k1 <= p -> _k && p -> _k <= k2 ) result . push_back ( make_pair ( p -> _k , p -> _v )); if ( k <= p -> _k ) en_rango_en_bst ( p -> _der , k1 , k2 , result ); }

BSTs

215

Arboles equilibrados
 

El problema de los BSTs estndar es que, como a resultado de ciertas secuencias de inserciones y/o borrados, pueden quedar muy desequilibrados. En caso peor la altura de un BST de tamao n es n (n) y en consecuencia el coste en caso peor de las operaciones de bsqueda, insercin y borrado es u o (n). A n de evitar este problema, se han propuesto diversas soluciones; la ms antigua y una de las ms a a elegantes y sencillas, es el equilibrado en altura, propuesto en 1962 por Adelson-Velskii y Landis. Los rboles de bsqueda resultantes se denominan a u AVLs en honor a sus inventores.

AVLs

216

Denicin 7. Un AVL T es un rbol binario de o a bsqueda tal que o es vac o bien cumple que u o 1. Los subrboles izquierdo y derecho, L y R, resa pectivamente, son AVLs. 2. Las alturas de L y de R dieren a lo sumo en una unidad: | altura(L) altura(R) | 1

AVLs

217

La naturaleza recursiva de la denicin anterior o garantiza que la condicin de equilibrio se cumple o en cada nodo x de un AVL. Si denotamos bal(x) la diferencia entre las alturas de los subrboles a izquierdo y derecho de un nodo cualquiera x tendremos que bal(x) {1, 0, +1}. Por otro lado todo AVL es un rbol binario de a bsqueda, por lo que el algoritmo de bsqueda en u u AVLs es idntico al de BSTs, y un recorrido en e inorden de un AVL visitar todos sus elementos en a orden creciente de claves.

AVLs

218

AVLs
marzo +1
+1

{enero, febrero, marzo, abril, mayo, junio, julio, agosto, septiembre, octubre noviembre, diciembre}

febrero octubre

+1

1 agosto

junio +1 mayo

1 0

septiembre

abril

+1

enero noviembre

julio

diciembre

219

Lema 4. La altura h(T ) de un AVL de tamao n n es (log n). Demostracin. Puesto que el AVL es un rbol o a binario se cumple que h(T ) log2(n + 1) , es decir, h(T ) (log n). Ahora vamos a demostrar que h(T ) O (log n). Sea Nh el m nimo nmero de nodos necesario para u construir un AVL de altura h. Claramente N0 = 0 y N1 = 1. El AVL ms desequilibrado posible de altura a h > 1 se consigue poniendo un subrbol, digamos el a izquierdo, de altura h 1 empleando el m nimo posible de nodos Nh1 y otro subrbol, el derecho, a que tendr altura h 2 (pero no puede tener a menos!) en el que pondremos tambin el m e nimo posible de nodos, Nh2. Por tanto, Nh = 1 + Nh1 + Nh2

AVLs

220

Veamos algunos valores de la secuencia {Nh}h0: 0, 1, 2, 4, 7, 12, 20, 33, 54, 88, . . .

La similitud con la secuencia de los nmeros de u Fibonacci llama rpidamente nuestra atencin y no a o es casual: puesto que F0 = 0, F1 = 1, Fn = Fn1 + Fn2 se cumple que Nh = Fh+1 1 para toda h 0. En efecto, N0 = F1 1 = 1 1 = 0 y Nh = 1+Nh1 +Nh2 = 1+(Fh 1)+(Fh1 1) = Fh+1 1

El nmero n-simo de Fibonacci cumple: u e n 1 Fn = + , 5 2 donde = (1 + 5)/2 1,61803 . . . es la razn o urea. a

AVLs

221

Consideremos ahora un AVL de tamao n y altura n h. Entonces n Nh por denicin y o h+1 3 n Fh+1 1 5 2 Luego, 3 5 h (n + ) 2 Tomando logaritmos en base , y simplicando h log n + O (1) = 1,44 log2 n + O (1) El lema queda demostrado.

AVLs

222

Del lema anterior deducimos que el coste de cualquier bsqueda en un AVL de tamao n ser, u n a incluso en caso peor, O (log n). El problema reside en cmo conseguir que se o cumpla la condicin de equilibrio en cada uno de los o nodos del AVL tras una insercin o un borrado. o La idea es que ambos algoritmos actan igual que u los correspondientes en BSTs, pero cada uno de los nodos en el camino que va de la ra al punto de z insercin (o borrado) debe ser comprobado para o vericar que contina cumpliendo la condicin de u o balance y si no es as hacer algo al efecto. En primer lugar nos damos cuenta de que ser necesario que cada nodo almacene informacin a o sobre su altura (o su balance), para evitar cmputos o demasiado costosos.

AVLs

223

template < typename Clave , typename Valor > class Diccionario { public : ... void buscar ( const Clave & k , bool & esta , Valor & v ) const throw ( error ); void inserta ( const Clave & k , const Valor & v ) throw ( error ); void elimina ( const Clave & k ) throw (); ... private : struct nodo_avl { Clave _k ; Valor _v ; int _alt ; nodo_avl * _izq ; nodo_avl * _der ; // constructora de la clase nodo_bst nodo_avl ( const Clave & k , const Valor & v , int alt = 1 , nodo_avl * izq = NULL , nodo_avl * der = NULL ); }; nodo_avl * raiz ; ... static int altura ( nodo_avl * p ) throw (); static void actualizar_altura ( nodo_avl * p ) throw (); };

AVLs

224

int max ( int x , int y ) { return x > y ? } template < typename Clave , typename Valor > static int Diccionario < Clave , Valor >:: altura ( nodo_avl * p ) throw () { if ( p == NULL ) return 0; else return p -> _alt ; } template < typename Clave , typename Valor > static void Diccionario < Clave , Valor >:: actualizar_altura ( nodo_avl * p ) throw () { p -> _alt = 1 + max ( altura ( p -> _izq ) , altura ( p -> _der )); } x : y;

AVLs

225

Para reestablecer el balance en un nodo que ya no cumpla la condicin de balance se utilizan o rotaciones. La gura muestra las rotaciones ms simples. a Obsrvese que si el rbol de la izquierda es un BST e a (A < x < B < y < C) entonces el de la derecha tambin lo es, y viceversa. e

x y C A B C

AVLs

226

Supongamos un subrbol de un AVL con ra y en el a z que estamos haciendo la insercin de una clave k tal o que k < x < y y que nalizado el proceso de insercin recursiva se produce un desequilibrio en y. o Si C tiene altura h entonces el subrbol enraizado a en x ha de tener altura h altura h + 1. Si la altura o de x fuera h entonces la insercin de la nueva clave o k no podr provocar el desequilibrio en y, de a manera que vamos a suponer que la altura de x es h + 1. Razonando de manera parecida llegamos a la conclusin de que la altura de A tiene que ser h. o As pues la altura de y, previa a la insercin era o h + 2. Como consecuencia de la insercin vamos a o suponer que la altura del subrbol A se incrementa, a de manera que el nuevo subrbol A tiene altura a h + 1. Si suponemos que x mantiene la condicin de o AVL tras la insercin (pero y no) eso nos lleva a o concluir que B tambin ten altura h. e a Tras la insercin el subrbol enraizado en x pasa a o a tener altura h + 2 de manera que bal(y) = +2!

AVLs

227

Antes:
y +1 x 0 C h+1 A h B h

Despues:
y +2 x +1 C h+2 A h+1 B h

LL

x 0 y 0 h+1 A B h C h+1

AVLs

228

Al aplicar la rotacin sobre el nodo y, se preserva la o propiedad de BST y adems se reestablece la a propiedad de AVL. Por hiptesis, A , B y C son o AVLs. Tras la rotacin la altura de y es h + 1 y su o balance 0, y la altura de x pasa a ser h + 2 y su balance 0. No slo hemos solventado el problema de o desequilibrio en y: el subrbol donde hemos aplicado a la insercin es un AVL y tiene la misma altura que o el subrbol antes de hacer la insercin, de manera a o que ninguno de los antecesores de y en el AVL va a estar desbalanceado. A la rotacin que hemos empleado se le suele o denominar LL (left-left).

AVLs

229

Un anlisis anlogo revela que si la clave insertada k a a cumple x < y < k y se produce un desbalanceo en el nodo x, entonces podemos reestablecer el equilibrio aplicando una rotacin RR (right-right) sobre el o nodo x. La rotacin RR es la simtrica de la o e rotacin LL. o
RR
2 x 1 y A h h+2 h B h+1 C h+1 A h B C h+1 0 y

0 x

AVLs

230

Analicemos ahora el caso de un subrbol de un AVL a con ra y en el que estamos haciendo la insercin z o de una clave k, pero esta vez x < k < y y que nalizado el proceso de insercin recursiva se o produce un desequilibrio en y. Si C tiene altura h entonces el subrbol enraizado a en x ha de tener altura h + 1, la altura de B ha de ser h (aqu razonamos igual que cuando hicimos el anlisis de la situacin LL). a o Como consecuencia de la insercin vamos a suponer o que la altura del subrbol B se incrementa, de a manera que el nuevo subrbol B tiene altura h + 1. a Si suponemos que x mantiene la condicin de AVL o tras la insercin (pero y no) eso nos lleva a concluir o que A tambin ten altura h. e a Tras la insercin el subrbol enraizado en x pasa a o a tener altura h + 2 de manera que bal(y) = +2!

AVLs

231

Si aplicamos la rotacin simple LL sobre el nodo y, o se preserva la propiedad de BST pero no se reestablece la propiedad de AVL. Por hiptesis, A, B o y C son AVLs. Pero tras la rotacin LL la altura de o y es h + 2 y su balance +1, y la altura de x pasa a ser h + 3 y su balance 2!
LL no sirve
+2 y 1 x C h+2 A h B h+1 h+1 B h C h+2 x 2 y +1

Vamos a tener que pensar alguna otra cosa y hacer un anlisis ms no. a a

AVLs

232

Vamos a suponer que la ra del subrbol B es z y z a que tiene subrboles B1 y B2, ambos AVLs. Puesto a que B tiene altura h + 1 y es un AVL, al menos uno de los dos subrboles Bi tiene altura h y el otro B j a tiene altura h h 1. o Si aplicamos una rotacin que lleva a z a la ra o z entonces x es la ra de su subrbol izquierdo y sus z a hijos son A y B1, y y es la ra de su subrbol z a derecho y sus hijos son B2 y C.
LR
y +2 1 x h+2 A h C 0 x h+1 A B1 B2 B1 h B2 z 0 y 1 o 0 h+1 C h

z +1

<h _

<h _

Ntese que antes de la rotacin el inorden era o o A < x < B1 < z < B2 < y < C, exactamente igual que tras la rotacin, esto, este nuevo tipo de rotacin o o tambin preserva la propiedad de BST. e

AVLs

233

Al aplicar la rotacin sobre el nodo y, se preserva la o propiedad de BST y adems se reestablece la a propiedad de AVL. Por hiptesis, A, B y C son AVLs. o Tras la rotacin la altura de x es h + 1 y su balance o 0 +1, la altura de y es h + 1 y su balance 0 1, o o y la altura de z pasa a ser h + 2 y su balance 0. Igual que con las rotaciones simples LL y RR, no slo arreglamos el problema de desequilibrio en y: el o subrbol donde hemos aplicado la insercin es un a o AVL y tiene la misma altura que el subrbol antes a de hacer la insercin, de manera que ninguno de los o antecesores de y en el AVL va a estar desbalanceado. A esta nueva rotacin se le denomina rotacin doble o o LR (left-right).

AVLs

234

Para la situacin en que una clave se inserta a la o derecha y luego hacia la izquierda, el desequilibrio que se pudiera producir se corrige mediante una rotacin RL, que es completamente anloga a la o a LR, cambiando derecha por izquierda y viceversa.
RL
2 x y +1 A h h+1 0 x 0 z y 1 o 0 h+1 C h

z +1 o 0
h C h+2 A B1 h B2

<h _

B1

B2

<h _

AVLs

235

Las rotaciones dobles (LR y RL) se denominan as porque podemos descomponerlas como una secuencia de dos rotaciones simples. Por ejemplo la rotacin doble LR sobre el nodo y consiste en o primero aplicar una rotacin RR sobre x y a o continuacin una rotacin LL sobre y (su hijo o o izquierdo ha pasado a ser z y no x).
RR(x)
y +2 1 x C h h+2 +1 o 0 z h h+2 0 x B2 B1 h C +2 y h

z +1 o 0
A h B1 B2

<h _

<h _

LL(y)

h+2

x 0

y 1 o 0 h+1 C h

B1

B2

<h _

AVLs

236

De manera similar, una rotacin RL se compone de o una rotacin LL seguida de una rotacin RR. o o
LL(y)
2 x y +1 A h +1 o 0 z h C h+2 h B1 B2 h B1 A h y 1 o 0

x 2
z 1

<h _

<h _

B2

z 0

RR(x)

h+2

0 x

1 o 0 h+1 C h

B1

B2

<h _

AVLs

237

Ejemplo de los meses del ao en un AVL: n


enero 2 febrero
1

RR
0 0

febrero enero marzo


0

marzo +{enero, febrero, marzo}

febrero
+2

0 +1

enero

marzo junio +1 julio


0

abril

mayo

0 agosto

+ {abril, mayo, junio, julio, agosto}

LR
1

febrero
0

agosto
0

marzo +1 enero julio


0

abril

junio +1

mayo

AVLs

238

febrero
0

agosto
0

marzo enero julio


0 0

1 2

abril

junio +1

mayo

septiembre octubre

+1

+ {septiembre, octubre}

RL

febrero
0

agosto
0

marzo 0 enero julio


0

abril

junio +1
0 mayo

octubre

septiembre

AVLs

239

febrero
0

agosto
0

marzo 1 enero julio


0

abril

junio +1
1

octubre

+1

mayo
0

septiembre noviembre

+ {noviembre}

RR
marzo 0

febrero

octubre

+1

0 agosto

junio +1

mayo

septiembre

abril

enero

julio

noviembre

marzo

+1

+1

febrero

octubre

+1

1 agosto

junio +1

mayo

septiembre

abril

+1

enero

julio

noviembre

diciembre

+ {diciembre}

AVLs

240

De nuestro anlisis previo podemos establecer los a siguientes hechos y sus consecuencias: 1. Una rotacin simple o doble slo involucra la o o modicacin de unos pocos apuntadores y actuao lizaciones de alturas, y su coste es por tanto (1), independiente de la talla del rbol. a 2. En una insercin en un AVL slo habr que efeco o a tuarse a lo sumo una rotacin (simple o doble) o para reestablecer el equilibrio del AVL. Dicha rotacin se aplica, en su caso, en uno de los nodos o en el camino entre la ra y el punto de insercin. z o 3. El coste en caso peor de una insercin en un AVL o es (log n).

AVLs

241

template < typename Clave , typename Valor > class Diccionario { public : ... private : struct nodo_avl { ... }; nodo_avl * raiz ; ... static nodo_avl * inserta_en_avl ( nodo_avl * p , const Clave & k , const Valor & v ) throw ( error ); static nodo_avl * rotacionLL ( nodo_avl * p ) throw (); static nodo_avl * rotacionLR ( nodo_avl * p ) throw (); static nodo_avl * rotacionRL ( nodo_avl * p ) throw (); static nodo_avl * rotacionRR ( nodo_avl * p ) throw (); ... };

AVLs

242

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: rotacionLL ( nodo_avl * p ) throw () { nodo_avl * q = p -> _izq ; p -> _izq = q -> _der ; q -> _der = p ; actualiza_altura ( p ); actualiza_altura ( q ); return q ; } template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: rotacionRR ( nodo_avl * p ) throw () { nodo_avl * q = p -> _der ; p -> _der = q -> _izq ; q -> _izq = p ; actualiza_altura ( p ); actualiza_altura ( q ); return q ; } template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: rotacionLR ( nodo_avl * p ) throw () { p -> _izq = rotacionRR ( p -> _izq ); return rotacionLL ( p ); } template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: rotacionRL ( nodo_avl * p ) throw () { p -> _der = rotacionLL ( p -> _der ); return rotacionRR ( p ); }

AVLs

243

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: inserta_en_avl ( nodo_avl * p , const Clave & k , const Valor & v ) throw ( error ) { if ( p == NULL ) return new nodo_avl (k , v ); if ( k < p -> _k ) { p -> _izq = inserta_en_avl ( p -> _izq , k , v ); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if ( altura ( p -> _izq ) - altura ( p -> _der ) == 2) { // p -> _izq no puede ser vac o if ( k < p -> _izq -> _k ) // caso LL p = rotacionLL ( p ); else // caso LR p = rotacionLR ( p ); } } else if ( p -> _k < k ) { p -> _der = inserta_en_avl ( p -> _der , k , v ); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if ( altura ( p -> _der ) - altura ( p -> _izq ) == 2) { // p -> _der no puede ser vac o if ( p -> _der -> _k < k ) // caso RR p = rotacionRR ( p ); else // caso RL p = rotacionRL ( p ); } } else // p -> _k == k p -> _v = v ; actualiza_altura ( p ); return p ; }

AVLs

244

Si bien resulta posible escribir una versin iterativa o del algoritmo de insercin, resulta complicada ya o que una vez efectuada la insercin del nuevo nodo o en la hoja que corresponde deberemos deshacer el camino hasta la ra para detectar si en alguno de z los nodos del camino se produce un desequilibrio y entonces aplicar la rotacin correspondiente. o Este deshacer el camino ocurre de manera natural con la recursividad, a la vuelta de cada llamada recursiva comprobamos si en el nodo desde el que se hace la llamada recursiva hay o no desequilibrio al terminar la insercin. o Para una versin iterativa necesitar o amos que cada nodo contuviese un apuntador expl cito a su padre o bien tendremos que almacenar la secuencia de nodos visitados durante la bajada en una pila, y despus e ir desapilando uno a uno para deshacer el camino.

AVLs

245

El algoritmo de borrado en AVLs se fundamenta en las mismas ideas, si bien el anlisis de qu rotacin a e o aplicar en cada caso es un poco ms complejo. a Baste decir que el coste en caso peor de un borrado en un AVL es (log n). Los desequilibrios se solucionan aplicando rotaciones simples o dobles, pero a diferencia de lo que sucede con las inserciones, podemos tener que aplicar varias. No obstante, todas las rotaciones (de coste (1)) se aplican a lo sumo una vez sobre cada uno de los nodos del camino desde la ra hasta el punto de z borrado, y como slo hay O (log n) de stos, el coste o e es logar tmico en caso peor. El algoritmo de borrado en AVLs es relativamente ms complejo que el de insercin, y la versin a o o iterativa an ms. u a En las siguientes transparencias se muestra el cdigo del algoritmo de borrado, en su versin o o recursiva, sin ms comentarios. a

AVLs

246

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: elimina_en_avl ( nodo_avl * p , const Clave & k ) throw () { if ( p == NULL ) return p ; if ( k < p -> _k ) { p -> _izq = elimina_en_avl ( p -> _izq , k ); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if ( altura ( p -> _der ) - altura ( p -> _izq ) == 2) { // p -> _der no puede ser vac o if ( altura ( p -> _der -> _izq ) - altura ( p -> _der -> _der ) == 1) p = rotacionRL ( p ); else p = rotacionRR ( p ); } } else if ( p -> _k < k ) { p -> _der = elimina_en_avl ( p -> _der , k ); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if ( altura ( p -> _izq ) - altura ( p -> _der ) == 2) { // p -> _izq no puede ser vac o if ( altura ( p -> _izq -> _der ) - altura ( p -> _der -> _izq ) == 1) p = rotacionLR ( p ); else p = rotacionLL ( p ); } } else { // p -> _k == k nodo_avl * to_kill = p ; p = juntar ( p -> _izq , p -> _der ); delete to_kill ; } actualiza_altura ( p ); return p ; }

AVLs

247

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: juntar ( nodo_avl * t1 , nodo_avl * t2 ) throw () { // // if if si uno vac o , ( t1 == ( t2 == de los dos sub rboles es a la implementaci n es trivial o NULL ) return t2 ; NULL ) return t1 ;

// t1 != NULL y tambi n t2 != NULL e nodo_alv * z ; elimina_min ( t2 , z ); z -> _izq = t1 ; z -> _der = t2 ; actualiza_altura ( z ); return z ; }

AVLs

248

template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_avl * Diccionario < Clave , Valor >:: elimina_min ( nodo_avl *& p , nod_avl *& z ) throw () { if ( p -> _izq != NULL ) { elimina_min ( p -> _izq , z ); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if ( altura ( p -> _der ) - altura ( p -> _izq ) == 2) { // p -> _der no puede ser vac o if ( altura ( p -> _der -> _izq ) - altura ( p -> _der -> _der ) == 1) p = rotacionRL ( p ); else p = rotacionRR ( p ); } } else { z = p; p = p -> _der ; } actualiza_altura ( p ); }

AVLs

249

Tablas de dispersin (hash) o


 

Una tabla de dispersin (ing: hash table) permite o almacenar un conjunto de elementos (o pares clave-valor) mediante una funcin de dispersin h o o que va del conjunto de claves al conjunto de ndices o posiciones de la tabla (p.e. 0..M 1). Idealmente la funcin de dispersin h har o o a corresponder a cada uno de los n elementos (de hecho a sus claves) que queremos almacenar una posicin distinta. Obviamente, esto no ser posible o a en general y a claves distintas les corresponder la a misma posicin de la tabla. o

Tablas de dispersin o

250

Pero si la funcin de dispersin dispersa o o ecazmente las claves, el esquema seguir siendo a util, ya que la probabilidad de que el diccionario a representar mediante la tabla de hash contenga muchas claves con igual valor de h ser pequea. a n Dadas dos claves x e y distintas, se dice que x e y son sinnimos, o que colisionan, si h(x) = h(y). o El problema bsico de la implementacin de un a o diccionario mediante una tabla de hash consistir en a la denicin de una estrategia de resolucin de o o colisiones.

Tablas de dispersin o

251

template < typename T > class Hash { public : int operator ()( const T & x ) const throw (); }; template < typename Clave , typename Valor , template < typename > class HashFunct = Hash > class Diccionario { public : ... void busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ); void inserta ( const Clave & k , const Valor & v ) throw ( error ); void elimina ( const Clave & k ) throw (); ... private : struct nodo { Clave _k ; Valor _v ; ... }; nat _M ; // tama~ o de la tabla n nat _n ; // n m . de elementos en la tabla u double _alfa_max ; // factor de carga nodo * _Thash ; // tabla con los pares // < clave , valor > // nodo ** _Thash ; // tabla de punteros a nodo static int hash ( const Clave & k ) throw () { HashFunct < Clave > h ; return h ( k ) % _M ; } };

Tablas de dispersin o

252

Una buena funcin de hash debe tener las siguientes o propiedades: 1. Debe ser fcil de calcular a 2. Debe dispersar de manera razonablemente uniforme el espacio de las claves, es decir, si dividimos el conjunto de todas las claves en grupos de sinnimos, todos los grupos deben contener o aproximadamente el mismo nmero de claves. u 3. Debe enviar a posiciones distantes a claves que son similares.

Tablas de dispersin o

253

La construccin de buenas funciones de hash no es o fcil y requiere conocimientos muy slidos de varias a o ramas de las matemticas. Est fuertemente a a relacionada con la construccin de generadores de o nmeros pseudoaleatorios. u Como regla general da buenos resultados calcular un nmero entero positivo a partir de la representacin u o binaria de la clave (p.e. sumando los bytes que la componen) y tomar mdulo M, el tamao de la o n tabla. Se recomienda que M sea un nmero primo. u La clase Hash<T> dene el operador () de manera que si h es un objeto de la clase Hash<T> y x es un objeto de la clase T, podemos aplicar h(x). Esto nos devolver un nmero entero. La operacin a u o privada hash de la clase Diccionario calcular el a mdulo mediante h(x) % M de manera que o obtengamos un ndice vlido de la tabla, entre 0 y a M - 1.

Tablas de dispersin o

254

// especializaci n del template para T = string o template <> class Hash < string > { public : int operator ()( const string & x ) const throw () { int s = 0; for ( int i = 0; i < x . length (); ++ i ) s = s * 37 + x [ i ]; return s ; }; // especializaci n del template para T = int o template <> class Hash < int > { static long const MULT = 31415926; public : int operator ()( const int & x ) const throw () { long y = (( x * x * MULT ) << 20) >> 4; return y ; } };

Otras funciones de hash ms sosticadas emplean a sumas ponderadas o transformaciones no lineales (p.e. elevan al cuadrado el nmero representado por u los k bytes centrales de la clave).

Tablas de dispersin o

255

Existen dos grandes familias de estrategias de resolucin de colisiones. Por razones histricas, que o o no lgicas, se utilizan los trminos dispersin abierta o e o (ing: open hashing ) y direccionamiento abierto (ing: open addressing ). Estudiaremos dos ejemplos representativos: tablas abiertas con sinnimos encadenados (ing: separate o chaining ) y sondeo lineal (ing: linear probing ).

Tablas de dispersin o

256

En las tablas con sinnimos encadenados cada o entrada de la tabla da acceso a una lista enlazada de sinnimos. o
template < typename Clave , typename Valor , template < typename > class HashFunct = Hash > class Diccionario { ... private : struct nodo { Clave _k ; Valor _v ; nodo * _sig ; // constructora de la clase nodo nodo ( const Clave & k , const Valor & v , nodo * sig = NULL ); }; nodo ** _Thash ; // tabla de punteros a nodo int _M ; // tama~ o de la tabla n int _n ; // numero de elementos double _alfa_max ; // factor de carga nodo * busca_sep_chain ( const Clave & k ) const throw (); void inserta_sep_chain ( const Clave & k , const Valor & v ) throw ( error ); void elimina_sep_chain ( const Clave & k ) throw (); };

Tablas de dispersin o

257

M = 13

X = { 0, 4, 6, 10, 12, 13, 17, 19, 23, 25, 30}

h (x) = x mod M 13 0

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

30

17

19

23

10

25

12

Tablas de dispersin o

258

Para la insercin, se accede a la lista o correspondiente mediante la funcin de hash, y se o recorre para determinar si ya exist o no un a elemento con la clave dada. En el primer caso, se modica la informacin asociada; en el segundo se o aade un nuevo nodo a la lista con el nuevo n elemento. Puesto que estas listas de sinnimos contienen por o lo general pocos elementos lo ms efectivo es a efectuar las inserciones por el inicio (no hace falta usar fantasmas, cierre circular, etc.). Existen variantes en que las listas de sinnimos estn o a ordenadas o se sustituyen por rboles de bsqueda, a u pero no comportan una ventaja real en trminos e prcticos (el coste asinttico terico es mejor, pero a o o el criterio no es efectivo ya que el nmero de u elementos involucrado es pequeo). n La bsqueda es tambin simple: se accede a la lista u e apropiada mediante la funcin de hash y se realiza o un recorrido secuencial de la lista hasta que se encuentra un nodo con la clave dada o la lista se ha examinado sin xito. e
Tablas de dispersin o 259

template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: inserta ( const Clave & k , const Valor & v ) throw ( error ) { if ( _n / _M > _alfa_max ) // la tabla est demasiado llena ... a inse rta_sep_chain (k , v ); } template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: inserta_sep_chain ( const Clave & k , const Valor & v ) throw ( error ) { int i = hash ( k ); nodo * p = _Thash [ i ]; // buscamos en la lista de sin nimos o while ( p != NULL && p -> _k != k ) p = p -> _sig ; // lo insertamos al principio // si no estaba if ( p == NULL ) { _Thash [ i ] = new nodo (k , v , _Thash [ i ]); ++ _n ; } else p -> _v = v ; }

Tablas de dispersin o

260

template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ) { nodo * p = buscar_sep_chain ( k ); if ( p == NULL ) esta = false ; else { esta = true ; v = p -> _v ; } } template < typename Clave , typename Valor , template < typename > class HashFunct > Diccionario < Clave , Valor , HashFunct >:: nodo * Diccionario < Clave , Valor , HashFunct >:: busca_sep_chain ( const Clave & k ) const throw () { int i = hash ( k ); nodo * p = _Thash [ i ]; // buscamos en su lista de sin nimos o while ( p != NULL && p -> _k != k ) p = p -> _sig ; return p ; }

Tablas de dispersin o

261

template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: elimina ( const Clave & k ) throw () { el im inar_sep_chain ( k ); } template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: eliminar_sep_chain ( const Clave & k ) throw () { int i = hash ( k ); nodo * p = _Thash [ i ]; nodo * ant = NULL ; // apuntara al anterior de p // buscamos en su lista de sin nimos o while ( p != NULL && p -> _k != k ) { ant = p ; p = p -> _sig ; } // // // if si p != NULL , lo quitamos de la lista teniendo en cuenta que puede ser el primero ( p != NULL ) { -- _n ; if ( ant != NULL ) ant -> _sig = p -> _sig ; else _Thash [ i ] = p -> _sig ; delete p ;

} }

Tablas de dispersin o

262

Sea n el nmero de elementos almacenados en la u tabla con encadenamiento de sinnimos. En o promedio, cada lista tendr = n/M elementos y el a coste de bsquedas (con y sin xito), de inserciones u e y borrados ser proporcional a . Si es un valor a razonablemente pequeo entonces podemos n considerar que el coste promedio de todas las operaciones sobre la tabla de hash es (1). Al valor se le denomina factor de carga (ing: load factor ). Otra variantes de hash abierto almacenan los sinnimos en una zona particular de la propia o tabla, la llamada zona de excedentes (ing: cellar ). Las posiciones de esta zona no son direccionables, es decir, la funcin de hash nunca toma sus valores. o

Tablas de dispersin o

263

En las estrategias de direccionamiento abierto los sinnimos se almacenan en la tabla de hash, en la o zona de direccionamiento. Bsicamente, tanto en a bsquedas como en inserciones se realiza una u secuencia de sondeos que comienza en la posicin o i0 = h(k) y a partir de ah contina en i1, i2, . . . hasta u que encontramos una posicin ocupada por un o elemento cuya clave es k (bsqueda con xito), una u e posicin libre (bsqueda sin xito, insercin) o o u e o hemos explorado la tabla en su totalidad. Las distintas estrategias var segn la secuencia de an u sondeos que realizan. La ms sencilla de todas es el a sondeo lineal: i1 = i0 + 1, i2 = i1 + 1, . . ., realizndose a los incrementos mdulo M. o

Tablas de dispersin o

264

template < typename Clave , typename Valor , template < typename > class HashFunct = Hash > class Diccionario { ... private : enum Estado { libre , borrado , ocupado }; struct nodo { Clave _k ; Valor _v ; Estado _estado ; // constructora de la clase nodo nodo ( const Clave & k , const Valor & v , Estado e = libre ); }; nodo * _Thash ; // tabla de nodos int _M ; // tama~ o de la tabla n int _n ; // numero de elementos double _alfa_max ; // factor de carga

int b u s ca_linear_probing ( const Clave & k ) const throw (); void i n s er ta_ li ne ar _pr ob in g ( const Clave & k , const Valor & v ) throw ( error ); void e l i mi na_ li ne ar _pr ob in g ( const Clave & k ) throw (); };

Tablas de dispersin o

265

M = 13

X = { 0, 4, 6, 10, 12, 13, 17, 19, 23, 25, 30} (incremento 1) 0 13 0 13 25

h (x) = x mod M 0

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

0 1 2 3

0 1 2 3

4 5

4 17 6 19

4 5 6 7 8 9

4 17 6 19 30

6 7 8 9

10 12

10 11 12

10 23 12

10 11 12

10 23 12

+ {0, 4, 6, 10, 12}


= ocupado

+ {13, 17, 19, 23}


= libre

+ {25, 30}

Tablas de dispersin o

266

// Solo se invoca si hay al menos un sitio // no ocupado en la tabla : _n < _M template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: ins er ta _li ne ar _pr ob in g ( const Clave & k , const Valor & v ) throw ( error ) { int i = hash ( k ); int vistos = 0; // aseguramos una sola pasada // prilibre es la primera posici n borrada o // que encontremos , vale -1 si no // vemos ninguna posici n borrada o int prilibre = -1; while ( _Thash [ i ]. _estado != libre && _Thash [ i ]. _k != k && vistos < _M ) { // nos guardamos la primera posici n borrada o if ( _Thash [ i ]. _estado == borrado && prilibre == -1) prilibre == i ; ++ vistos ; i = ( i + 1) % _M ; } // k no est ; si sabemos una posici n a o // borrada utilizable , usarla if ( _Thash [ i ]. _estado == libre || _Thash [ i ]. _k != _k ) { ++ _n ; if ( prilibre != -1) i = prilibre ; } _Thash [ i ]. _k = k ; _Thash [ i ]. _v = v ; _Thash [ i ]. _estado = ocupado ; }

Tablas de dispersin o

267

template < typename Clave , typename Valor , template < typename > class HashFunct > int Diccionario < Clave , Valor , HashFunct >:: busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ) { int i = busca_linear_probing ( k ); if ( _Thash [ i ]. _estado == ocupado && _Thash [ i ]. _k == k ) { esta = true ; v = _Thash [ i ]. _v ; } else esta = false ; } template < typename Clave , typename Valor , template < typename > class HashFunct > int Diccionario < Clave , Valor , HashFunct >:: busca_linear_probing ( const Clave & k ) const throw () { int i = hash ( k ); int vistos = 0;

// aseguramos una pasada , // no m s a

// buscamos en su lista de sin nimos o while ( _Thash [ i ]. _estado != libre && _Thash [ i ]. _k != k && vistos < _M ) { ++ vistos ; i = ( i + 1) % _M ; } return i ; }

Tablas de dispersin o

268

El borrado en tablas de direccionamiento abierto es algo ms complicado. No basta con marcar la a posicin correspondiente como libre ya que una o bsqueda posterior podr no hallar el elemento u a buscado aunque ste se encontrase en la tabla. En e su lugar debe marcarse la posicin que conten al o a elemento como borrada. A efectos de las bsquedas es como si dicha u posicin estuviera ocupada. Una posicin borrada o o puede utilizarse para colocar nuevos elementos. Para ello, durante la iteracin que busca a la nueva clave o podemos anotar la primera (o la ultima) de las posiciones borradas vistas y cuando la iteracin o nalice podemos emplear esa posicin en lugar de la o posicin libre donde naliza la iteracin previa a o o la insercin propiamente dicha. o

Tablas de dispersin o

269

M = 13 h (x) = x mod M 0 13 25 (incremento 1) 0 13 25 36 13 25

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

0 1 2 3

0 1 2 3

4 17 6 19 30

4 5 6 7 8 9

4 17 6 20 30

4 5 6 7 8 9

4 17 6 20 30

10 23 12 {0, 19, 25 }
= ocupado

10 11 12

10 23 12 + {20}
= libre

10 11 12

10 23 12 + {36}

= borrado

Tablas de dispersin o

270

template < typename Clave , typename Valor , template < typename > class HashFunct > void Diccionario < Clave , Valor , HashFunct >:: e li mi na_ li ne ar _pr ob in g ( const Clave & k ) throw () { int i = busca_linear_probing ( k ); if ( _Thash [ i ]. _estado == ocupado && _Thash [ i ]. _k == k ) Thash [ i ]. _estado = borrado ; }

Tablas de dispersin o

271

Con todo, los borrados degradan notablemente el rendimiento general, ya que las posiciones borradas cuentan a efectos de la bsqueda tanto como las u realmente ocupadas. El sondeo lineal ofrece algunas ventajas ya que los sinnimos tienden a encontrarse en posiciones o consecutivas, lo que resulta ventajoso en implementaciones sobre memoria externa. Generalmente se usar junto a alguna tcnica de a e bucketing : cada posicin de la tabla es capaz de o albergar b > 1 elementos.

Tablas de dispersin o

272

Por otra parte tiene algunas serias desventajas y su rendimiento es especialmente sensible al factor de carga = n/M. Si es prximo a 1 (tabla llena o o casi llena) el rendimiento ser muy pobre. Si < 1 a el coste de las bsquedas con xito (y u e modicaciones) ser proporcional a a 1 1 1+ 2 1 y el de bsquedas sin xito e inserciones u e ser proporcional a a 1 1 1+ . 2 (1 )2

Tablas de dispersin o

273

Un fenmeno indeseable que se acenta cuando o u es prximo a 1 es el apiamiento (ing: clustering ). o n Muchos elementos no pueden ocupar su posicin o preferida al estar ocupada por otro elemento que no tiene porqu ser un sinnimo (invasor ). Grupos e o de sinnimos ms o menos dispersos acaban o a fundindose en grandes clusters y cuando e buscamos una cierta clave tenemos que examinar no slo sus sinnimos sino un buen nmero de claves o o u que no tienen relacin alguna con la clave buscada. o

Tablas de dispersin o

274

Muchos lenguajes de programacin permiten crear o tablas jando su tamao en tiempo de ejecucin. n o Entonces es posible utilizar la llamada tcnica de e redimensionamiento (ing: resizing ). Si el factor de carga es muy alto, superando un cierto valores umbral, se reclama a la memoria dinmica una tabla a cuyo tamao es aproximadamente el doble del n tamao de la tabla en curso y se reinserta toda la n informacin de la tabla en curso sobre la nueva o tabla.

Tablas de dispersin o

275

Para ello se recorre secuencialmente la tabla en curso y cada uno de los elementos presentes se inserta por el procedimiento habitual en la nueva tabla, usando una funcin de hash distinta, o obviamente. La operacin de redimensionamiento o requiere tiempo proporcional al tamao de la tabla n en curso (y por tanto (n)); pero hay que hacerla slo muy de vez en cuando. El redimensionamiento o permite que el diccionario crezca sin l mites prejados, garantizando un buen rendimiento de todas las operaciones y sin desperdiciar demasiada memoria. La misma tcnica puede aplicarse a la e inversa, para evitar que el factor de carga sea excesivamente bajo (desperdicio de memoria). Puede demostrarse que, aunque una operacin o individual podr llegar tener coste (n), una a secuencia de n operaciones de actualizacin o tendr coste total (n). a

Tablas de dispersin o

276

 

Tries

Las claves que identican a los elementos de una coleccin estn formadas por una secuencia de o a s mbolos (p.e. caracteres, d gitos, bits), siendo est descomposicin ms o menos natural, y a o a dicha circunstancia puede aprovecharse ventajosamente para implementar las operaciones t picas de un diccionario de manera notablemente eciente. Adicionalmente, es frecuente que necesitemos ofertar operaciones del TAD basadas en esta descomposicin de las claves en s o mbolos: por ejemplo, podemos querer una operacin que dada o una coleccin de palabras C y una palabra p nos o devuelva la lista de todas las palabras de C que contienen a la palabra p como subcadena.

Tries

277

Consideremos un alfabeto nito = {1, . . . , m} de cardinalidad m 2. Mediante denotaremos, como es habitual en muchos contextos, el conjunto de las secuencias o cadenas formadas por s mbolos de . Dadas dos secuencias u y v denotaremos u v la secuencia resultante de concatenar u y v. Para la secuencia de longitud 0, es decir, la secuencia vac a usaremos la notacin . o Denicin 8. Dado un conjunto nito de secueno cias X de idntica longitud, el trie T correse pondiente a X es un rbol m-ario denido recursia vamente de la siguiente manera: 1. Si X contiene un slo elemento o ninguno eno tonces T es un rbol consistente en un unico a nodo que contiene al unico elemento de X o est vac a o. 2. Si |X| 2, sea Ti el trie correspondiente a Xi = {y | x = i y X i }. Entonces T es un rbol m-ario constituido por una ra y los m a z subrboles T1, T2, . . . , Tm. a

Tries

278

x1 = 0 0 0 1 0 1 x2 = 0 1 0 0 0 1 x3 = 1 0 1 0 0 0 x4 = 0 1 0 1 0 1 x5 = 1 1 0 1 0 1

m=2 0 1

0 x6 = 1 0 0 1 1 1 x7 = 1 1 0 0 0 1 x8 = 0 1 1 1 1 1 x9 = 0 0 1 1 1 0 x 10 = 1 0 0 0 0 1 x2 x4 x 10 x6 x1 x9 1 x8

1 x3

x7

x5

Tries

279

Lema 5. Si las aristas del trie T correspondiente a un conjunto X se etiquetan mediante los s mbolos de de tal modo que la arista que une la ra con z su primer subrbol se etiqueta 1, la que une la ra a z con su subrbol se etiqueta 2, etc. entonces las etia quetas del camino que nos llevan desde la ra hasta z una hoja no vac que contiene a x constituyen el a prejo ms corto que distingue un a vocamente a x; es decir, ningn otro elemento de X empieza con u el mismo prejo. Lema 6. Sea p la etiqueta correspondiente a un camino que va de la ra de un trie T hasta un cierto z nodo (interno u hoja) de T . Entonces el subrbol a enraizado en dicho nodo contiene todos los elementos de X que tienen en comn al prejo p (y no ms u a elementos).

Tries

280

Lema 7. Dado un conjunto X de secuencias de igual longitud, su trie correspondiente es unico. En particular T no depende del orden en que se presenten los elementos de X. Lema 8. La altura de un trie T es igual a la longitud m nima de prejo necesaria para distinguir cualesquiera dos elementos del conjunto al que corresponde el trie. En particular, si es la longitud de las secuencias en X, la altura de T ser . a

Tries

281

La denicin de tries impone que todas las o secuencias sean de igual longitud, lo cual es muy restrictivo. Pero si no exigimos esta condicin o entonces tenemos un problema que habremos de afrontar: si un elemento x es prejo propio de otro elemento y, cmo podremos distinguirlo? Cmo o o diferenciamos las situaciones en la que x e y pertenecen ambos a X de las situaciones en la que slo y est en X? o a Una solucin habitual consiste en ampliar con un o s mbolo especial (p.e. ) de n de secuencia, y marcar cada una de las secuencias en X con dicho s mbolo. Ello garantiza que ninguna de las secuencias (marcadas) es prejo propio de otra. El precio a pagar es que hay que trabajar con un alfabeto de m + 1 s mbolos.

Tries

282

X = {ahora, alto, amigo, amo, asi, bar, barco, bota, ...}


a b c

...
a

...
a

h l m

... ...
bota
a r

ahora alto

asi

...

...

... ...
c

amigo

amo

...

barco

bar

Tries

283

Las tcnicas de implementacin de los tries son las e o convencionales para rboles. Si se utiliza un vector a de apuntadores por nodo, los s mbolos de suelen poderse utilizar directamente como ndices (eventualmente, se habr de utilizar una funcin a o ind : {1, . . . , m}). Las hojas que contienen los elementos de X pueden almacenar exclusivamente los sujos restantes, ya que el prejo est a mplicitamente codicado en el camino desde la ra a la hoja. z En el caso en que se utilice la representacin o primognito-siguiente hermano, cada nodo almacena e un s mbolo y sendos apuntadores al primognito y al e siguiente hermano. Puesto que suele haber denido un orden sobre la lista de hijos de cada nodo suele ordenarse de acuerdo a quel. a

Tries

284

... ...
i

Tries

285

Aunque es ms costoso en espacio emplear nodos a del trie para representar las palabras completas, resulta ventajoso evitar la necesidad de nodos de distinto tipo, apuntadores a nodos de diferentes tipos, o representaciones poco ecientes para los nodos (p.e. unions).
// La clase Clave debe soportar las siguientes // operaciones : // x . length () devuelve la longitud >= 0 de una clave x template < typename Clave > int length () throw (); // x [ i ] devuelve el i - simo s mbolo de x , e // lanza un error si i < 0 o i >= x . length () template < typename Simbolo , typename Clave > Simbolo operator []( const Clave & x , int i ) throw ( error ); template < typename Simbolo , typename Clave , typename Valor > class DiccDigital { public : ... private : struct nodo_trie { Simbolo _c ; nodo_trie * _primg ; nodo_trie * _sigher ; Valor _v ; }; nodo_trie * raiz ; ... };

Tries

286

< typename Simbolo , typename Clave , typename Valor > void DiccDigital < Simbolo , Clave , Valor >:: busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ) { nodo_trie * p = busca_en_trie ( raiz , k , 0); if ( p == NULL ) esta = false ; else { esta = true ; v = p -> _v ; } } // Pre : p es la ra z del sub rbol conteniendo a // las claves cuyos i -1 primeros s mbolos // coinciden con los i -1 s mbolos iniciales de k // Coste : (longitud(k))} template < typename Simbolo , typename Clave , typename Valor > DiccDigital < Simbolo , Clave , Valor >:: nodo_trie * DiccDigital < Simbolo , Clave , Valor >:: busca_en_trie ( nodo_trie * p , const Clave & k , int i ) const throw () { if if if if ( p == NULL ) return NULL ; ( i == k . length ()) return p ; ( p -> _c > k [ i ]) return NULL ; ( p -> _c < k [ i ]) return busca_en_trie ( p -> _sigher , k , i ); // p -> _c == k [ i ] return busca_en_trie ( p -> _primg , k , i +1); }

template

Tries

287

Una solucin que intenta combinar eciencia en el o acceso a los subrboles y en espacio consiste en a implementar cada nodo del trie como un BST. La estructura resultante se denomina rbol ternario de a bsqueda ya que cada uno de sus nodos contiene u tres apuntadores: dos apuntadores al hijo izquierdo y derecho, respectivamente, en el BST, y un apuntador a la riz del subrbol al que da acceso el a a nodo.
template < typename Simbolo , typename Clave , typename Valor > class DiccDigital { public : ... void inserta ( const Clave & k , const Valor & v ) throw ( error ); ... private : struct nodo_tst { Simbolo _c ; nodo_tst * _izq nodo_tst * _cen ; nodo_tst * _der ; Valor _v ; }; nodo_tst * raiz ; ... static nodo_tst * inserta_en_tst ( nodo_tst * t , int i , const Clave & k , const Valor & v ) throw ( error ); ... };

Tries

288

X = {DICCION, DADO, DADOS, DEDO, DONDE,


corresponde a un nodo del trie

ABACO, CASA, FARO, FAMA, ELLO, EL} D

Tries

289

template < typename Simbolo , typename Clave , typename Valor > void DiccDigital < Simbolo , Clave , Valor >:: inserta ( const Clave & k , const Valor & v ) throw ( error ) { // Simbolo () es un simbolo nulo , // p . e . si Simbolo == char entonces // Simbolo () == \0 k [ k . length ()] = Simbolo (); // a~ adir centinela n // al final de la clave raiz = inserta_en_tst ( raiz , 0 , k , v ); }

Tries

290

template < typename Simbolo , typename Clave , typename Valor > DiccDigital < Simbolo , Clave , Valor >:: nodo_tst * DiccDigital < Simbolo , Clave , Valor >:: inserta_en_tst ( nodo_tst * t , int i , const Clave & k , const Valor & v ) throw ( error ) { if ( t == NULL ) { t = new nodo_tst ; t -> _izq = t -> _der = t -> cen = NULL ; t -> _c = k [ i ]; if ( i < k . length () - 1) { t -> _cen = inserta_en_tst ( t -> _cen , } else { // i == k . length () - 1; k [ i ] == t -> _v = v ; } } else { if ( t -> _c == k [ i ]) t -> _cen = inserta_en_tst ( t -> _cen , if ( k [ i ] < t -> _c ) t -> _izq = inserta_en_tst ( t -> _izq , if ( t -> _c < k [ i ]) t -> _der = inserta_en_tst ( t -> _der , } return t ; }

i + 1 , k , v ); Simbolo ()

i + 1 , k , v ); i , k , v ); i , k , v );

Tries

291

La descomposicin digital de las claves puede o emplearse adems de para la bsqueda para la a u ordenacin. Los algoritmos de ordenacin basados o o en estos principios se denominan de ordenacin o digital (ing: radix sort). Consideremos un vector de n elementos cada uno de los cuales es una secuencia de bits. Dado un elemento x, bit(x, i) denotar su a i-simo bit. e Si ordenamos el vector con relacin al bit de mayor o peso, cada bloque resultante de acuerdo al bit de siguiente peso, y as sucesivamente, habremos ordenado el vector en su totalidad.

Tries

292

// Llamada inicial : // radix_sort (A , 0 , A . size () -1 , 1); template < typename Elem , typename Symb > void radix_sort ( vector < Elem >& A , int i , int j , int r ) { if ( i < j && r >= 0) { int k ; radix_split (A , i , j , r , k ); radix_sort (A , i , k , r - 1); radix_sort (A , k + 1 , j , r - 1); } }

Tries

293

// bit (x , r ) devuelve el bit r - simo de x e // r == 0 = > bit de menor peso // r == 1 = > bit de mayor peso template < typename Elem , typename Symb > void radix_split ( vector < Elem >& A , int i , int j , int r , int & k ) { int u = i ; int v = j ; while ( u < v + 1) { while ( u < v + 1 && bit ( A [ u ] , r ) == 0) ++ u ; while ( u < v + 1 && bit ( A [ v ] , r ) == 1) --v ; if ( u < v + 1) swap ( A [ u ] , A [ v ]); } k = v; }

Tries

294

Cada etapa de radix sort tiene un coste no recursivo lineal; puesto que el nmero de etapas es el coste u del algoritmo es (n ). Otra forma de deducir el coste consiste en considerar el coste asociado a cada elemento de A: un elemento cualquiera de A es examinado (y eventualmente intercambiado con otro) a lo sumo veces, de ah que el coste total sea (n ).

Tries

295

Skip lists
 

Una skip list es una estructura de datos muy sencilla que permite la implementacin de un TAD o diccionario con poco esfuerzo de manera eciente. Las skip lists consiguen tener rendimiento (log n) en promedio en bsquedas y actualizaciones gracias u al uso de aleatorizacin. Los costes promedio no o dependen de que la entrada sea aleatoria o no: slo o de las decisiones aleatorias tomadas por los algoritmos. Las skip lists fueron inventadas por W. Pugh en 1989.

Skip lists

296

Una skip list S que representa a un conjunto de elementos X consiste en un cierto nmero de listas u enlazadas no vac ordenadas por las claves de los as, elementos que contienen y numeradas de 1 en adelante, de modo que se satisface 1. Todos los elementos de X pertenecen a la lista 1. 2. Si x pertenece a la lista i entonces, con probabilidad q, x pertenece tambin a la lista i + 1. e Dado un elemento x, su nivel es el nmero de listas u en las que est inclu De la denicin anterior se a do. o desprende que el nivel de cada elemento es una variable aleatoria independiente y P[nivel(x) = i] = p q
i1

p = 1 q.

Skip lists

297

Para implementar una skip list, cada elemento se almacenar en un nodo, que asimismo a contendr tantos apuntadores como correspondan al a nivel del elemento. Cada uno de dichos apuntadores apunta al sucesor de x en la correspondiente lista. Adicionalmente, usaremos un nodo cticio de cabecera con apuntadores a los primeros elementos de cada lista. El nivel de la skip list es el mximo a nivel de entre sus elementos y ste ser el nmero e a u de apuntadores en el header.
template < typename Clave , typename Valor > class Diccionario { public : ... private : struct nodo_skip_list { Clave _k ; Valor _v ; int _alt ; nodo_skip_list ** _sig ; nodo_skip_list ( Clave k , Valor v , int alt ) : _k ( k ) , _v ( v ) , _alt ( alt ) , _sig ( new nodo_skip_list *[ alt ]) { } }; nodo_skip_list * _header ; int _nivel ; double _p ; // p . e . , _p = 0.5 ... };

Skip lists

298

Skip lists
12 +O O NIL 21 37 40 42 53 66 12 21 37 40 42 46 53 66 +O O NIL

O O Header

O O Header

299

template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: busca ( const Clave & k , bool & esta , Valor & v ) const throw ( error ) { nodo_skip_list * p = buscar_en_skip_list ( _header , _nivel -1 , k ); if ( p == NULL ) esta = false ; else { esta = true ; v = p -> _v ; } } template < typename Clave , typename Valor > Diccionario < Clave , Valor >:: nodo_skip_list * Diccionario < Clave , Valor >:: buscar_en_skip_list ( nodo_skip_list * p , int l , const Clave & k ) const throw () { while ( l >= 0) if ( p -> _sig [ l ] == NULL || k <= p -> _sig [ l ] -> _k ) --l ; else p = p -> _sig [ l ]; if ( p -> _sig [0] == NULL || p -> _sig [0] -> _k != k ) // k no est a return NULL ; else // k est , modificamos el valor asociado a return p -> _sig [0]; }

Skip lists

300

Para realizar la insercin de un nuevo elemento se o procede en cuatro fases: 1: Se busca en la skip list la clave k dada. Se emplea un bucle de bsqueda ligeramente distinto, para u que anote en un vector de apuntadores los ultimos nodos examinados en cada nivel. Dichos nodos son los potenciales predecesores del nuevo nodo. Basta anotar cal es el ultimo nodo visitado en el u nivel antes de bajar de nivel. 2: Si la clave k ya existe se modica el valor asociado y se termina.
template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: inserta_en_skip_list (...) { nodo_skip_list ** pred = new nodo_skip_list *[ l + 1]; while ( l >= 0) if ( p -> _sig [ l ] == NULL || k <= p -> _sig [ l ] -> _k ) { pred [ l ] = p ; // <====== anotar el predecesor --l ; } else { p = p -> _sig [ l ]; } if ( p -> _sig [0] == NULL || p -> _sig [0] -> _k != k ) // k no est ; a~ adimos un nuevo nodo ... a n else // k est , modificamos el valor asociado a p -> _sig [0] -> _v = v ; }

Skip lists

301

3: En caso contrario, se crea un nuevo nodo con la clave y valor dados, utilizando procedimiento aleatorio para decidir el nivel r del elemento 4: Se enlaza el nuevo nodo en las r primeras listas
template < typename Clave , typename Valor > class Diccionario { public : ... private : ... util :: Random _rng ; // generador de n meros aleatorios u // asociado a la skip list }; template < typename Clave , typename Valor > void Diccionario < Clave , Valor >:: inserta_en_skip_list (...) { ... // a~ adir nuevo nodo n // generar aleatoriamente su altura int alt = 1; while ( _rng () > _p ) ++ alt ; nodo_skip_list * nn = new nodo_skip_list (k , v , alt ); if ( alt > _nivel ) { // a~ adir nuevos niveles al header n } // enlazar el nuevo nodo en las listas // enlazadas pertinentes for ( int i = alt - 1; i >= 0; --i ) { nn -> _sig [ i ] = pred [ i ] -> _sig [ i ]; pred [ i ] -> _sig [ i ] = nn ; } }

Skip lists

302

... if ( alt > _nivel ) { // a~ adir nuevos niveles al _header y a pred n // ( desde i = _nivel hasta i = alt - 1) // nuevo header y nueva tabla pred nodo_skip_list ** _new_header = new nodo_skip_list *[ alt ]; nodo_skip_list ** new_pred = new nodo_skip_list *[ alt ]; // copiamos for ( int i = _nivel - 1; i >= 0; --i ) { _new_header -> _sig [ i ] = _header -> _sig [ i ]; new_pred -> _sig [ i ] = pred -> _sig [ i ]; } // los niveles desde _nivel a alt - 1 est n vac os a for ( int i = alt - 1; i >= _nivel ; --i ) { _new_header -> _sig [ i ] = NULL ; new_pred -> _sig [ i ] = NULL ; } // eliminamos el header y la tabla pred antiguas delete [] _header ; delete [] pred ; // actualizamos _header = _new_header ; pred = new_pred ; _nivel = alt ; } ...

Skip lists

303

VI Colas de prioridad

Una cola de prioridad (cat: cua de prioritat; ing: priority queue) es una coleccin de elementos donde o cada elemento tiene asociado un valor susceptible de ordenacin denominado prioridad. Una cola de o prioridad se caracteriza por admitir inserciones de nuevos elementos y la consulta y eliminacin del o elemento de m nima prioridad. Anlogamente se a pueden denir colas de prioridad que admitan la consulta y eliminacin del elemento de mxima o a prioridad en la coleccin. o

Colas de prioridad

305

En esta especicacin asumimos que el tipo Prio o ofrece una relacin de orden total <. Por otra parte, o puede darse el caso de que existan varios elementos con igual prioridad y en dicho caso es irrelevante cul de los elementos es devuelto por min o a eliminado por elim min. En ocasiones se utiliza una operacin prio min que devuelve la m o nima prioridad.

Colas de prioridad

306

template < typename Elem , typename Prio > class ColaPrioridad { public : // Constructora , crea una cola vac a . ColaPrioridad () throw ( error ); // Destructora , constr . por copia y asignaci n o ~ ColaPrioridad () throw (); ColaPrioridad ( const ColaPrioridad & P ) throw ( error ); ColaPrioridad & operator =( const ColaPrioridad P ) throw ( error ); // A~ ade el elemento x con prioridad p a la cola de n // prioridad . void inserta ( cons Elem & x , const Prio & p ) throw ( error ) // Devuelve un elemento de m nima prioridad en la cola de // prioridad . Se lanza un error si la cola est vac a . a Elem min () const throw ( error ); // Devuelve la m nima prioridad presente en la cola de // prioridad . Se lanza un error si la cola est vac a . a Prio prio_min () const throw ( error ); // Elimina un elemento de m nima prioridad de la cola de // prioridad . Se lanza un error si la cola est vac a . a void elim_min () throw ( error ); // Devuelve cierto si y s lo si la cola est vac a . o a bool vacia () const throw (); };

Colas de prioridad

307

Las colas de prioridad tienen mltiples usos. Con u frecuencia se emplean para implementar algoritmos voraces. Este tipo de algoritmos suele tener una iteracin principal, y una de las tareas a realizar en o cada una de dichas iteraciones es seleccionar un elemento de entre varios que minimiza (o maximiza) un cierto criterio de optimalidad local. El conjunto de elementos entre los que se ha de efectuar la seleccin es frecuentemente dinmico y admitir o a inserciones ecientes. Algunos de estos algoritmos incluyen: Algoritmos de Kruskal y Prim para el clculo del a rbol de expansin m a o nimo de un grafo etiquetado. Algoritmo de Dijkstra para el clculo de caminos a m nimos en un grafo etiquetado. Construccin de cdigos de Human (cdigos bio o o narios de longitud media m nima). Otra tarea para la que obviamente podemos usar una cola de prioridad es para ordenar.
Colas de prioridad 308

// Tenemos dos arrays Peso y Simb con los pesos at micos o // y s mbolos de n elementos qu micos , // p . e . , Simb [ i ] = " C " y Peso [ i ] = 12.2. // Utilizamos una cola de prioridad para ordenar la // informaci n de menor a mayor s mbolo en orden alfab tico o e ColaPrioridad < double , string > P ; for ( int i = 0; i < n ; ++ i ) P . inserta ( Peso [ i ] , Simb [ i ]); int i = 0; while (! P . vacia ()) { Peso [ i ] = P . min (); Simb [ i ] = P . prio_min (); ++ i ; P . elim_min (); }

Tambin se usan colas de prioridad para hallar el e k-simo elemento de un vector no ordenado. Se e colocan los k primeros elementos del vector en una max-cola de prioridad y a continuacin se recorre el o resto del vector, actualizando la cola de prioridad cada vez que el elemento es menor que el mayor de los elementos de la cola, eliminando al mximo e a insertando el elemento en curso.

Colas de prioridad

309

La mayor de tcnicas empleadas en la a e implementacin de diccionarios puede usarse para o implementar colas de prioridad. La excepcin la o constituyen las tablas de dispersin y los tries. Se o puede usar una lista ordenada por prioridad. Tanto la consulta como la eliminacin del m o nimo son triviales y su coste es (1). Pero las inserciones tienen coste lineal, tanto en caso peor como en promedio. Otra opcin es usar un rbol de bsqueda o a u (equilibrado o no), utilizando como criterio de orden de sus elementos las correspondientes prioridades. Hay que modicar ligeramente el invariante de representacin para admitir y tratar adecuadamente o las prioridades repetidas. Puede garantizarse en tal caso que todas las operaciones (inserciones, consultas, eliminaciones) tienen coste (log n): en caso peor si el BST es equilibrado (AVL), y en caso medio si no lo es.

Colas de prioridad

310

Tambin pueden usarse skip lists con idntico e e rendimiento al que ofrecen los BSTs (aunque los costes son logar tmicos slo en caso medio, dicho o caso medio no depende ni del orden de insercin ni o de la existencia de pocas prioridades repetidas). Si el conjunto de posibles prioridades es reducido entonces ser conveniente emplear una tabla de a listas, correspondiendo cada lista a una prioridad o intervalo reducido de prioridades. En lo que resta estudiaremos una tcnica espec e ca para la implementacin de colas de prioridad basada o en los denominados mont culos.

Colas de prioridad

311

Un mont culo (ing: heap) es un rbol binario tal que a 1. todos las hojas (subrboles son vac a os) se sitan u en los dos ultimos niveles del rbol. a 2. en el antepenltimo nivel existe a lo sumo un u nodo interno con un slo hijo, que ser su hijo o a izquierdo, y todos los nodos a su derecha en el mismo nivel son nodos internos sin hijos. 3. el elemento (su prioridad) almacenado en un nodo cualquiera es mayor (menor) o igual que los elementos almacenados en sus hijos izquierdo y derecho. Se dice que un mont culo es un rbol binario a quasi-completo debido a las propiedades 1-2. La propiedad 3 se denomina orden de mont culo, y se habla de max-heaps o min-heaps segn que los u elementos sean ques sus hijos. En lo sucesivo o slo consideraremos max-heaps. o

Colas de prioridad

312

n = 10 altura = 4 72

76

nivel 0

34

nivel 1

59

63

17

29

nivel 2

37

33

29

nivel 3 nivel 4 hojas

Colas de prioridad

313

De las propiedades 1-3 se desprenden dos consecuencias importantes: 1. El elemento mximo se encuentra en la ra a z. 2. Un heap de n elementos tiene altura h = log2(n + 1) . La consulta del mximo es sencilla y eciente pues a basta examinar la ra z.

Colas de prioridad

314

Cmo eliminar el mximo? Un procedimiento que se o a emplea a menudo consiste en ubicar al ultimo elemento del mont culo (el del ultimo nivel ms a la a derecha) en la ra sustituyendo al mximo; ello z, a garantiza que se preservan las propiedades 1-2. Pero como la propiedad 3 deja eventualmente de satisfacerse, debe reestablecerse el invariante para lo cual se emplea un procedimiento privado denominado hundir. Este consiste en intercambiar un nodo con el mayor de sus dos hijos si el nodo es menor que alguno de ellos, y repetir este paso hasta que el invariante se haya reestablecido.

Colas de prioridad

315

32

72

34

59

63

17

29

37

33

29

72

32

34

59

63

17

29

37

33

29

72

63

34

59

32

17

29

37

33

29

Colas de prioridad

316

Cmo aadir un nuevo elemento? Una posibilidad o n consiste en colocar el nuevo elemento como ultimo elemento del mont culo, justo a la derecha del ultimo o como primero de un nuevo nivel. Para ello hay que localizar al padre de la primera hoja y sustituirla por un nuevo nodo con el elemento a insertar. A continuacin hay que reestablecer el o orden de montculo empleando para ello un procedimiento flotar, que trabaja de manera similar pero a la inversa de hundir: el nodo en curso se compara con su nodo padre y se realiza el intercambio si ste es mayor que el padre, iterando e este paso mientras sea necesario.

Colas de prioridad

317

Puesto que la altura del heap es (log n) el coste de inserciones y eliminaciones es O (log n). Se puede implementar un heap mediante memoria dinmica. a La representacin elegida debe incluir apuntadores o al hijo izquierdo y derecho y tambin al padre, y e resolver de manera ecaz la localizacin del ultimo o elemento y del padre de la primera hoja. Una alternativa atractiva es la implementacin de o heaps mediante un vector. No se desperdicia demasiado espacio ya que el heap es quasi-completo. Las reglas para representar los elementos del heap en un vector son simples: 1. A[1] contiene la ra z. 2. Si 2i n entonces A[2i] contiene al hijo izquierdo del elemento en A[i] y si 2i + 1 n entonces A[2i + 1] contiene al hijo derecho de A[i]. 3. Si i div 2 1 entonces A[i div 2] contiene al padre de A[i].

Colas de prioridad

318

Ntese que las reglas anteriores implican que los o elementos del mont culo se ubican en posiciones consecutivas del vector, colocando la ra en la z primera posicin y recorriendo el rbol por niveles, o a de izquierda a derecha.
template < typename Elem , typename Prio > class ColaPrioridad { public : ... private : static const int MAX_ELEM = ...; int nelems ; // n mero de elementos en el heap u // array de MAX_ELEMS pares < Elem , Prio > , // la componente 0 no se usa pair < Elem , Prio > h [ MAX_ELEM + 1]; void flotar ( int j ) throw (); void hundir ( int j ) throw (); };

Colas de prioridad

319

template < typename Elem , typename Prio > bool ColaPrioridad < Elem , Prio >:: vacia () const throw () { return nelems == 0; } template < typename Elem , typename Prio > void ColaPrioridad < Elem , Prio >:: inserta ( cons Elem & x , cons Prio & p ) throw ( error ) { if ( nelems == MAX_ELEMS ) throw error ( ColaLlena ); ++ nelems ; h [ nelems ] = make_pair (x , p ); flotar ( nelems ); } template < typename Elem , typename Prio > Elem ColaPrioridad < Elem , Prio >:: min () const throw ( error ) { if ( nelems == 0) throw error ( ColaVacia ); return h [1]. first ; } template < typename Elem , typename Prio > Prio ColaPrioridad < Elem , Prio >:: prio_min () const throw ( error ) { if ( nelems == 0) throw error ( ColaVacia ); return h [1]. second ; } template < typename Elem , typename Prio > void ColaPrioridad < Elem , Prio >:: elim_min () const throw ( error ) { if ( nelems == 0) throw error ( ColaVacia ); swap ( h [1] , h [ nelems ]); -- nelems ; hundir (1); }

Colas de prioridad

320

// // // //

Versi n recursiva . o Hunde el elemento en la posici n j del heap o hasta reestablecer el orden del heap ; por hip tesis los sub rboles del nodo j son heaps . o a

// Coste : O ( log ( n / j )) template < typename Elem , typename Prio > void ColaPrioridad < Elem , Prio >:: hundir ( int j ) throw () { // si j no tiene hijo izquierdo , hemos terminado if (2 * j > nelems ) return ; int minhijo = 2 * j ; if ( minhijo < nelems && h [ minhijo ]. second > h [ minhijo + 1]. second ) ++ minhijo ; // // // if minhijo apunta al hijo de minima prioridad de j si la prioridad de j es mayor que la de su menor hijo intercambiar y seguir hundiendo ( h [ j ]. second > h [ minhijo ]. second ) { swap ( h [ j ] , h [ minhijo ]); hundir ( minhijo );

} }

Colas de prioridad

321

// // // //

Versi n iterativa . o Hunde el elemento en la posici n j del heap o hasta reestablecer el orden del heap ; por hip tesis los sub rboles del nodo j son heaps . o a

// Coste : O ( log ( n / j )) template < typename Elem , typename Prio > void ColaPrioridad < Elem , Prio >:: hundir ( int j ) throw () { bool fin = false ; while (2 * j <= nelems && ! fin ) { int minhijo = 2 * j ; if ( minhijo < nelems && h [ minhijo ]. second > h [ minhijo + 1]. second ) ++ minhijo ; if ( h [ j ]. second > h [ minhijo ]. second ) { swap ( h [ j ] , h [ minhijo ]); j = minhijo ; } else { fin = true ; } } }

Colas de prioridad

322

// Flota al nodo j hasta reestablecer el orden del heap ; // todos los nodos excepto el j satisfacen la propiedad // de heap // Coste : O ( log j ) template < typename Elem , typename Prio > void ColaPrioridad < Elem , Prio >:: flotar ( int j ) throw () { // si j es la ra z , hemos terminado if ( j == 1) return ; int padre = j / 2; // si el padre tiene mayor prioridad // que j , intercambiar y seguir flotando if ( h [ j ]. second < h [ padre ]. second ) { swap ( h [ j ] , h [ padre ]); flotar ( padre ); } }

Colas de prioridad

323

Heapsort
 

Heapsort (Williams, 1964) ordena un vector de n elementos construyendo un heap con los n elementos y extrayndolos, uno a uno del heap a e continuacin. El propio vector que almacena a los n o elementos se emplea para construir el heap, de modo que heapsort acta in-situ y slo requiere un u o espacio auxiliar de memoria constante. El coste de este algoritmo es (n log n) (incluso en caso mejor) si todos los elementos son diferentes. En la prctica su coste es superior al de quicksort, a ya que el factor constante multiplicativo del trmino e n log n es mayor.

Colas de prioridad

324

// Ordena el vector v [1.. n ] // ( v [0] no se usa ) // de Elem s de menor a mayor template < typename Elem > void heapsort ( Elem v [] , int n ) { crea_max_heap (v , n ); for ( int i = n ; i > 0; --i ) { // saca el mayor elemento del heap swap ( v [1] , v [ i ]); // hunde el elemento de indice 1 // para reestablecer un max - heap en // el subvector v [1.. i -1] hundir (v , i -1 , 1); } }

i 1 A n

heap _ _ _ _ A[1] < A[i+1] < A[i+2] < ... < A[n] A[1] = max 1 < k <i A[k] _ _

ordenado

Colas de prioridad

325

76

76 72 34 59 63 17 29 37 33 29

i = 10

72

34

59

63

17

29

37

33

29

29

29 72 34 59 63 17 29 37 33 76

i=9

72

34

59

63

17

29

37

33

72

72 63 34 59 29 17 29 37 33 76

i=9

63

34

59

29

17

29

37

33

Colas de prioridad

326

33

33 63 34 59 29 17 29 37 72 76

i=8

63

34

59

29

17

29

37

33

33 59 34 37 29 17 29 63 72 76

i=7

59

34

37

29

17

29

59 37 34 33 29 17 29 63 72 76 59
i=7

37

34

33

29

17

29

Colas de prioridad

327

// Da estructura de max - heap al // vector v [1.. n ] de Elem s ; aqu // cada elemento se identifica con su // prioridad template < typename Elem > void crea_max_heap ( Elem v [] , int n ) { for ( int i = n /2; i > 0; --i ) hundir (v , n , i ); }

Colas de prioridad

328

13

13 2 5 27 8 15 10 4
i=8

27

15

10

4 satisfacen la propiedad de heap

13

hundir(A, 8, 4) hundir(A, 8, 3)

15

27

10

27

hundir(A, 8, 2) hundir(A, 8, 1)

13

15

10

Colas de prioridad

329

Sea H(n) el coste en caso peor de heapsort y B(n) el coste de crear el heap inicial. El coste en caso peor de hundir(v, i 1, 1) es O (log i) y por lo tanto
i=n

H(n) = B(n) + O (log i)


i=1

= B(n) + O

1in

log2 i

= B(n) + O (log(n!)) = B(n) + O (n log n) Un anlisis grueso de B(n) indica que a B(n) = O (n log n) ya que hay (n) llamadas a hundir, cada una de las cuales tiene coste O (log n). Podemos concluir por tanto que H(n) = O (n log n). No es dif construir una entrada de tamao n tal cil n que H(n) = (n log n) y por tanto H(n) = (n log n) en caso peor. La demostracin de que el coste de o heapsort en caso mejor3 es tambin (n log n) es e bastante ms complicada. a
3 Siendo todos los elementos distintos.

Colas de prioridad

330

Por otra parte, la cota dada para B(n) podemos renarla ya que B(n) =

1i n/2

O (log(n/i))

nn/2 = O log (n/2)! = O log(2e)n/2 = O (n). Puesto que B(n) = (n), podemos armar que B(n) = (n). Otra forma de demostrar que B(n) es lineal consiste en razonar del siguiente modo: Sea h = log2(n + 1) la altura del heap. En el nivel h 1 k hay como mucho 2h1k < n+1 2k

nodos y cada uno de ellos habr de hundirse en caso a peor hasta el nivel h 1; eso tiene coste O (k).
Colas de prioridad 331

Por lo tanto, B(n) =

0kh1

O (k)

n+1 2k

=O n

k 2k 0kh1 k 2k k0 = O (n),

=O n ya que

k 2k = 2. k0 En general, si r < 1, k rk =
k0

r . 2 (1 r)

Colas de prioridad

332

Aunque globalmente H(n) = (n log n), es interesante el anlisis detallado de B(n). Por a ejemplo, utilizando un min-heap podemos hallar los k menores elementos de un vector (y en particular el k-simo) con coste: e S(n, k) = B(n) + k O (log n) = O (n + k log n). y si k = O (n/ log n) entonces S(n, k) = O (n).

Colas de prioridad

333

VII Particiones

 

Particiones

Una particin de un conjunto no vac A es una o o coleccin de subconjuntos no vac o os = {A1, . . . , Ak } tal que / 1. Si i = j entonces Ai A j = 0. 2. A =
S
1ik

Ai.

Se suele denominar bloque de la particin a cada o uno de los Ais. Las particiones y las relaciones de equivalencia estn estrechamente ligadas. a Recordemos que es una relacin de equivalencia o en A si y slo si o 1. es reexiva: para todo a A , a a. 2. es transitiva: si a b y b c, entonces a c, para cualesquiera a, b y c en A . 3. es simtrica: a b si y slo si b a, para e o cualesquiera a y b en A .
Particiones 335

Dada una particin de A , sta induce una o e relacin de equivalencia denida por o x y x e y pertenecen a un mismo bloque Ai Y a la inversa, dada una relacin de equivalencia o en A , sta induce una particin = {Ax}xA , donde e o Ax = {y A | y x}.

Particiones

336

Al subconjunto de elementos equivalentes a x se le denomina clase de equivalencia de x. Cada uno de los bloques de la particin inducida por una relacin o o es por lo tanto una clase de equivalencia. Ntese o que si x y entonces Ax = Ay. Un elemento cualquiera de la clase Ax se denomina representante de la clase. En muchos algoritmos, especialmente en algoritmos sobre grafos, es importante poder representar particiones de un conjunto nito de manera eciente.

Particiones

337

Vamos a suponer que el conjunto soporte sobre el que se dene la particin es {1, . . . , n}; sin excesiva o dicultad, empleando alguna de las tcnicas vistas e en temas precedentes podemos representar de manera eciente una biyeccin entre un conjunto o nito A de cardinalidad n y el conjunto {1, . . . , n} y con ello una particin sobre el conjunto soporte A o en caso necesario. Adicionalmente, supondremos que el conjunto soporte es esttico, es decir, ni se aaden ni se a n eliminan elementos. No obstante, con poca dicultad extra podemos obtener representaciones ecientes para particiones de conjuntos dinmicos. a

Particiones

338

Generalmente el tipo de operaciones que el TAD PARTICION debe soportar son las siguientes: 1) dados dos elementos i y j, determinar si pertenecen al mismo bloque o no; 2) dados dos elementos i y j fusionar los bloques a los que pertenecen (si procede) en un slo bloque, devolviendo la particin o o resultante. Frecuentemente el primer tipo de operacin se o realiza mediante una operacin find que dado un o elemento i, devuelve un representante de la clase a la que pertenece i. Si dos elementos i y j tienen el mismo representante, entonces han de estar en el mismo bloque. El segundo tipo de operacin se llama merge o o union. De ah que las particiones se les llame a menudo estructuras union-nd o mfsets (abreviacin de merge-nd sets). o La operacin make crea una particin de {1, . . . , n} o o consistente en n bloques cada uno de los cuales contiene un elemento.

Particiones

339

TAD MFSET genero mfset ops make: nat mfset find: mfset nat mfset union: mfset mfset mfset fops ecns 1) 1 i n = find(make(n), i) = i 2) i < 1 i > n = find(make(n), i) = error 3) find(m, k) = find(m, i) find(m, k) = find(m, j) = find(union(m, i, j), k) {find(m, i), find(m, j)} 4) find(m, k) = find(m, i) find(m, k) = find(m, j) = find(union(m, i, j), k) = find(m, k) fecns

Particiones

340

En muchas aplicaciones antes de hacer cualquier union se habr determinado previamente si los a elementos i y j cuyos bloques habr de unirse, an estn o no en el mismo bloque; para ello se a habr preguntado si find(m, i) = find(m, j) o no. a Por esta razn es habitual que en el TAD o PARTICION la operacin union slo acte sobre o o u elementos que son representantes de sus respectivas clases. ... u = find(m, i); v = find(m, j) si u = v entonces union(m, u, v) . . . fsi

Particiones

341

Puesto que la operacin make recibe como o parmetro el nmero de elementos n del conjunto a u soporte, podremos utilizar implementaciones en vector, reclamando un vector con el nmero u apropiado de componentes a la memoria dinmica si a nuestro lenguaje de programacin lo soporta. o Tambin ser posible utilizar este tipo de e a implementacin si el valor de n est acotado y dicha o a cota no es irrazonablemente grande. En cualquier caso podremos modicar sin demasiado esfuerzo las implementaciones que se vern a continuacin para a o que sean completamente dinmicas y funcionen a correctamente en aquellos casos en que no se puedan crear vectores cuyo tamao se ja en n tiempo de ejecucin o si el conjunto soporte ha de o soportar inserciones y borrados. Por ejemplo, podemos emplear una estructura jerrquica (rbol) a a que indexa grupos de N elementos: cada grupo se puede implementar como se describe a continuacin o y la estructura jerrquica nos permitir acceder al o a a a los grupo(s) pertinentes de manera eciente.

Particiones

342

Quick-nd consiste representar la particin o mediante un vector P y almacenar en la componente i de P el representante de la clase a la que pertenece i. De este modo la operacin find tiene o coste constante, ya que basta examinar P[i]. Sin embargo, la operacin union tendr coste (n) ya o a que cada uno de los elementos de la clase en la que est j (los ks tales que P[k] = P[ j]) han de cambiar a de representante (P[k] = P[i]). O bien los elementos del mismo bloque que i han de pasar al bloque de j. Con algunas modicaciones puede evitarse el recorrido completo del vector P y restringirlo a los elementos del bloque de j (o del bloque de i); pero an as el coste de una union sigue siendo lineal en u n en el caso peor, ya que cualquiera de los dos bloques pude contener una fraccin considerable del o total de los elementos.

Particiones

343

Aunque resulta un tanto forzado, conviene contemplar la representacin quick-nd como un o bosque de rboles; cada rbol representa a un a a bloque de la particin en un momento dado. Los o rboles estn representados mediante apuntadores a a al padre, siendo la ra de cada rbol el z a representante del bloque correspondiente. Puesto que la ra de un rbol no tiene padre, las ra se z a ces apuntan a s mismas. Del invariante de la representacin de quick-nd se sigue que todos los o rboles tienen altura 1 (si slo contienen un a o elemento) o altura 2 (todos los elementos de un bloque excepto el representante estn en el segundo a nivel, apuntando a la ra z).

Particiones

344

Particiones

345

Otra estrategia, quick-union, explota la correspondenicia entre bloques y rboles del a siguiente modo: para unir los bloques de i y j se localizan al representante de i, digamos u, y se coloca a u como hijo de j. Alternativamente, podemos localizar ambos representantes y hacer que uno de ellos sea hijo del otro. accion make(sal m : mfset; ent n : nat) var i : nat fvar m.P = nuevo(nat, n) m.n = n para i = 1 hasta n hacer m.P[i] = i fpara faccion accion union(ent/sal m : mfset; ent i, j : nat) u = find(m, i) v = find(m, j) m.P[u] = v faccion

Particiones

346

funcion find(ent m : mfset; ent i : nat) retorna nat var j : nat fvar si i < 1 i > m.n entonces ERROR(ElementoNoExiste) fsi j=i mientras m.P[ j] = j hacer j = m.P[ j] fmientras retorna j ffuncion Aunque en general los rboles resultantes de una a secuencia de uniones sern relativamente a equilibrados y el coste de las operaciones ser bajo, a en caso peor podemos crear rboles poco a equilibrados de modo que tanto una union como un find tengan coste proporcional al nmero de u elementos involucrados. Por ejemplo, si realizamos una secuencia de uniones de tal modo que la clase en la que est j slo contenga a j, obtendremos a o rboles equivalentes a listas. a

Particiones

347

Particiones

348

El prrafo previo sugiere posibles soluciones al a problema. En la unin por peso el rbol con menos o a elementos es que el que se aade como hijo del que n tiene ms elementos. En la unin por rango el rbol a o a de menor altura es el que se pone como hijo del de mayor altura. Tanto una como otra estrategia son fciles de implementar, pero requieren, en principio, a que se almacene informacin auxiliar sobre el o tamao o la altura de los rboles. n a Se puede evitar el uso de espacio auxiliar observando que slo se necesita esta informacin de o o tamao o altura para las ra y que el espacio n ces correspondiente a sus apuntadores es esencialmente intil, ya que slo se precisar un bit que indique u o a que i es una ra o no. Por ejemplo, podemos z adoptar el convenio de que si m.P[i] < 0 entonces i es una ra y m.P[i] es el tamao del rbol. z n a

Particiones

349

accion union(ent/sal m : mfset; ent i, j : nat) u = find(m, i) v = find(m, j) si m.P[u] > m.P[v] entonces u v fsi m.P[v] = m.P[v] + m.P[u] m.P[u] = v faccion funcion find(ent m : mfset; ent i : nat) retorna nat var j : nat fvar si i < 1 i > m.n entonces ERROR(ElementoNoExiste) fsi j=i mientras m.P[ j] 0 hacer j = m.P[ j] fmientras retorna j ffuncion El rendimiento O (log n) de estas operaciones es consecuencia directa del siguiente lema.
Particiones 350

Lema 9. Dado un bloque de tamao k en un mfset n con unin por peso, la altura del rbol correspono a diente es log2 k.

Particiones

351

Demostracin. Si k = 0, el lema es obviamente ciero to. Supongamos que es cierto para todos los tamaos n hasta k y demostraremos entonces que es cierto para k + 1. Sea t el rbol correspondiente a un bloque a de tamao k + 1. Dicho bloque es el resultado de la n unin de dos bloques de tamaos r y s, r s k. El o n rbol t tiene altura h(t) m x{log2 r +1, log2 s}, aplia a cando la hiptesis de induccin, y por la denicin de o o o unin por peso. Supongamos que log2 r + 1 log2 s. o Entonces h(t) m x{log2 r + 1, log2 s} a = log2 s < log2(k + 1). Por otro lado, si log2 r + 1 > log2s, y teniendo en cuenta que k + 1 = r + s 2r, h(t) m x{log2 r + 1, log2 s} a = log2 r + 1 = log2(2r) log2(k + 1).

Particiones

352

Todav puede conseguirse un mejor rendimiento a empleando una heur stica de compresin de o caminos. La idea es reducir la distancia a la ra de z los elementos en el camino de i hasta la ra durante z una operacin find(m, i). Mientras se asciende o desde i hasta la ra se disminuye la altura del rbol. z a Subsiguientes finds que afecten a i o alguno de los elementos que eran antecesores suyos sern ms a a rpidos. La compresin de caminos acorta caminos a o de manera que poco a poco los rboles adoptan la a forma que tendr con quick-nd, pero no teniendo an que encargarse de ello la operacin union ni o compactndose todo un rbol de una sola vez. a a Se ha demostrado que una secuencia de m uniones y n finds tiene coste O ((m + n) (m, n)), donde (m, n) es la denominada funcin inversa de o Ackermann. Puesto que (m, n) 4 para cualesquiera valores de m y n concebibles en la prctica, el coste puede considerarse O (m + n). a Aunque el coste de una union o un find no es constante, el coste amortizado s lo es ya que el coste de las m + n operaciones es O (m + n) en caso peor.
Particiones 353

1: Compresin por mitades: se modica el apuntador o de cada elemento en el camino desde i hasta find(m, i), excepto a find(m, i) y el hijo de ste, e para que apunte a su abuelo. funcion find(ent m : mfset; ent i : nat) retorna nat var j, k : nat fvar si i < 1 i > m.n entonces ERROR(ElementoNoExiste) fsi j = i; k = m.P[ j] mientras m.P[k] = k hacer m.P[ j] = m.P[k] j=k k = m.P[ j] fmientras retorna k ffuncion

Particiones

354

2: Compresin total : se modica el apuntador de cao da elemento en el camino desde i hasta find(m, i), para que apunte a la ra z. funcion find(ent m : mfset; ent i : nat) retorna nat var j, k, aux : nat fvar si i < 1 i > m.n entonces ERROR(ElementoNoExiste) fsi j=i mientras m.P[ j] = j hacer j = m.P[ j] fmientras k=i mientras m.P[k] = k hacer aux = m.P[k]; m.P[k] = j; k = aux fmientras retorna k ffuncion

Particiones

355

VIII Grafos

Algoritmo de Dijkstra
 

Dado un grafo dirigido G = V, E etiquetado, el algoritmo de Dijkstra (1959) nos permite hallar los caminos m nimos desde un vrtice s V dado a e todos los restantes vrtices del grafo. e Si el grafo G contuviera ciclos de peso negativo la nocin de camino m o nimo no estar bien denida a para todo par de vrtices, pero el algoritmo de e Dijkstra puede fallar en presencia de arcos con peso negativo, incluso si no hay ciclos de peso negativo. Supondremos por lo tanto que todos los pesos : E R+ son positivos. Sea P (u, v) el conjunto de caminos entre dos vrtices u y v de G. Dado un camino e = [u, . . . , v] P (u, v) su peso es la suma de los pesos de los n arcos que lo forman: () = (u, v1) + (v1, v2) + + (vn1, v).

Grafos

357

Sea (u, v) = mn{() | P (u, v)} y (u, v) un camino de P (u, v) cuyo peso es m nimo. Si / P (u, v) = 0 entonces tomamos (u, v) = +, por convenio. Consideraremos en primer lugar la versin o del algoritmo de Dijkstra que calcula los pesos de los caminos m nimos desde un vrtice a todos los e restantes, y ms tarde la versin que calcula a o adicionalmente los caminos propiamente dichos. G = V, E es un grafo dirigido con pesos positivos sV dijkstra(G, s, D) Para todo u V , D[u] = (s, u) G = V, E es un grafo dirigido con pesos positivos sV dijkstra(G, s, D, cam) Para todo u V , D[u] = (s, u) Para todo u V , D[u] < + = cam[u] = (s, u)

Grafos

358

El algoritmo de Dijkstra acta en una serie de u etapas. En todo momento el conjunto de vrtices V e se divide en dos partes: los vrtices vistos y los e vrtices no vistos o candidatos. Al inicio de cada e etapa, si u es un vrtice visto entonces e D[u] = (s, u); si u es un candidato entonces D[u] es el peso del camino m nimo entre s y u que pasa exclusivamente por vrtices intermedios vistos. e Este es el invariante del algoritmo de Dijkstra. En cada etapa un vrtice pasa de ser candidato a e ser visto. Y cuando todos los vrtices son vistos e entonces tenemos completamente resuelto el problema.

Grafos

359

Qu vrtice candidato debe seleccionarse en cada e e etapa para pasar a ser visto? Intuitivamente, aqul e cuya D sea m nima de entre todos los candidatos. Sea u dicho vrtice. Entonces D[u] no slo es el e o peso del camino m nimo entre s y u que slo pasa o por vistos (segn el invariante), sino el peso del u camino m nimo. En efecto, si el camino m nimo pasase por algn otro vrtice no visto x, tendr u e amos que el peso de dicho camino es D[x] + (x, u) < D[u], pero como (x, u) 0 y D[u] es m nimo llegamos a una contradiccin. o

Grafos

360

Ahora debemos pensar cmo mantener el resto del o invariante. Para los vrtices vistos D ya tiene el e valor adecuado (inclu u, el vrtice que pasa de do e candidatos a vistos, como acabamos de ver). Pero si v es un candidato, D[v] tal vez haya de cambiar ya que tenemos un vrtice adicional visto, el vrtice u. e e Un sencillo razonamiento demuestra que si el camino m nimo entre s y v que pasa por vrtices e vistos incluye a u entonces u es el inmediato antecesor de v en dicho camino.

Grafos

361

De ah se sigue que el valor de D slo puede o cambiar para los vrtices v sucesores de u. Esto e ocurrir si y slo si a o D[v] > D[u] + (u, v). Si v fuera sucesor de u pero ya estuviera visto la condicin anterior no puede ser cierta. o

Grafos

362

accion dijkstra(ent g : grafo vertice, real ; ent s : vertice; sal D : dict vertice, real ) var cand : conjunto vertice u, v : vertice; d : real fvar para v V (g) hacer D[v] = + fpara D[s] = 0 cand = V (g) / mientras cand = 0 hacer u = el v rtice de cand con D mnima e cand = cand \ {u} para v sucesores(g, u) hacer d = D[u] + etiqueta(g, u, v) si d < D[v] entonces D[v] = d fsi fpara fmientras faccion

Grafos

363

CANDIDATOS

1 2 3 4 5 6 0 {1, 2, 3, 4, 5, 6} 0 3 6 {2, 3, 4, 5, 6} 0 3 6 7 4 {3, 4, 5, 6} 0 3 6 7 4 9 {3, 4, 6} 0 3 6 7 4 8 {4, 6} 0 3 6 7 4 8 {6} / 0

Grafos

364

Para conseguir que el algoritmo compute los caminos m nimos propiamente dichos, empezamos observando que si el camino m nimo entre s y u pasa por x entonces la parte de ese camino que nos lleva de s a x ha de ser necesariamente el camino m nimo entre s y x. Por lo tanto, bastar con computar un a rbol de caminos m a nimos que impl citamente se representa mediante una tabla cam tal que: s si v = s, cam[v] = u si (u, v) es el ultimo arco de (s, v), si (s, v) = +, donde cam[v] = indica que cam[v] no est denido. a

Grafos

365

Claramente, los unicos cambios que sern necesarios a son: 1. inicializar cam[s] = s. 2. incluir la actualizacin de cam en el bucle interno: o ... para v sucesores(g, u) hacer d = D[u] + etiqueta(g, u, v) si d < D[v] entonces D[v] = d cam[v] = u fsi fpara ... Si queremos el camino completo de s a v podemos deshacer el recorrido a la inversa: cam[u], cam[cam[u]], . . . , cam[ [cam[u]] ] hasta que lleguemos a s.
Grafos 366

Sea n el nmero de vrtices de g y m el nmero de u e u arcos. Si el grafo g se implementa mediante matriz de adyacencias, el coste del algoritmo de Dijkstra es (n2) ya que se hacen n iteraciones del bucle principal y dentro de cada una de ellas se incurrir a en un coste como m nimo (n) para recorrer los sucesores del vrtice seleccionado. Descartaremos e esta posibilidad, y supondremos que el grafo se implementa mediante listas de adyacencia que nos darn mejores resultados tanto en tiempo como en a espacio. Supongamos, para simplicar, que V (g) = {1, . . . , n} y que implementamos el conjunto cand y el diccionario D mediante sencillas tablas indexadas de 1 a n (cand[i] = cierto si y slo si el vrtice i es un o e candidato). Entonces el coste del algoritmo de Dijkstra es (n2 + m) = (n2) puesto que se hacen n iteraciones del bucle principal, buscar el m nimo de la tabla D en cada iteracin tiene coste (n) y o actualizar la tabla D tiene coste proporcional al nmero de sucesores del vrtice seleccionado u e (combinando la obtencin de los vrtices sucesores o e y la etiqueta de los arcos correspondientes).
Grafos 367

Si V (g) es un conjunto arbitrario, podemos conseguir el mismo rendimiento utilizando tablas de hash para implementar cand y D. Y el problema sigue siendo la ineciencia en la seleccin del vrtice o e con D m nima. Para mejorar la eciencia podemos convertir cand en una cola de prioridad cuyos elementos son vrtices y donde la prioridad de cada vrtice es su e e correspondiente valor de D:
Grafos 368

var cand : cola prio vertice, real u, v : vertice; d : real fvar cand = vacia() D[s] = 0 para v V (g) hacer D[v] = + fpara para v V (g) hacer inserta(cand, v, D[v]) fpara mientras es vacia(cand) hacer u = min(cand); elim min(cand) para v sucesores(g, u) hacer ... fpara fmientras

Grafos

369

El problema es que dentro del bucle que recorre los sucesores de u se puede modicar el valor de D (la prioridad) de vrtices candidatos. Por ello e deberemos dotar al TAD COLA PRIO de una operacin adicional que, dado un elemento, nos o permita decrementar su prioridad (los valores de D se modican siempre a la baja). para v sucesores(g, u) hacer d = D[u] + etiqueta(g, u, v) si d < D[v] entonces D[v] = d; decr prio(cand, v, d) fsi fpara

Grafos

370

Si los costes de las operaciones sobre la cola de prioridad son O (log n) entonces el coste del bucle principal es D(n) =

vV (g)

O (log n) (1 + # sucesores de v)

= (n log n) + O (log n)

vV (g)

# sucesores de v

= (n log n) + O (m log n) = O ((n + m) log n) Por otra parte, el coste de las inicializaciones previas es O (n log n). Por lo tanto el coste que nos queda es O ((n + m) log n). Puede demostrarse que, de hecho, el coste en caso peor del algoritmo de Dijkstra es ((m + n) log n). Pero en muchos casos el coste es menor, porque no hay que usar decr prio para todos los sucesores del vrtice seleccionado y/o e porque el coste de las operaciones sobre la cola de prioridad es frecuentemente menor que (log n).

Grafos

371

Al crear cand sabemos cuntos vrtices tiene el a e grafo y podemos crear dinmicamente una a estructura de datos para ese tamao. n Adicionalmente podemos evitar la redundancia de la tabla D, ya que para cada elemento de cand tenemos su prioridad, que es su valor de D. Necesitamos por lo tanto un TAD con la funcionalidad combinada t pica de las colas de prioridad y de los diccionarios: TAD COLA PRIO DIJKSTRA ELEM,PRIO genero priodijks ops crea: nat priodijks inserta: priodijks elem prio priodijks min: priodijks elem elim min: priodijks priodijks prio: priodijks elem prio esta: priodijks elem bool decr prio: priodijks elem prio priodijks es vacia: priodijks bool fops

Grafos

372

var cand : priodijks vertice, real u, v : vertice; d, du : real fvar cand = crea(numero vertices(g)) para v V (g) hacer inserta(cand, v, +) fpara decr prio(cand, s, 0) / mientras cand = 0 hacer u = min(cand); du = prio(cand, u) elim min(cand) para v sucesores(g, u) hacer si esta(cand, v) entonces d = du + etiqueta(g, u, v) si d < prio(cand, v) entonces decr prio(cand, v, d) fsi fsi fpara fmientras

Grafos

373

Pueden conseguirse costes logar tmicos o inferiores en todas las operaciones sobre la cola de prioridad utilizando un heap implementado en vector. Adems necesitaremos una tabla de hash que nos a permita traducir vrtices a e ndices. Y una tabla de ndices y otra de posiciones en el heap. tipo priodijks = tupla prio : tabla [. . .] de real e : tabla [. . .] de elem index : tabla [. . .] de pos : tabla [. . .] de entero nelems : entero map : tabla hash elem, entero ftupla ftipo Las tablas prio y e, junto al contador nelems representan al heap. Si e[i] = e j entonces index[i] = j y pos[ j] = i, es decir, pos nos da la posicin en el heap del elemento e j e index su o ndice, siendo valor(map, e j ) = j.

Grafos

374

La operacin vacia reclama a la memoria dinmica o a las tablas prio, e, index y pos con n componentes y crea la tabla de hash. Cada vez que se inserta un nuevo elemento se le asigna el ndice nelems + 1, se inserta en map, se coloca en el heap en la posicin o nelems + 1, y se le hace otar. La operacin flotar o se encarga de mantener actualizadas las tablas pos e index. Para elim min, se intercambia el ultimo nodo del heap con la ra z prio[1] prio[nelems]; e[1] e[nelems] pos[index[1]] pos[index[nelems]] index[1] index[nelems] A continuacin se hunde la ra cuidando de o z, mantener actualizadas las tablas pos e index.

Grafos

375

La operacin decr prio requiere averiguar el o ndice j del elemento e j cuya prioridad se va a decrementar usando la tabla de hash, usar la tabla pos[ j] para obtener la posicin i del nodo que le o corresponde en el heap, y una vez modicada la prioridad, otar el nodo. Es fcil comprobar que las operaciones del TAD, a exceptuando crea, tienen coste (1) (es vacia, min, prio, esta) o O (log n) (inserta, elim min, decr prio).

Grafos

376

IX Ejemplos

Pilas: Evaluacin de expresiones o


 

Nuestro objetivo es disear un algoritmo que evale n u una expresin en notacin postja dada. Por o o ejemplo, dada la expresin o 3 6 + 10 2 - * su valor es 72 ((3 + 6) (10 2) = 72). Supondremos que las expresiones se construyen de acuerdo a la siguiente gramtica: a expr ::= numero | expr expr + | expr expr - | expr expr * | expr expr /

Ejemplos

378

La entrada del algoritmo es una lista de tokens que representa a una expresin sintcticamente correcta. o a const SUMA = 1; PROD = 2; . . . fconst tipo token = tupla es operando : bool valor : entero ftupla ftipo funcion eval(ent expr : lista(token)) retorna entero ... ffuncion

Ejemplos

379

Si pensamos en trminos recursivos, el algoritmo es e sencillo: salvo que la expresin consista en un o nmero (caso trivial), se evalan recursivamente las u u dos subexpresiones y a continuacin se aplica el o operador sobre los valores obtenidos. La transformacin recursivo-iterativa de este algoritmo o da lugar a un algoritmo tambin simple, que precisa e del uso de una pila. Se recorre secuencialmente la lista de tokens; si el token en curso es un operando se apila. Si el token es un operador se extraen los dos ultimos valores de la pila, se operan y se apila el resultado de nuevo. Una vez nalizado el recorrido de la lista de tokens, la pila contendr un slo elemento: el valor de la a o expresin. o Suponemos que la expresin es sintcticamente o a correcta y por ello no haremos la comprobacin de o errores que ser de rigor. a

Ejemplos

380

funcion eval(ent L : lista(token)) retorna entero var p : pila(entero) t : token op1, op2 : entero fvar vacia(p) primero(L) mientras es valido(L) hacer t = elemento(L) si t.es operando entonces apilar(p,t.valor) sino desapilar(p, op2) desapilar(p, op1) [ t.valor = SUMA apilar(p, op1 + op2) [] t.valor = RESTA apilar(p, op1 op2) [] t.valor = PROD apilar(p, op1 op2) [] . . . . . . ] fsi siguiente(L) fmientras retorna cima(p) ffuncion

Ejemplos

381

Secuencias: Dequeues
 

Una dequeue es una secuencia en la que las inserciones, borrados y consultas se pueden realizar en ambos extremos. Combina, pues, el comportamiento de pilas y colas.

Los valores del TAD se generan a partir de las generadoras puras empty e inject. TAD DEQUEUE(ELEM) genero dequeue ops empty: dequeue inject, push: dequeue elem dequeue eject, pop: dequeue dequeue front, rear: dequeue elem is empty: dequeue bool fops
Ejemplos 382

ecns 1) push(empty, e) = inject(empty, e) 2) push(inject(d, e), e) = inject(push(d, e), e) 3) eject(empty) = error 4) eject(inject(d, e)) = d 5) pop(empty) = error 6) pop(inject(empty, e)) = empty 7) pop(inject(inject(d, e), e) = inject(pop(inject(d, e)), e) 8) front(empty) = error 9) front(inject(empty, e)) = e 10) front(inject(inject(d, e), e)) = front(inject(d, e)) 11) rear(empty) = error 12) rear(inject(d, e)) = e 13) is empty(empty) = cierto 14) is empty(inject(d, e)) = falso fecns

Ejemplos

383

Para implementar las dequeues podemos usar una lista doblemente enlazada (eventualmente, cerrada circularmente y con elemento fantasma), obteniendo una solucin eciente en tiempo y en espacio. Para o una dequeue de n elementos, el espacio usado es (n). Todas las operaciones tienen coste (1). El doble encadenamiento es imprescindible para poder implementar de manera eciente la operacin eject o (o equivalentemente, la operacin pop). o Pero si el nmero de elementos mximo en la u a dequeue est acotado y podemos permitirnos a reservar para las dequeues una cantidad de espacio ja e independiente de sus tamaos (= nmero de n u elementos) entonces podemos usar una implementacin en vector circular, soportando muy o ecientemente todas las operaciones (coste: (1)).

Ejemplos

384

Los elementos de la dequeue se sitan en las u componentes indicadas por f r y re, pero consideramos que a la ultima componente cont[MAXELEMS 1] le sucede la primera cont[0]. El campo f r indica la posicin del frente de la o dequeue; el campo re la posicin del nal (rear ). o const MAXELEMS = . . . fconst tipo dequeue = tupla cont : tabla [0..MAXELEMS 1] de elem f r, re : 0..MAXELEMS 1 nelems : 0..MAXELEMS ftupla ftipo

Ejemplos

385

Ejemplos

386

accion empty(sal d : dequeue) d. f r = 0; d.re = MAXELEMS 1 d.nelems = 0 faccion funcion is empty(ent d : dequeue) retorna bool retorna d.nelems = 0 ffuncion accion inject(ent/sal d : dequeue; ent x : elem) si d.nelems = MAXELEMS entonces ERROR(FaltaMemoria) fsi d.re = (d.re + 1) mod MAXELEMS d.nelems = d.nelems + 1 d.cont[d.re] = x faccion accion push(ent/sal d : dequeue; ent x : elem) si d.nelems = MAXELEMS entonces ERROR(FaltaMemoria) fsi d. f r = (d. f r 1) mod MAXELEMS d.nelems = d.nelems + 1 d.cont[d. f r] = x faccion
Ejemplos 387

accion eject(ent/sal d : dequeue; sal x : elem) si d.nelems = 0 entonces ERROR(DequeueVacia) fsi x = d.cont[d.re] d.re = (d.re 1) mod MAXELEMS d.nelems = d.nelems 1 faccion accion pop(ent/sal d : dequeue; sal x : elem) si d.nelems = 0 entonces ERROR(DequeueVacia) fsi x = d.cont[d. f r] d. f r = (d. f r + 1) mod MAXELEMS d.nelems = d.nelems 1 faccion funcion front(ent d : dequeue) retorna elem si d.nelems = 0 entonces ERROR(DequeueVacia) fsi retorna d.cont[d. f r] faccion funcion rear(ent d : dequeue) retorna elem si d.nelems = 0 entonces ERROR(DequeueVacia) fsi retorna d.cont[d.re] faccion

Ejemplos

388

Listas autoorganizadas
 

Queremos implementar un TAD TABLA FREC TAD TABLA FREC(ELEM) genero tfrec ops vacia: tfrec incr: tfrec elem tfrec frec: tfrec elem nat fops mediante listas autoorganizadas. La operacin incr o incrementa la frecuencia de un elemento dado; frec nos permite consultar la frecuencia de un elemento dado.

Ejemplos

389

ecns 1) incr(incr( f , e), e ) = incr(incr( f , e ), e) 2) frec(vacia, e) = 0 3) e = e = frec(incr( f , e), e ) = frec( f , e ) + 1 4) e = e = frec(incr( f , e), e ) = frec( f , e ) fecns La lista autoorganizada es una lista enlazada simple, donde cada uno de los nodos contiene un par elemento-frecuencia. Todas las inserciones se efectuan en el principio de la lista y no hay eliminaciones ni operaciones de recorrido, por lo que bastar mantener un apuntador al primer elemento a de la lista. Por razones de eciencia y simplicidad del cdigo interesa tener un elemento fantasma o inicial.

Ejemplos

390

tipo ptr nodo = nodo nodo = tupla in f o : elem f rec : nat sig : ptr nodo ftupla lista = ptr nodo ftipo El rasgo distintivo de las listas autoorganizadas es que cada vez que se accede a un nodo para una consulta o modicacin, adems de realizarse la o a tarea en cuestin, se hace una reorganizacin de la o o lista. Una estrategia popular es mover al frente, de modo que el nodo recin accedido pasa a ocupar e el inicio de la lista (si no lo estaba ya). La idea es que los elementos a los que se accede ms a menudo estarn situados en la parte inicial de a a la lista y el coste promedio de las operaciones ser ms reducido. a a

Ejemplos

391

La opercin privada mtf coloca el nodo conteniendo o al elemento e al principio de la lista L si tal nodo existe y no estaba ya en primer lugar; en caso contrario, deja la lista L inalterada. accion mtf(ent/sal L : lista; ent e : elem) var p, q : ptr nodo fvar p = L sig; q = L q es el predecesor de p mientras p = NULL p in f o = e hacer q = p; p = p sig fmientras si p = NULL y q = L entonces q sig = p sig p sig = L sig L sig = p fsi faccion

Ejemplos

392

Ejemplos

393

Ejemplos

394

funcion nuevo nodo(ent e : elem; ent f : nat) retorna ptr nodo var p : ptr nodo fvar p = nuevo(nodo) si p = NULL entonces ERROR(FaltaMemoria) fsi p in f o = e p f rec = f retorna p ffuncion funcion es primero(ent L : lista; ent e : elem) retorna bool si L sig = NULL entonces retorna falso sino retorna L sig in f o = e fsi ffuncion accion incr(ent/sal L : lista; ent e : elem) var p : ptr nodo fvar mtf(L, e) si es primero(L, e) entonces L sig f rec = L sig f rec + 1 sino p = nuevo nodo(e, 1) p sig = L sig L sig = p fsi faccion
Ejemplos 395

funcion frec(ent/sal L : lista; ent e : elem) mtf(L, e) si es primero(L, e) entonces retorna L sig f rec sino retorna 0 fsi ffuncion Se ha demostrado que si los elementos x1, . . . , xn se acceden con probabilidades p1, . . . , pn entonces el nmero medio de elementos examinados en una u llamada a mtf es pi p j Cn = 1 + 2 1i< jn pi + + p j

Ejemplos

396

Arboles: Derivadas simblicas o 

Supongamos que representamos expresiones tales como sin(x) + 2 * x^3 - log(x + 3) mediante rboles. Cada nodo contendr un a a operando (variable o constante) o un operador. Los operandos estarn en las hojas del rbol de a a expresin, y los operadores en nodos cuyo grado o depender de la aridad del operador. Para a simplicar, asumiremos que las constantes son nmeros enteros, que slo hay una variable (x) y u o que los operadores posibles son, p.e., los siguientes: + (suma), - (resta), * (producto), / (divisin), ^ (potencia), o exp (exponencial), log (logaritmo), sin (seno), cos (coseno).

Ejemplos

397

const SUMA = 1; RESTA = 2; . . . fconst tipo tipo tk = (OP, CNST, VAR) token = tupla el tipo : tipo tk el valor : entero ftupla arbol expr = arbol bin(token) ftipo

Ejemplos

398

Crea un duplicado del arbol a Coste: (n); n = tama o del arbol n funcion copia(ent a : arbol bin) retorna arbol bin var p : ptr nodo fvar Aplicamos el esquema de recorrido en preorden si a = NULL entonces p = nuevo(nodo) si p = NULL entonces ERROR(FaltaMemoria) fsi p in f o = a in f o p izq = copia(a izq) p der = copia(a der) fsi retorna p ffuncion

Ejemplos

399

Omitiremos la declaraci n de variables locales o funcion derivada(ent a : arbol expr) retorna arbol expr si es vacio(a) entonces t = raiz(a) si t.el tipo = CNST entonces retorna arb cnst(0) fsi si t.el tipo = VAR entonces retorna arb cnst(1) fsi si t.el tipo = OP entonces [ t.el valor = SUMA retorna d suma(a) [] t.el valor = PROD retorna d prod(a) [] t.el valor = POT retorna d pot(a) [] . . . ] fsi fsi ffuncion

Ejemplos

400

funcion arb cnst(ent v : entero) retorna arbol expr retorna plantar( CNST, v , vacio, vacio) ffuncion funcion d suma(ent a : arbol expr) retorna arbol expr da1 = derivada(hijo izq(a)) da2 = derivada(hijo der(a)) retorna plantar(raiz(a), a1, a2) ffuncion funcion d prod(ent a : arbol expr) retorna arbol expr da1 = derivada(hijo izq(a)) da2 = derivada(hijo der(a)) a1 = copia(hijo izq(a)) a2 = copia(hijo der(a)) a1 = plantar( OP, PROD , a1, da2) a2 = plantar( OP, PROD , da1, a2) retorna plantar( OP, SUMA , a1, a2) ffuncion

Ejemplos

401

funcion d pot(ent a : arbol expr) retorna arbol expr el hijo derecho contiene el valor del exponente f = copia(hijo izq(a)) d f = derivada(hijo izq(a)) CNST, n = raiz(hijo der(a)) a1 = plantar( OP, POT , f , arb cnst(n 1)) a1 = plantar( OP, PROD , d f , a1) retorna plantar( OP, PROD , a1, arb cnst(n)) ffuncion funcion d log(ent a : arbol expr) retorna arbol expr f = copia(hijo izq(a)) d f = derivada(hijo izq(a)) retorna plantar( OP, DIV , d f , f ) ffuncion

Ejemplos

402

Arboles: Ejemplos diversos


 

Ejemplo 1: Implementar una funcin que calcule la o altura de un rbol binario T . a Se obtiene por aplicacin directa del esquema de o recorrido en postorden, puesto que la altura de T es el mximo de las alturas de sus subrboles ms a a a 1.

funcion altura(ent T : arb bin) retorna nat si es vacio(T ) entonces retorna 0 sino retorna 1 + max(altura(hijo izq(T )), altura(hijo der(T ))) fsi ffuncion

Devuelve el m ximo de x e y a funcion max(ent x, y : nat) retorna nat . . . ffuncion

Ejemplos

403

Ejemplos

404

Ejemplo 2: Implementar una funcin que calcule el o nmero de nodos de un rbol binario T cuyos dos u a subrboles son vac a os. Se obtiene por aplicacin directa de un esquema o de recorrido. Cualquiera de ellos puede servir; en nuestra solucin emplearemos el preorden. o funcion nodos sin hijos(ent T : arb bin) retorna nat si es vacio(T ) entonces retorna 0 sino si es nodo unico(T ) entonces retorna 1 sino retorna nodos sin hijos(hijo izq(T ))+ nodos sin hijos(hijo der(T )) fsi fsi ffuncion Pre: T no es vaco funcion es nodo unico(ent T : arb bin) retorna bool retorna es vacio(hijo izq(T )) es vacio(hijo der(T )) ffuncion

Ejemplos

405

Ejemplo 3: Implementar una funcin que lista todos o los caminos desde la ra a las hojas de un rbol z a (general) T . Para resolver este problema habr que generalizara lo de la siguiente forma. Sea p una secuencia dada de elementos y T un bosque; hay que desarrollar un algoritmo que genere una lista con todas las secuencias que se obtienen siguiendo los caminos desde cada una de las ra ces del bosque hasta las hojas, prejndolas con la secuencia dada p. a Ntese que en el problema original el rbol dado o a no puede ser vac (no existe el concepto de rbol o a sin elementos); en cambio, un bosque s puede ser vac Una especicacin consistente con la o. o denicin y adecuada a nuestro propsito es que o o el resultado del algoritmo sobre un bosque vac o sea una lista conteniendo una unica secuencia: p. El problema original se resuelve invocando a este algoritmo con el prejo vacia y con un bosque consistente en un unico rbol T . a

Ejemplos

406

El algoritmo lista caminos se obtiene aplicando el esquema de recorrido en preorden: si el bosque no es vac se visita la ra del primer rbol del bosque o, z a (aadiendola al prejo en curso para obtener un n nuevo prejo paux), se resuelve el problema recursivamente sobre el bosque de hijos del primer rbol con paux y nalmente se resuelve el problema a recursivamente sobre el resto del bosque con el prejo original.
Ejemplos 407

tipo camino = lista(elem) ftipo accion lista caminos(ent B : bosque; ent p : camino; ent/sal L : lista(camino) var e : elem; paux : camino fvar si es vacio(B) entonces a~adir final(L, p) n sino e = raiz(B); paux = p a~adir final(paux, e) n lista caminos(primg(B), paux, L) lista caminos(sig herm(B), p, L) fsi faccion

Ejemplos

408

Ejemplo 4: Desarrollar un algoritmo que reconstruya un rbol binario T dados sus recorridos (en a forma de lista de elementos) en preorden e inorden. Puede asumirse que todos los elementos son distintos. Sean PRE e IN las dos listas con los recorridos del rbol T a reconstruir. Si ambas son vac T a as, es el rbol vac Si una es vac pero la otra no a o. a lo es, no es posible reconstruir el rbol y de hecho a se trata de una situacin de error. o

Ejemplos

409

Si ambas secuencias son no vac entonces tendrn as a la siguiente estructura: el primer elemento r de PRE es la ra de T ; a continuacin viene una z o subsecuencia PRIZ que es el recorrido en preorden del hijo izquierdo de T y nalmente viene una subsecuencia PRDE con el recorrido en preorden del hijo derecho de T . La secuencia IN comienza con una subsecuencia INIZ correspondiente al inorden del hijo izquierdo de T , luego viene r y nalmente la subsecuencia INDE correspondiente al inorden del hijo derecho de T .

Ejemplos

410

Es obvio que bastar hacer llamadas recursivas con a las subsecuencias PRIZ e INIZ por un lado y PRDE e INDE por otro para reconstruir los subrboles a izquierdo y derecho de T , respectivamente. La parte no recursiva no trivial es por tanto averiguar donde se termina PRIZ y empieza PRDE. Pero es simple: PRIZ ha de tener la misma longitud que INIZ, por lo que ser suciente empezar en el a segundo y primer elemento de las listas PRE e IN y avanzar simultneamente en ambas listas hasta que a el elemento en curso de la lista IN sea r (o nos salgamos de cualquiera de las dos listas; esta es otra situacin de error). o

Ejemplos

411

funcion reconstruir arbol(ent PRE, IN : lista) retorna arb bin var PRE , IN : lista; r : elem fvar si es vacia(PRE) es vacia(IN) entonces retorna arbol vacio() fsi si es vacia(PRE) es vacia(IN) entonces ERROR(. . . ) fsi primero(PRE); r = elemento(PRE) punto corte sit a los puntos de inter s de PRE e IN u e sobre el primer elemento de PRDE y sobre r, resp. punto corte(PRE, IN, r) partir(PRE, PRE ) partir(IN, IN ) eliminar primero(PRE) eliminar primero(IN ) retorna plantar(r,reconstruir arbol(PRE, IN), reconstruir arbol(PRE , IN )) ffuncion

Ejemplos

412

accion punto corte(ent/sal PRE, IN : lista; ent r : elem) siguiente(PRE); primero(IN) f ound = f also mientras es valido(PRE) es valido(IN) f ound hacer f ound = elemento(IN) = r si f ound entonces siguiente(PRE); siguiente(IN) fsi fmientras si f ound entonces ERROR(. . . ) fsi faccion El coste R(n) de reconstruir un rbol de n nodos a satisface R(n) = m x {(k) + R(k) + R(n 1 k)} a
0k<n

ya que el coste de la parte no recursiva viene fundamentalmente de la llamada a punto corte y el coste de sta es directamente proporcional a la e longitud k de PRIZ (= longitud de INIZ). La
Ejemplos 413

solucin de esta recurrencia es R(n) = (n2); o sale tomando k = n 1. El coste promedio es (n n).

Ejemplos

414

Tambin es posible reconstruir un rbol binario a e a partir de sus recorridos en post- e inorden. No es factible reconstruirlo en cambio dados los recorridos pre- y postorden, ya que rboles distintos tienen a idnticos ambos recorridos. e

Ejemplos

415

BSTs: Ejemplos diversos


 

Ejemplo 1: Escribir un algoritmo que dado un BST t y un elemento (=clave) x devuelve dos BSTs t y t + que contienen los elementos menores o iguales y mayores que x respectivamente.

Ejemplos

416

Esta acci n es destructiva o Pre: T = t T = T + = NULL Post: T = t T + = t + accion partir(ent/sal T : bst; sal T , T + : bst; ent x : elem) si T = NULL entonces T = NULL; T + = NULL sino si T in f o x entonces T = T partir(T der, T der, T +) sino T+ = T partir(T izq, T , T + izq) fsi fsi faccion

Ejemplos

417

Ejemplo 2: Implementar un algoritmo que dado un BST t de tamao n > 0 y un rango i, 1 i n, n devuelve el i-simo elemento de t. Modicar la e representacin de los BSTs de manera que el o algoritmo sea eciente. Supongamos que el subrbol izquierdo tuviese k a elementos. Entonces si 1 i k el i-simo de e t estar en el subrbol izquierdo. Si i = k + 1 a a entonces la ra de t es el elemento pedido. Y z nalmente si k + 1 < i n entonces el i-simo e estar en el subrbol derecho; pero en trminos a a e relativos ser el i (k + 1)-simo del subrbol a e a derecho. La conclusin obvia es que cada nodo del o BST deber almacenar el nmero de elementos en a u su subrbol izquierdo, junto a su informacin y los a o apuntadores a los hijos izquierdo y derecho.

Ejemplos

418

Para mantener permanentemente actualizada el nmero de de elementos a la izquierda hay que u modicar los algoritmos de insercin y borrado. Por o ejemplo, cada vez que la insercin hubiese de o continuar, recursivamente, en el subrbol izquierdo a del nodo en curso el campo numizq deber a incrementarse en una unidad, pero si hubiese de continuar en el subrbol derecho no se modicar a a el campo. tipo ptr nodo = nodo nodo = tupla in f o : elem hizq, hder : ptr nodo numizq : nat ftupla bst = ptr nodo ftipo

Ejemplos

419

i=9> 5+ 1
5

f i = 9 6 = 3 < 3 + 1

c i = 3 > 1+ 1

i = 3 2 = 1 =0 + 1

Pre: 1 i t funcion i esimo(ent t : bst; ent i : nat) retorna elem var k : nat fvar k = t numizq si i k entonces retorna i esimo(t hizq, i) sino si i = k + 1 entonces retorna t in f o sino i > k+1 retorna i esimo(t hder, i k 1) fsi fsi ffuncion

Ejemplos

420

 

Particiones: Generacin de laberintos o

Dados m y n, 0 < m, n MAX, generar un laberinto aleatorio de tamao m n (i.e. con m n n encrucijadas, dispuestas sobre una rejilla de m las y n columnas) de tal modo que exista al menos un camino entre la entrada (1, 1) y la salida (m, n). Suponed que se dispone de una funcin rand(k) o que genera un nmero aleatorio entre 1 y k con u probabilidad uniforme.

Ejemplos

421

Para solucionar este problema emplearemos la siguiente estrategia: comenzamos con un laberinto en el que se alzan muros entre todas las encrucijadas colindantes (y un muro que rodea todo el per metro del laberinto, que no se puede derribar). Despus vamos derribando muros, al azar, e que conecten encrucijadas previamente inconexas hasta que que exista una conexin entre la entrada o y la salida. inicializar laberinto mientras no hay conexi n entre (1, 1) y (m, n) hacer o elegir un muro entre (u, v) y (u , v ) al azar si (u, v) y (u , v ) no est n conectados entonces a derribar el muro entre (u, v) y (u , v ) fsi fmientras Si sustitu mos la condicin del bucle por existen o posiciones del laberinto inconexas los laberintos generados suelen tener ms apariencia de a aleatoriedad.

Ejemplos

422

Para resolver ecientemente este problema podemos usar un TAD PARTICION con N = m n elementos. Numeraremos las posiciones del laberinto por columnas y las: a la posicin (i, j) le corresponde o el valor k = (i 1)n + j. A la inversa, dado k la posicin a la que representa es o (k div n + 1, k mod n + 1). Inicialmente cada elemento k forma un bloque por s mismo, ya que representa a una posicin del o laberinto inconexa de las dems. A medida que a vayamos derribando paredes, fusionaremos bloques, de tal modo que dos elementos estarn en el mismo a bloque si existe un camino entre las posiciones correspondientes. Adems emplearemos un TAD LABERINTO para a mantener registro del estado del laberinto (qu muros se han derribado, qu muros quedan en e e pie). Supondremos que el TAD ofrece las operaciones inicializa(L, m, n), derriba(L, i, j, dir) y hay pared derribable(L, i, j, dir), donde dir {N, E, S,W }, con los signicados obvios.
Ejemplos 423

Consideraremos la versin en la que se derriban o paredes hasta que toda posicin queda conectada a o todas las dems. La operacin num bloques(C) del a o TAD PARTICION devuelve el nmero de bloques en u el mfset C, y es fcil implementarla con un a pequeo overhead en union. n
Ejemplos 424

var L : laberinto;C : mfset; . . . fvar f [1] = 1; f [2] = 0; f [3] = +1; f [4] = 0 c[1] = 0; c[2] = +1; c[3] = 0; c[4] = 1 inicializa(L, m, n) N = m n; crea(C, N) mientras num bloques(C) > 1 hacer i = rand(m); j = rand(n) d = rand(4) r = (i 1) n + j s = (i 1 + f [d]) n + ( j + c[d]) si hay pared derribable(L, i, j, d) find(C, r) = find(C, s) entonces derriba(L, i, j, d) union(C, r, s) fsi fmientras

Ejemplos

425

Sea I(N) el nmero de iteraciones y D(N) el nmero u u de paredes derribadas en una ejecucin del o algoritmo. En cada iteracin se realizan 2 llamadas a o find y un cierto nmero de operaciones adicionales u de coste constante. Por otra parte, el nmero de u uniones es igual a D(N) y D(N) 2N m n, ya que 2N m n es el nmero de paredes derribables. u Pero hay que derribar al menos N 1 paredes para que toda posicin est conectada a cualquier otra, y o e recordemos que el algoritmo nunca derriba paredes innecesarias. Por lo tanto D(N) = N 1. El coste del algoritmo es ((I(N) + D(N)) (I(N), D(N)) + N) (I(N) + N) ya que D(N) I(N) y (, ) puede asimilarse a un factor constante. Aunque I(N) puede ser mucho mayor que N (intentamos derribar paredes no derribables, paredes ya derribadas o paredes que separan posiciones conexas), se puede demostrar que I(N) = O (N) en promedio y por lo tanto que el coste promedio total es (N). Los costes de inicializacin del laberinto y el mfset son (N). o

Ejemplos

426

También podría gustarte