Documente Academic
Documente Profesional
Documente Cultură
5
Tipos abstractos de datos
Contenido
5. Tipos de datos abstractos 2
5.1. Definición de TDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
5.1.1. Historia de la abstracción de los datos en programación . . . . . . . . . . . 3
5.1.2. Especificaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
5.1.3. Operaciones y operadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
5.2. Ejemplos de TDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
5.2.1. Ejemplo de TDA, los Naturales . . . . . . . . . . . . . . . . . . . . . . . . . 7
5.2.2. Segundo ejemplo, el Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.2.3. Especificación del TDA TVector . . . . . . . . . . . . . . . . . . . . . . . . 8
5.3. Implementación mediante Objetos C++ . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.4. Pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.5. Especificación del TDA pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.5.1. Especificación formal del TDA pila . . . . . . . . . . . . . . . . . . . . . . . 11
5.5.2. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
5.5.3. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
5.6. Formas de implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
5.6.1. Comparación de las complejidades de las implementaciones de pilas . . . . 14
5.7. Aplicaciones de las pilas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
5.7.1. Análisis de expresiones aritméticas . . . . . . . . . . . . . . . . . . . . . . . 15
5.7.2. Paso de parámetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
5.7.3. Eliminación de la recursividad con pilas . . . . . . . . . . . . . . . . . . . . 17
5.7.4. Comprobación de paréntesis . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
5.7.5. Pilas.Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.8. Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.9. Especificación del TDA cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.9.1. Especificación formal del TDA cola . . . . . . . . . . . . . . . . . . . . . . . 22
5.9.2. Constructores, Selectores, Iteradores . . . . . . . . . . . . . . . . . . . . . . 23
5.9.3. Excepciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.9.4. Formas de Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.9.5. Colas de prioridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5.9.6. Colas.Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.10. Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
5.10.1. Especificación formal del TDA lista . . . . . . . . . . . . . . . . . . . . . . 28
5.10.2. Interfaz e implementaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.10.3. Implementaciones acotada . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.10.4. Implementación no acotada . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.10.5. Listas.Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.11. Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.11.1. Iteradores sobre tablas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.11.2. Colisiones en hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.11.3. Propiedades de las funciones hash . . . . . . . . . . . . . . . . . . . . . . . 36
5 Tipos de datos abstractos 2
Los TDAs que nos van a interesar de ahora en adelante son aquellos que reflejen cierto
comportamiento organizando variedad de datos estructuradamente. A esta forma estructurada
de almacenar los datos será a la que nos refiramos para caracterizar cada TDA. Los TDAs que
tienen informaciones simples pero dependientes de un comportamiento estructural serán llamados
polilı́ticos por contra de aquellos TDAs simples, ya conocidos, como son los tipos predefinidos
simples, o TDAs monolı́ticos dónde la información no es relacionada mediante ninguna estructura
y no admiten más que un valor en cada momento. Un número entero o vale 10 o vale 5, pero no
puede contener simultáneamente ambos estados ya que para ello tendrı́a que disponer de una
estructura, definida de alguna manera, que relacionara ambos valores.
Un TDA tı́pico por su sencillez es la pila (stack ). El concepto de pila es el de un montón de
información (del mismo tipo) a la que se puede acceder sólo por el “sitio por dónde se introduce”,
o sea, que sólo se puede extraer el objeto que se acaba de añadir. Esta limitación, sin embargo, la
hace mucho más simple aún de ser implementada, y además no deja de hacerla sumamente útil en
muchı́simas situaciones. De hecho, después del array, la pila es la estructura que probablemente
más tiempo está funcionando en cualquier computador moderno3 . Nótese que para hablar del TDA
1 Con frecuencia diremos también con gran parte de la bibliografı́a en castellano, Tipos Abstractos de Datos en
vez de la más correcta Tipos de Datos Abstractos, ya que la abstracción se refiere a los datos no al ya abstracto
concepto de tipo, pero esto se convierte más en un juego de palabras, y, en cualquier caso, TDA es más fácil de
decir TAD que TDA. Usaremos ambas siglas indistintamente.
2 de ahora en adelante hablaremos sencillamente de cliente del TDA
3 recuérdese que las llamadas a las funciones están basadas en pilas dónde se almacenan los registros de estado.
5.1 Definición de TDA 3
pila no hemos hecho ninguna alusión a ningún tipo de elemento que tenga que estar apilado, sino
tan sólo a la forma en cómo disponer los elementos. Sólo nos interesa la estructura que soporta
la información. Los elementos a guardar dependerán de cada programa concreto en el último
momento.
¿Cómo se caracteriza este comportamiento estructural? Observando el tipo de acciones que
podemos aplicar sobre la pila. En este caso tenemos la posibilidad de amontonar y de sacar ele-
mentos, luego una pila basta que tenga las operaciones de apilar y desapilar para estar totalmente
controlada y llegar, mediante estas operaciones a ser posible construir cualquier instancia de la
misma.
Las operaciones apilar y desapilar que constituyen las mı́nimas necesarias para manipular
una pila (de lo que sea) constituyen la interfaz pública del TDA pila. Podremos apilar números,
letras, arrays, estructuras complejas e incluso otra pila, pero las operaciones serán las mismas. Es
por eso que la caracterización del TDA pila la dan los operadores, no los elementos que llamaremos
base, de tipo base o ı́tems que la integren.
Los TDAs tendrán una parte interna invisible al usuario. Esta parte oculta, innecesaria para
su uso, la constituyen tanto la maquinaria algorı́tmica que implemente la semántica de los opera-
dores, como los datos que sirvan de enlace entre los ı́tems del TDA, información interna necesaria
por la implementación que se esté haciendo para ese comportamiento del TDA. Ası́ tanto la im-
plementación de los operadores como los datos serán internos al TDAs, privados al acceso externo
y ocultos a cualquier otra parte cliente de la misma.
Esta división de aspectos no es nueva con la aparición de los TDAs y se ha demostrado muy
fructı́fera en muchos otros terrenos de la ingenierı́a, por ejemplo, la mecánica y los mecanismos de
conducción de automóviles, la independencia de la interfaz persona-ordenador de su implementa-
ción en el entorno orientado a gráficos, etcétera.
Los TDAs refuerzan el diseño y desarrollo de aplicaciones desde la perspectiva de “la infor-
mación y de su uso”. Por un lado, desde el punto de vista de la información pura, los TDAs están
relacionados con la diagramación tipo Entidad-Relación, mientras que desde el punto de vista del
uso, los TDAs permiten caracterı́sticas funcionales que admiten su exploración tanto en forma
top-down como bottom-up. Ambas perspectivas ofrecen una nueva forma de diseño más completa
que cada una por separado.
En este tema no desarrollaremos toda la teorı́a subyacente al diseño orientado a TDAs y los
procesos seguidos para ello, sino que nos guiaremos de algunos TDAs muy comunes para verlos en
acción en situaciones frecuentes en programación.
operaciones definidas por el lenguaje para los tipos básicos, las operaciones añadidas para los
tipos creados por el usuario no se heredan. Tampoco permite forzar esta “herencia”, definiendo el
antiguo operador que necesitamos para el nuevo tipo con el mismo nombre y que el compilador
elija el que debe usarse según el parámetro sea del tipo original o el derivado. No existe pues
la sobrecarga de operadores. Tampoco el polimorfismo, que es una propiedad que abarca a la
sobrecarga de operadores y que en esencia es la posibilidad de que un objeto cualquiera pueda ser
de distintos tipos, siendo el compilador el que seleccione según el contexto, los métodos a usar.
Cuando en un lenguaje como C, Pascal, o Modula-2 se define un tipo “nuevo” con la decla-
ración typedef o TYPE realmente no se está creando un nuevo tipo en el sentido estricto; y en
realidad el tipo nuevo es compatible (intercambiable) con su base:
TYPE LONGITUD= CARDINAL;
VAR l: LONGITUD;
i: CARDINAL;
...
l:= i; (* es válido *)
luego son de igual tipo (Ada es más coherente respecto a esto). En realidad lo que estos lenguajes
crean con los nuevos tipos son subtipos (denominados subtypes en Ada). El siguiente esquema
representa el árbol tipológico usual en los lenguajes procedurales tipo Pascal:
Bits
Bytes
Words
Atómicos Integers
@ No Punto Fijo
@ No Punto Flotante
@
R
@ Enumerados
Tipos base
H
J HH
j Compuestos * Arrays
J HHj Records
J
^ Punteros
J - Acceso a otros
d: DIAMES;
...
l:= a; (* SI es válido! *)
l:= v; (* NO es válido *)
dc:= d;(* SI es válido! *)
5.1.2. Especificaciones
Siempre debemos distinguir entre la implementación de un TDA y su especificación. Uno es
el qué y el otro el cómo debe hacerlo. Cuando nos preocupamos de la implementación estamos
hablando de una estructura de datos no de un TDA.
Una especificación debe ser formal. Por ejemplo, una especificación informal del tipo pila
podrı́a ser:
“Una pila es una colección de elementos o datos (ı́tems) de un mismo tipo puestos
de forma que el primero que se extraiga sea temporalmente el último que se añadió y
subsiguientes extracciones sean por orden los últimos añadidos. Se llama head, top,
cima o cabeza al elemento que acabo de añadir que es además el único que puede ser
extraı́do.”
Las posibles operaciones son:
cima devuelve el top
apilar añade una nueva cima
desapilar extrae la cima
crear crea un stack vacı́o
estáVacı́a devuelve pila = vacı́o
formalidad
Sin embargo, veamos lo que es una especificación formal. Existen dos formas posibles de especificar
formalmente un TDA
1. Axiomática expresión de la forma de las operaciones
2. Semántica expresión de la operatoria de las operaciones
Existen cuatro partes en la especificación formal de un TDA:
1. Nombre tanto del TDA como del Objeto ı́tem
2. Conjuntos de objetos y/o otros TDAs involucrados en las operaciones
3. Sintaxis método de uso de los objetos y nombre de las operaciones
4. Semántica axiomática y/u operativa
completitud
1. El conjunto de axiomas que definen un TDA debe ser completo en el sentido de definir el
resultado de todas las aplicaciones permisibles de las operaciones sobre el TDA.
2. completo en el sentido de definir operaciones que permitan construir todas las posibles ins-
tancias o situaciones del TDA
Se pueden poner expresiones compuestas
Se considera sintácticamente no válido
Desapilar(Desapilar(Desapilar(Apilar(Crear(),i))))
En orden de poder aplicar todas las posibles operaciones a todos los posibles resultados
conviene ampliar con
5.1 Definición de TDA 6
Axiomas:
a) Cima(E2) ::= E2
b) Desapilar(E1) ::= E1
pero esto puede añadir complicados y numerosos axiomas. Una mejor opción es usar la
aserción de invariantes:
aserción de invariantes: en el caso de aplicarse una operación a un valor de excepción
el resultado es el mismo valor de excepción
Para especificar los TDAs usaremos una notación formal ampliamente aceptada.
1. En primer lugar describiremos los nombres tanto del TDA (que coincidirá con la clase de
programación orientada a objetos) y del elemento que constituye la base del mismo.
2. Después describiremos todos los conjuntos de datos que van a intervenir en la definición
del TDA. Entre ellos estarán los conjuntos del TDA en definición, el conjunto de elemento
del tipo base, y conjuntos adecuados para trabajar con estos.
Operaciones
abstractas
Cambios Visualiza
de estado estado
Ver Cambiar
Limpiar Asignar EstáDefinido IsEmpty ítems
Crear Destruir ítems
EsIqual TamañoDe
Loop Traverse Loop Traverse
Over Change Change
Conjuntos:
N: conjunto de los objetos-números naturales
B: (TRUE, FALSE) (booleanos)
E: (FueraDeRango)
Especificaciones Sintácticas:
Cero: →N
EsCero: N →B
Pred: N →N ∪ E
Suce: N →N
Añade: N × N →N
Multip: N × N →N
Axiomas:
A1) EsCero(Cero) ::= TRUE
A2) EsCero(Suce( )) ::= FALSE
A3) Pred(Suce(x)) ::= x
A4) Pred(Cero) ::= FueraDeRango
A5) Añade(x,y) ::= si EsCero(y) entonces
x
sino
Suce(Añade(x, Pred(y)))
finsi
A6) Mult(x,y) ::= si EsCero(y) o EsCero(x) entonces
Cero
sino
Añade(Mult(x, Pred(y)), x)
finsi
Notar que los números naturales son: Cero, Suce(Cero), Suce( Suce( Cero )), etc.
hasta que reciben “nombres más cortos”: ‘0’, ‘1’, ‘2’, etc.
Conjuntos:
V: conjunto de los TVector
I: conjunto de items de tipo = base
X: conjunto finito. Por ej.: 1..k
E: Excepciones
5.2 Ejemplos de TDA 9
Especificaciones Sintácticas:
Crear: →V
Cambiar: V × X × I →V
Valor: T × X →I
Destruir: V→
Especificaciones Constructivas:
CreateVector(S
TVector v);
Pre ::= Ninguna
Post ::= v 0 ∈ V (X, I)
Cambiar(VAR
v : VECTOR; i: INDEX; x: TBase);
Pre ::= v ∈ V (X, I)
Post ::= Valor(v 0 , i) = x
Valor( v : VECTOR; i: INDEX; VAR x: TBase);
Pre ::= v ∈ V (X, I)
Post ::= v 0 = v; x0 = Valor(v, i)
Destruir(VAR
v : VECTOR);
Pre ::= v ∈ V (X, I)
Post ::= v 6∈ V (X, I)
Axiomas:
∀v ∈ V (X, I); i, j ∈ X; x ∈ I − {error}
A1) Valor(CreateVector(), i) ::= error
A2) Valor(Cambiar(v, i, x), j) ::= si i=j entonces
x
sino
Valor(v, j)
finsi
El problema primero, el de falta de genericidad es el de que cuando se declara una clase para
especificar un TDA es necesario concretar el tipo base sobre el que se está trabajando en el TDA
de manera que la clase pueda recibir parámetros de ese tipo, devolverlos etc. Sin embargo, en
realidad cuando especificamos formalmente un TDA no hacemos referencia alguna a la base o
tipo de los elementos sobre los que la estructura abstracta está. En realidad sı́ hay una referencia,
pero muy elemental. Los elementos de tipo base se supone que son copiables. Suponemos que los
elementos de tipo base que hemos de meter en una pila, por ejemplo, admitirán la operación de
copia, ası́ como suponemos que podemos devolver esos elementos copiados. En la mayorı́a de las
ocasiones ésta será la única exigencia que se le hará a los tipos base y esto es ası́ en todos los tipos
simples y en los objetos usuales, o al menos se podrá definir una operación de copia de objetos
cuando nos haga falta el usarlos como base de una estructura de un TDA.
Aunque sólo exijamos la mayorı́a de las veces tan sólo la copiabilidad del tipo base del TDA,
sin embargo, la programación orientada a objetos nos obliga a especificar el tipo del parámetro de
manera que habrı́a que tener una clase para definir pilas de caracteres, otra clase, con otro nombre,
para pilas de enteros, otra para. . . lo que es bastante incómodo por no decir inútil o imposible.
En principio, en las especificaciones formales en pseudocódigo o sencillamente en sus especifi-
caciones algebraicas formales, nos referiremos al tipo base de una manera general y no hará falta
concretar si el tipo base es un carácter o un número real o una estructura. Dejaremos este pro-
blema de la concreción de la base para ser resuelto en el lenguaje de programación concreto. En
particular el lenguaje de programación C++ aporta un potente mecanismo pseudosintáctico que
permite la construcción de unidades (funciones y clases) genéricas en las que serı́a posible dejar sin
especificar aún algún tipo, etc. y postergando la concreción del tipo con el que deberá trabajar la
clase hasta el último momento en el que se use. Tenemos, pues un mecanismo bastante potente con
el que aproximarnos a esta deseada genericidad. Por otro lado, la construcción sin estas plantillas
de las clases correspondientes a los TDAs más importantes es tarea suficientemente interesante y
compleja como para poder, en principio dejar de usar estas plantillas que permiten la genericidad
en aras de una mayor sencillez del código y menor distracción sintáctica con esta complicación
añadida. Más tarde, se indicará cómo emplear estas plantillas.
La segunda gran dificultad que no resuelven la programación orientada a objetos es la inde-
pendencia total de la declaración de la clase (y, por ende, del TDA) de la implementación. Y es que
aunque eliminemos la construcción de los métodos online y posterguemos totalmente el contenido
de los métodos al fichero correspondiente de implementación del TDA, queda la parte privada.
Inevitablemente, si una clase tiene atributos, estos atributos deben aparecer en la declaración.
Lenguajes como C++ no permiten el que los atributos se puedan postergar a la implementación
del TDA. Esto expone innecesariamente una parte pretendidamente privada en la cabecera de una
clase-TDA. Aunque el cliente del TDA no pueda hacer uso de los atributos privados del TDA, sin
embargo la más leve modificación en la forma de implementar el TDA implicará la modificación
4 uno de los padres de Smalltalk y desarrollador del también, más moderno, lenguaje orientado a objetos, Eiffel
5.4 Pilas 11
de estos atributos por parte del programador del TDA, que ası́ tendrá que tocar en el fichero de
cabecera de declaración de la clase-TDA con lo que todo el sistema que depende de él tendrá que
ser recompilado aún sin necesidad ya que ninguno de los clientes del TDA, como hemos dicho, veı́a
o hacı́a uso de la parte privada. Tan sólo habrı́a sido necesario recompilar, al menos en teorı́a, la
parte de la implementación del TDA, pero no todos los clientes de la clase.
Por ejemplo, si decidimos que el TDA pila debe ser implementado mediante un array, este
array deberı́a estar declarado en la parte privada de la clase pila. Si en otra ocasión, vemos
más adecuada la implementación la pila mediante una lista de nodos encadenados, la declaración
privada será diferente y todos los clientes (que no tendrı́an en principio que saber nada de estos
mecanismos internos del TDA pila) se tendrán que compilar de nuevo.
La única solución práctica que vemos a esta limitación de la programación orientada a objetos
es la utilización, al estilo Modula-2 de tipos opacos. Este tipo es un sencillo puntero sin tipo destino
(una especie de void *), que se resolverı́a dentro de la implementación de la clase pero ya sin
tocar más la declaración aunque se cambiase de array a lista o a cualquier otro el mecanismo de
implementación del TDA.
Este método de implementar los TDAs asemejarı́a mucho a los lenguajes orientados a objetos
al mecanismo de ocultación de Modula-2. Sólo que la programación orientada a objetos añade, en
el caso de C++ la posibilidad de plantillas (cosa innecesaria, por otro lado en el lenguaje Ada).
5.4. Pilas
Una estructura que aparece frecuentemente en programación es la Pila (Stack ). En lenguajes
de alto nivel, es muy importante para la la eliminación de la recursividad, análisis de expresiones,
etc. En lenguajes de bajo nivel es indispensable y actúa constantemente en todos los lenguajes
compilados y en todo el sistema operativo.
La caracterı́stica más importante de las pilas es su forma de acceso. En los arrays y en las
listas, el acceso es directo: se selecciona el ı́tem de la secuencia mediante algún parámetro. La
pila simplifica el acceso a su información y tan sólo son imprescindibles dos procedimientos para
trabajar con una pila: añadido y extracción. No se necesitan parámetros para ninguno de los
procedimientos, en general. Es pues un tipo muy simple de uso e implementación, como veremos.
Una pila es una estructura de datos ordenados según el orden de inserción y de los que
sólo es posible acceder al último insertado. Este tipo de control del acceso se denomina
LIFO: último en entrar primero en salir (last in first out).
Una pila es pues un conjunto totalmente ordenado, en el que se insertan y eliminan elementos pero
sólo accesibles por el elemento CIMA (o Top) que fue el último en ser insertado.
Conjuntos:
P: conjunto de las pilas
I: conjunto de items de tipo TBase (copiables)
B: {FALSO, CIERTO}
E: PilaVacı́a
5.5 Especificación del TDA pila 12
Especificaciones Sintácticas:
Crear: →P
Destruir: P→
Apilar: P × I →P ∪ E
Desapilar: P → (P × I) ∪ E
Cima: P →I ∪ E
EstáVacı́a: P →B
Especificaciones Constructivas:
TPila p()
Pre ::= Ninguna
Post ::= EstáVacı́a(p0 )
~TPila p()
Pre ::= existe p
Post ::= p no existe
p.Apilar(TBase y)
Pre ::= existe p
Post ::= ¬EstáVacı́a(p0 ) ∧ Cima(p0 ) = y
TBase p.Desapilar()
Pre ::= ¬ EstáVacı́a(p)
Post ::= p0 6= p
Ret ::= Cima(p)
TBase p.Cima()
Pre ::= ¬ EstáVacı́a(p)
Post ::= p0 = p
Ret ::= Cima(p)
Bool p.EstáVacı́a()
Pre ::= existe p
Post ::= p = p0
Ret ::= desde Crear(p), No de Apilar == No Desapilar
Axiomas:
A1) Cima(Apilar(s,i)) ::= i
A2) IsEmpty(Crear()) ::= TRUE
A3) IsEmpty(Apilar(s,i)) ::= FALSE
A4) Desapilar(Apilar(s,i)) ::= s
A5) Desapilar(Crear()) ::= E1
A6) Cima(Crear()) ::= E2
En la sintaxis sólo se han considerado los excepciones que producirı́an cualquier forma de imple-
mentación sin considerar la verificación de las precondiciones. Para considerar todas las posibles
excepciones es necesario concretar la forma de implementación. La sintaxis es variable en cuanto
a la forma en que se devuelven los objetos de tipo TBase; a veces se empleará el modo “return”
y otras, como en la expuesta, en modo parámetro. En la descripción semántica formal de las pre
y las postcondiciones no se ha incluido el método de control de estas excepciones; aunque esta
sintaxis podrı́a corresponder a control de excepciones por “variable global”.
También es conveniente el el constructor de formas de implementación acotadas el indicar
(mediante un parámetro con algún valor por defecto) el tamaño total de la estructura estática.
5.6 Formas de implementación 13
5.5.2. Procedimientos
La pila crece con Apilar y decrece con Desapilar, los demás son selectores y modificadores
globales, no siempre utilizados.
Iteradores No existen
En nuestra implementación se ha reunido Cima con Desapilar, esto es, se ha hecho que al eliminar
la cima, Desapilar, se devuelva el elemento Cima. Esto no siempre se hace ası́, la ventaja es práctica,
según las aplicaciones.
5.5.3. Excepciones
La única excepción que puede aparecer en un TDA pila es la de tratar de leer (desapilar) algo
de una pila vacı́a. Otras situaciones de excepción no dependen de las especificaciones formales,
sino de la forma de implementación y deberı́an estar reflejadas en cada caso. En este segundo caso
la excepción no aparece por violación de precondiciones sino, en general, por alguna limitación de
la forma de implementación.
El control de excepciones es un problema muy difı́cil, por no decir, imposible, en general, en el
desarrollo de software, sin embargo cada vez son más los lenguajes que se apoyan en el mecanismo
de aptrapar excepciones ideado en Ada, en el que se forman ámbitos en los que cualquier excepción
lanzada en cualquier parte interna del mismo se caza mediante un mecanismo de selección del tipo
de excepción, etcétera.
No nos preocuparemos más que de destacar el tipo de excepciones que se pueden dar en cada
caso, pero no haremos más hincapié en este tema.
En la práctioca se pueden dar las excepciones:
1. Por violación de las precondiciones. Esto es, el usuario del TDA comete algún error de uso
que detectará el propio TDA. Estas excepciones son debidas a defectos del programa usuario.
Son:
SINDATOS Se trata de extraer un elemento de una pila vacı́a. Este es un error del usuario del
TDA.
EXCEDIDA tı́pico de las implementaciones acotadas de los TDAs aunque se puede dar alguna
vez en las no acotadas.
2. Por defecto de la implementación se podrı́an dar errores como el de ı́ndice fuera de rango,
etcétera.
un flag condición de error global es mediante una variable definida en la interfaz de declaración
del TDA.
Según la extensibilidad espacial, existen dos formas de implementación, la acotada y la no
acotada.
La implementación acotada utiliza un array (que tiene un tamaño fijo, aunque en C++ este
tamaño se puede concretar fácilmente en el momento de la “construcción” del objeto, en ese caso
hay que guardar ese tamaño en un atributo nuevo, para no sobrepasar los lı́mites del array), y
tiene también un ı́ndice entero que es el punto de lectura escritura en el array. Ver ejercicio 1.
pero, en ninguno de los dos casos el tiempo de acceso depende del aumento del tamaño de la
pila. En la instrucción delete de la implementación No acotada sin embargo topamos con una
tı́pica “caja negra” muy dependiente del sistema compilador-y/o-sistema operativo, pero siempre
aportando un mayor número de instrucciones internas, eso sı́, independientes, de nuevo del tamaño
de nuestra pila.
En cuanto a las complejidades espaciales, la complejidad de la implementación No acotada es
lineal (O(N )), mientras que la no acotada es constante (C). Lo que ocurre es que esa constante
C es un valor mucho más alto que el coeficiente de N de la implementación No acotada. Dicho
de otra forma la implentación acotada requiere un espacio fijo, mucho mayor que el pequeño y
variable espacio requerido por los elementos que se van añadiendo a la implementación dinámica,
que además es utilizable en más diversas aplicaciones, por la adaptabilidad de su tamaño, etc.
Veremos que la implementación acotada es la más utilizada a bajo nivel, mientras la No
acotada se emplea más en lenguajes a alto nivel.
la anterior pero que se implementa a alto nivel que es la eliminación de la recursividad por medio
de pilas.
Figura 2: Las expresiones infijas recurren a los paréntesis para indicar el orden en que
se desea se hagan las operaciones.
Efectivamente todos hemos aprendido matemáticas utilizando una sintaxis llamada infija, por
situar los operadores entre los operandos. Todos sabemos como calcular 3+2×5. En las operaciones
infijas es necesario primero ver la expresión entera ya que tenemos que tener el cuenta la prioridad
de los operadores. En la expresión anterior entendemos que debe primero multiplicar 2 por 5 y, al
resultado, sumarle 3. En esta expresión no hace falta utilizar paréntesis porque el orden buscado
de las operaciones es el mismo que el de la prioridad implı́cita de los operadores. Sin embargo,
en la expresión 3 × (2 + 5) los paréntesis son inevitables, ya que debemos superar la prioridad
del signo × realizando antes el +. Ası́ pues las expresiones en formato infijo no se pueden evaluar
secuencialmente y requieren el uso de paréntesis.
Además de la notación infija existen otras dos, la prefija y la postfija. La prefija sitúa los
operadores antes que los operandos, por ejemplo + 3 × 2 5, mientras la postfija, lo hace al revés:5
25×3+.
Volviendo a nuestras expresiones en forma infija, vemos que el segundo ejemplo con paréntesis
se puede escribir en forma postfija como 2 5 + 3× (aunque también como: 3 2 5 + ×).
¿Cómo se leen estas expresiones postfijas?
Para evaluar una expresión postfija se empiezan tomando los operandos de izquierda a derecha
(2, y 5); cuando se topa con un operador (+), se hace actuar sobre los operandos leı́dos y el
resultado se toma como la expresión semievaluada en curso (7); ası́, se sigue hacia la derecha
siempre tomando ahora el siguiente operador/operando, etc. En nuestro ejemplo el operando 3 y,
después el operador × que por tanto actúa sobre los operando guardados hasta ahora, 7 y 3. El
resultado que nos pedı́an es 21.
Dos cosas fundamentales:
en los que los nodos operadores tienen descendientes u operadores u operandos, y tenemos la representación lineal
prefija, infija o postfija, según recorramos esos árboles en forma “preorden”, “enorden” o “Posorden”, pero dejaremos
esto para el tema de árboles.
5.7 Aplicaciones de las pilas 16
(normalmente) dos operandos, éstos se extraen de la pila; posteriormente el resultado del operador
sobre los operandos se apila.
Esta eficiente forma de evaluación fue descubierta por el matemático polaco Lukasiewicz y
hoy dı́a tiene muchos adeptos (existen incluso calculadoras de bolsillo que la emplean pese a que
no es la más familiar). Muchos lenguajes de programación (FORTH, PostScript, . . . ) basan su
estructura de valuación directamente en la sintaxis postfija, de manera que en ellos, para calcular
el seno de 30, escribimos “30 sin”, etc. Además, los compiladores de todos los lenguajes dejan un
código máquina tipo postfijo.
A las expresiones aritméticas en forma postfijas las llamaremos expresiones “polacas”
Definición Una expresión polaca es una secuencia de operandos numéricos x, y, . . . (números)
(N ) y de operadores (por ejemplo binarios +, −, ×, /, Pow; que representaremos +, -, *, /, ^;
√
unarios: , sin, etc, que podemos representar q, s, etc.) en general n con n siendo normalmente
1, 2 ó 3 representando la aridad: número de operandos que requiere el operador. Estos operandos
y operadores para formar una expresión postfija o polaca habrán de estar ordenados de la siguiente
forma:
1. x ∈ N es (ya) una expresión polaca
2. si pi son 1 o más expresiones polacas entonces también lo es p1 . . . pn n
Ejemplos de expresiones polacas:
1 32
2 17 40 *
3 35 17 40 * +
4 35 17 - 40 * 9 5 4 - + *
Algoritmo de evaluación Para evaluar una expresión s polaca bien formada se usan las dos
reglas siguientes que se aplican hasta que sólo queda un número en la expresión. El rastreo se hace
desde la izquierda, cada lectura se hace desde el punto en que se quedó en la última:
1. Analizar la expresión hacia la derecha hasta encontrar el primer operador si =
2. Aplicar los operadores a los operandos xi−2 y xi−1 , inmediantamente a su izquierda, o sea,
x1−2 xi−1 , (suponiendo un operador binario, si no, si fuera n-ario, a los n a su izquierda),
obteniendo de esta operación el resultado r, y reemplazar la secuencia xi−n . . . xi−1 de la
expresión por r
Uso de pilas en las expresiones polacas si nos servimos de una estructura de almacenamiento
temporal de tipo pila, podemos ir calculando los resultados intermedios y almacenándolos en la
pila conforme leemos le expresión polaca. Al tomar los operandos de las expresiones polacas vamos
“hacia atrás” tomando los operandos en el sentido contrario al de su lectura. Ası́, si conforme leemos
la expresión polaca hacemos el paso primero pero guardando los operandos en una pila, cuando
topemos con un operador, tan sólo tendremos que desapilar los últimos operandos necesarios para
el operador. Se trata pues de analizar la expresión hacia la derecha y con cada operando: apilar el
operando, con cada operador : desapilar tantos operandos como necesite éste operador y apilar en
su lugar el resultado de operarlos.
Como ejemplo, evaluar 1 2 5 + - 2 *:
125+−2∗ queda en la pila: 1
1
125+−2∗ queda en la pila: 2
1
2
125+−2∗ queda en la pila: 5
5.7 Aplicaciones de las pilas 17
1
125+−2∗ queda en la pila: 7
125+−2∗ queda en la pila: −6
−6
125+−2∗ queda en la pila: 2
125+−2∗ queda en la pila: −12
Si tenemos a la expresión polaca original escrita en una cadena de caracteres en la que cada
operando ocupa un carácter: podrı́amos evaluar esta expresión con:
1 TBase EvaluaPolish(char *s);
2 {
3 TPila p;
1. Al principio del procedimiento (o función) se inserta código que declare un PILA (llamado
pila de recursión) y lo inicialize a vacı́o. La mayorı́a de las veces el mismo PILA podrá ser
usado para guradar parámetros, variables locales y una dirección de vuelta para cada llamada
recursiva, pero pueden usarse PILAs independientes.
2. Se etiqueta 1 a la primera sentencia ejecutable.
3. Si el procedimiento es una función, entonces, todas las apariciones de return convertirlas en
los pasos 9, 10 y 11 y en una asignación del valor a devolver a una variable z del mismo tipo
que la función
Con cada llamada recursiva, hacer los siguiente:
4. Guardar los valores de todos los parámetros por copia (sin &) en la pila. El Cima de pila es
global para todo el algoritmo.
5. Crear una etiqueta secuencialmente conforme nos encontramos con llamadas recursivas, sea
la etiqueta la i-sima. Guardar i en la pila. El valor guardado en la pila será usado como
dirección de vuelta.
6. Evaluar los argumentos correspondientes (sin &) y asignar los resultados a los parámetros
formales inicialmente recibidos.
7. Insertar un salto incondicional (goto) al comienzo del procedimiento (ya etiquetado)
8. Si estamos tratando con un procedimiento, añadir la etiqueta creada en 5 a la instrucción
inmediatamente siguiente al salto incondicional. Si esta sentencia ya tuviese una etiqueta,
cambiarla y todas sus referencias por la calculada en 5. Si se trata de una función, continuar
el salto incondicional con el código en que se asigna z en vez del supuesto valor devuelto por
la función. Etiquetar esa sentencia con la etiqueta calculada en 5
Con esto ya hemos eliminado todas las llamadas recursivas. Necesitamos ahora preceder la salida
final con:
9. Si la pila de recursión está vacı́a, usar el valor de z como valor de return, si se trata de una
función, si no, hacer return.
10. Si la pila no está vacı́a, restaurar el valor de todos los parámetros por valor y de todas las
variables locales que no sean parámetros por referencia. Estos valores están en la cima de la
pila. Usar el valor de vuelta del tope de la pila y ejecutar un salto a esa etiqueta. Esto puede
hacerse usando una instrucción select (switch).
11. Si existiese una etique al final del código se mueve a la primera lı́nea del código para 9 y 10
Se deja como ejercicio el comprobar este algoritmo para el algortimo recursivo del cálculo del
factorial de un número y de el n-simo número de Fibonacci.
(x × (y + z × (u − v)))/(y − z)
Pero la expresión:
da también resultado neto 0 y no está bien parentizada. En este caso bastarı́a con considerar que
el contador no debe hacerse nunca negativo. Sin embargo la expresión:
(x × {y + z × (u} − v)))/(y − z)
está mal agrupada porque se ha abierto ha cerrado un subgrupo parentizao con {} y sin haber
cerrado un subgrupo interior a él, se ha cerrado el exterior.
Para comprobar este tipo de cuestiones no hay nada mejor que una pila. Si apilamos los
paréntesis de apertura ‘(’, ‘[’, ‘{’, ‘<’, ‘¡’, ‘¿’, “’, ‘“’, etcétera y cuando nos encontremos uno de
cierre vemos que en la cima de la pila está su correspondiente pareja, es que todo va bien. Al final
la pila debe quedar vacı́a.
Se deja al estudiante la comprobación de esto construyendo una rutina de utilidad que ins-
tanciarı́a una pila de caracteres y recibiendo una cadena de caracteres con paréntesis, devolverı́a
cierto o falso según estuviese bien o mal parentizada.
5.7.5. Pilas.Ejercicios
. 1 Implementar el TDA pila mediante una clase que contenga los atributos: un array (un puntero
al mismo; el tamaño por definir) y el ı́ndice entero dónde está el último, el tamaño total del
array según se pasa al constructor en la creación del objeto, 100 por defecto.
. 2 Implementar el TDA pila mediante una clase que contenga como atributo una lista de nodos
dinámicamente enlazados. Para encolar, añadir por el principio, para desencolar, borrar el
primero. Ver las Figuras 3, 4 y 5.
1 2
nuevo nuevo
p p
. 3 Utilizando la clase pila, diseña un algoritmo que determine si una cadena de caracteres de
entrada es de la forma
xx̂
dónde x es una cadena que consiste en caracteres arbitrarios y x̂ es lexicográficamente la
inversa de x. Por ejemplo, si x = αβγδ, entonces x̂ = δγβα.
5.7 Aplicaciones de las pilas 20
temp 1 temp 2
p p
. 4 Hemos visto como se puede evaluar una expresión polaca de operandos simples. Desarrollar
un algoritmo que construya dos pilas, una con los operandos (la ya conocida) y otra con los
operadores. La interfaz serı́a:
void CompilarPolaca(const char s[], PilaChar& prandos, PilaChar& pdores);
Nótese que la pila de operandos es de caracteres de manera que puede contener sı́mbolos
(a, b, c . . . ) y no números. De esta forma:
int EvalCPolaca(int valores[], const PilaChar& prandos, const PilaChar& pdores)
puede recibir en su primer parámetro los valores actuales de las variables simbolizadas por
a, b, . . . .
Cuando se encuentre un sı́mbolo x = b en la ‘prandos’ su valor actual será valores[x-’a’];
. 5 Analizar el siguiente código que convierte una expresión infija en posfija:
1 void infijaAposfija (char infija[], char posfija[])
2 {
3 int iinfija=0, iposfija=0;
4 PilaNoAc pila;
35 }
36 while (!pila.EstaVacia())
37 posfija[iposfija++]=pila.Desapilar();
38 posfija[iposfija]=’\0’;
39 }
5.8. Colas
Las colas, al igual que las pilas, aparecen espontáneamente de la solución de muchos problemas
informáticos. Aunque en general no tanto con problemas algorı́tmicos como con problemas de tipo
“productor-consumidor”, esto es una parte del sistema produce algo que otra consume a un ritmo
diferente, normalmente más lento.
productor consumidor
Figura 6: El productor produce a un ritmo diferente del ritmo al que consume el con-
sumidor
Una cola o queue es un almacén Q = (a1 , . . . , an ), ordenado según se llega, y dónde los
elementos salen “por un lado” (Top, Frente, Primero o Cabeza) mientras que se añaden “por el
otro” (Bottom, Rear, Último o Final ).
Es un almacén de datos de tipo FIFO (First In First Out): el primer elemento que entra es
el primero en salir.
Se respeta el orden de llegada.
an -Cima
an−1
..
.
a2
a1 Prime a1 a2 . . . an−1 an Ulti
Pila Cola
La cola es una forma de almacenar los datos muy común cuando el consumidor de los mismos no
los puede atender tan rápido como los prepara el productor. Es muy útil en sistemas operativos
multitarea, donde una tarea produce información a un ritmo a veces mayor que la tarea que la
absorbe. En estos casos, se almacenan los elementos conforme llegan de los productores y los
5.9 Especificación del TDA cola 22
retiran los consumidores, de una cola; de esta forma es atendido cada proceso según el orden de
llegada. Esto se puede hacer ignorando las prioridades de cada proceso o, si se tienen en cuenta,
creando una cola por cada grado de prioridad con tipos de cola sencilla.
Mientras que las pilas se ven más frecuentemente asociadas a la ejecución de algunos algorit-
mos, las colas se usan como almacenes de datos.
Al igual que la pila, una cola es un conjunto totalmente ordenado en el tiempo, en el que
se añaden y eliminan elementos. Ahora los elementos son accesibles por dos puntos, el Primero
o Frente de dónde se borran y extraen los elementos y el Último o Final por dónde se añaden.
Igual que con las pilas, se trata de una ordenación temporal, en cuanto al orden de la inserción:
un elemento está antes si se añadió antes. A ver qué
Conjuntos:
Q: conjunto de las colas
I: conjunto de items de tipo = base
N: conjunto de los números naturales {0, 1, . . . }
E: Excepciones (colaVacı́a)
Especificaciones Sintácticas:
Crear: →Q
Destruir: Q→
Encolar: Q × I →Q
Desencolar: Q → (Q × I) ∪ E
Primero: Q→I ∪ E
Ultimo: Q→I ∪ E
NElementos: Q→N
Especificaciones Constructivas:
TCola q();
Pre ::= Ninguna
Post ::= NElementos(q 0 ) = 0 ∨ q 0 = ( )
~TCola q();
Pre ::= q ∈ Q
Post ::= q 6∈ Q
q.Encolar(x TBase);
Pre ::= q ∈ Q
Post ::= an = x ∧ NElementos(q 0 ) = NElementos(q) + 1
TBase q.Desencolar();
Pre ::= qn ∈ Q ∧ q.NElementos() > 0
Post ::= q 0 6= q ∧ NElementos(q 0 ) = NElementos(q) − 1
Ret ::= a1 / q0 .Encolar(a1 )
TBase q.Primero();
Pre ::= q ∈ Q ∧ q.NElementos() > 0
Post ::= q 0 = q
Ret ::= a1 / q0 .Encolar(a1 )
5.9 Especificación del TDA cola 23
TBase q.Ultimo();
Pre ::= q ∈ Q ∧ q.NElementos() > 0
Post ::= q 0 = q
Ret ::= an / qn−1 .Encolar(an )
N q.NElementos();
Pre ::= q ∈ Q
Post ::= q = q 0
Ret ::= n
Iteradores: no definidos
5.9.3. Excepciones
En la especificación abstracta del TAD cola sólo existe la posible excepción genérica colaVacı́a,
que se puede dar con:
Desencolar
colaVacı́a : Primero
Ultimo
Acotada mediante array Tenemos una implementación acotada del tipo d[N] y necesitamos
dos variables prim y ulti para acceder a los dos extremos. Convendremos en que ult indica
donde está actualmente el último que ha entrado (q.Ultimo()) y prim donde está q.Primero()
el primero que entró y el primero que va a salir. Para leer esos extremos bastará indicar el ı́ndice
correspondiente. Para desencolar se leerá en prim y se aumentará prim para preparar el acceso al
antes era el segundo. Para encolar se aumentará ulti para posicionarnos en un sitio vacante aún
no usado y se escribirá allı́ el nuevo. En otras palabras, tendrı́amos:
1 ultimo == d[ulti]
2 primero == d[prim]
3 encolar -> d[++ulti] = x
4 desencolar -> d[prim++]
5.9 Especificación del TDA cola 24
prim ulti d[0] d[1] d[2] d[3] d[4] d[5] d[6] Paso
0 -1 Crear
0 0 a0 Encolar()
0 1 a0 a1 Encolar()
0 2 a0 a1 a2 Encolar()
1 2 a1 a2 Desencolar()
1 3 a1 a2 a3 Encolar()
2 3 a2 a3 Desencolar()
y además, aunque halla pocos datos estos van moviéndose como una mancha hacia lo alto del array.
Naturalmente, si no le ponemos remedio, aunque haya uno o pocos elementos, el ı́ndice dónde hay
que escribir el siguiente último, sobrepasará N. Nótese la inicialización a 0 y −1: cuando creamos
la pila ponemos ult=-1 con lo cual el pre incremento antes de escribir colocará al nuevo encolado
en el ı́ndice 0 del array6 .
Pero esta implementación va dejando espacio inutilizado desde la cima que se va eliminando y
tiene un periodo de utilizabilidad muy pequeño: aunque vayamos extrayendo elementos, los datos
van corriendo hacia el final del array hasta tropezar con su lı́mite quedando seguramente el primer
segmento del array aún vacı́o. Ası́ pues esta implementación NO nos interesa. Veamos la forma de
aprovechar siempre todo el array para los datos.
Para hacer esto definiremos un array circular, esto es, el siguiente del final es el primero.
Manteniendo la misma definición del tipo, hemos de preocuparnos en la implementación de los
procedimientos de que cuando vayamos a insertar por encima del final fı́sico del array, insertemos,
si hay sitio, en su comienzo fı́sico, reciclando ası́ esas posiciones vacı́as.
La solución está, en esta forma de implementar las colas con arrays, en utilizar un indexado
del array circular. De manera que si al incrementar el ı́ndice sobrepasamos el máximo, volvemos
el ı́ndice a cero. Ası́, en vez de sencillamente:
++ulti
(¡aritmética modular!), tanto para ulti como para prim. Ver Figura 7.
PRIMERO
PRIMERO ÚLTIMO
ÚLTIMO
El siguiente problema es cómo saber si la cola está totalmente llena o totalmente vacı́a.
¿Cuántos elementos hay en la cola? La primera respuesta serı́a: ult-prim+1, pero esto sólo serı́a
válido en un estado inicial (antes de empezar a girar la cola) antes de dar la vuelta ult. Ası́ pues
habrı́a que considerar (ver Fig. 8):
6 Sin embargo ponemos 0 en el ı́ndice del primer elemento aún cuando no hay ninguno, esto es conveniente para
ulti–prim+1 ulti-prim+1
ulti prim
ulti+1 N–1–prim+1 N–prim+ulti+1
ulti prim
recién llenada
recién vaciada
ulti prim
?
O sea que justo cuando extraigamos el último elemento (supiendo el ı́ndice prim hasta ponerse por
delante de ulti) estaremos en la misma situación que cuando añadamos completando la capacidad
un elemento, en cuyo caso será ulti el que se pondrá justo detrás de prim. Por lo tanto habrá un
caso en el que no sabremos si el array está lleno del todo o vacı́o totalmente.
Para evitar este estado confuso podrı́amos hacer una de estas dos cosas:
1. Dejar una celda vacı́a antes de llegar a prim. Cuando la cola esté vacı́a, la distancia de ulti
a prim será de uno, pero cuando al la distancia sea de dos, estará llena.
2. Mantener un atributo extra (nelem) que nos guarde cuántos elementos hay tras cada enco-
lado/desencolado de la cola sin recurrir a cálculos entre ulti y prim.
Cuando se estuviese usando la cola, el control del error por sobrepasar la capacidad de la cola
habrı́a que hacerlo antes de llamar al método de encolado, para no caer en un desbordamiento de
la capacidad.
Esto lleva a que en las implementaciones acotadas parezca necesario el conocer a priori
antes de encolar si vamos a poder meter el elemento o a posteriori si el proceso se pudo
hacer; lo que suele resolverse añadiendo un método bool EstáLlena(); sin embargo
no lo creemos aconsejable ya que esto crea una dependencia del uso respecto de la
implementación muy poco en la filosofı́a de los TADs. La solución vuelve a estar,
como tantas otras veces, en una adecuada elección de la implementación (acotada y
con qué lı́mites o no acotada) antes de elegirla.
El saber si una cola está vacı́a habrı́a que considerlo antes del desencolado (si queremos controlar
este posible error) como de Primero() y de Ultimo() y se debe controlar siempre mediante la
respuesta del método NElementos().
Se deja al alumno la realización de esta forma de implementación acotada. Ver el ejercicio 7.
hacer una lista de nodos circular y considerar el nodo último aquél al que apuntamos
con la referencia exterior. Los enlaces se pondrán además al revés que hasta ahora:
“mirando hacia atrás”.
5.9 Especificación del TDA cola 26
q ulti prim
Figura 9: Truco para enlazar los nodos permitiendo un encolado y desencolado directos.
ulti (1)
(3) (2)
q prim
Figura 10: Encolado: (1) el nuevo nodo apunta al siguiente del q; (2) el q apunta al
nuevo y actualizamos el q.
La Figura 9 muestra el aspecto de los enlaces, mientras que las figuras 10 y 11 muestran las
operaciones de encolado (tres pasos) y desencolado (un paso).
Hay que tener cuidado, sin embargo, como es usual con los casos extremos de extraer el último
que queda y en el encolado del primer nodo. Ver ejercicio 6.
Sin embargo parece más fácil mantener un parámetro adicional para indicar la prioridad con
la que entra el elemento (independiente ası́ su contenido de la prioridad)
void Encolar(const TBase x, const int p);
(1)
q prim
ulti
En este caso hay que cuidar la devolución en TBase Desencolar(int &p) dónde serı́a necesario
obtener no sólo el valor encolado sino también su prioridad, por ejemplo, como una parámetro por
referencia. No siempre es necesario recuperar la prioridad, que parece más un medio de acomoda-
miento de la información, pero a nivel estructural, si no se recupera la prioridad, no podrı́amos,
por ejemplo, copiar una cola de prioridad, duplicarla.
Las colas de prioridad son almacenes, por lo que parece, en general más conveniente la expo-
sición del selector NElementos() que la del EstaVacia(), más propia en el caso de la pila.
En la implementación de colas de prioridad hay que cuidar (en el segundo caso de paso
explı́cito del parámetro prioridad) mantener el campo prioridad en cada nodo o celdilla junto a la
información TBase neta.
Si se implementa en forma no acotada, se puede mantener la misma estructura de lista de
nodos simples que se usó en las colas normales, pero ahora hay que apartar dos casos (además
del de está vacı́a que también es un caso aparte), el del caso en el que el elemento entre después
del último, como en una cola normal, por llevar menor o igual prioridad que el último de la cola
(p <= datos->p) y el caso de que el elemento entre antes que el primero por tener más prioridad que
el primero (p > datos->sigui->p). En el caso que queda, si no se ha dado ninguno de los anteriores,
se deberá recorrer la cola desde el primero hacia al último hasta que se encuentre un elemento de
menor prioridad que la que traemos.
Se deja como ejercicio para el estudiante la implementación de las colas de prioridad mediante
una lista de nodos dinámicamente enlazados (8).
Otra técnica de implementación, rápida por lo sencilla, es la de mantener un array de colas
normales, de manera que en cada celda del array el ı́ndice del array indica la prioridad de la
correspondiente cola. Ver el ejercicio 9.
5.9.6. Colas.Ejercicios
. 6 Implementar el TDA cola en forma no acotada como se indica en la Figura 12. ¿Qué ocurrirı́a
último primero
produce consume
Figura 12: Implementación del TDA cola en forma no acotada mediante una lista
dinámica de nodos en la que cada nuevo nodo apunta “hacia atrás”. En
esta representación, el último que entra es apuntado por el anterior último
y él mismo apunta al primero a salir.
5.10. Listas
Definición Una lista es, o el vacı́o (notado por ‘()’), o una sucesión finita de elementos del
mismo tipo, notada por (a1 , a2 , . . . , an ) de la que se pueden tanto leer, como borrar o insertar los
elementos indicando su posición. Al número de elementos de la lista n lo llamaremos “longitud de
la lista”.
Conjuntos:
L: conjunto de las listas
I: conjunto de items de tipo = base
N: números naturales
E: Excepciones (Fuera de rango)
Especificaciones Sintácticas:
Crear: →L
Destruir: L→
Longitud: L→N
Elemento: L × N →I ∪ E
Reescribir: L × N × I →L ∪ E
Insertar: L × N × I →L ∪ E
Borrar: L × N →L ∪ E
Especificaciones Constructivas:
TLista l();
Pre ::= Ninguna
Post ::= Longitud(l0 ) = 0
~TLista
l();
Pre ::= l ∈ L
Post ::= l 6∈ L
l.Longitud();
Pre ::= l ∈ L
Post ::= l0 = l
Ret ::= n
l.Elemento(N p);
Pre ::= l ∈ L ∧ 1 <= p <= n
Post ::= l0 = l
Ret ::= ap
l.Reescribir(N p, TBase x);
Pre ::= l ∈ L ∧ 1 <= p <= n
Post ::= n0 = n ∧ a0p = x
l.Insertar(N p, TBase x);
Pre ::= l ∈ L ∧ 1 <= p <= n + 1
Post ::= n0 = n + 1 ∧ a0k = ak ∀k < p; a0p = x; a0m = am−1 ∀m > p
l.Borrar(N p);
Pre ::= l ∈ L ∧ 1 <= p <= n
Post ::= n0 = n − 1 ∧ a0k = ak ∀k < p; a0m = am+1 ∀m ≥ p
5.10 Listas 29
Cursores Una implementación acotada muy importate es la de los bloques de memoria con
cursores.
La lista implementada mediante cursores es interesante por dos motivos:
es sólo algo menos eficiente que el array en su acceso, pero lo es igualmente en el borrado e
inserción, pero además
puede ser manipulada como un todo de forma que todo el bloque de cursores puede, por
ejemplo ser copiado como bloque de memoria sin necesidad de pedir bloques pequeños de
memoria que se localizarı́an en distintos sitios. Ası́mismo, puede, por ejemplo, ser guardada
o leı́da directamente como un fichero7 .
Muchos lenguajes carecen del tipo de datos de bajo nivel puntero, esto es, son incapaces de re-
ferenciar mediante variables de programa la dirección fı́sica en tiempo de ejecución de los datos.
Ejemplos de tales son FORTRAN, o COBOL, o Java, por nombrar los más conocidos. Es pues
inevitable en estos casos tratar de conseguir implementaciones eficientes de las listas usando el
mecanismo mejor de los cursores. Con los cursores lo que realmente se hace es implementar un
mecanismo semejante al de new delete que tiene ya el sistema, pero desarrollados por nosotros
7 si se utilizan punteros, el almacenamiento y posterior recuperación de la información en un fichero ha de hacerse
secuencialmente elemento a elemento ya que NO se pueden guardar los punteros de memoria interna en un fichero.
Muchos sistemas almacenan diversos tipos de estructuras más o menos complejas mediante cursores debido a que
los cursores son arrays de tamaño fijo en los que los elementos se acceden mediante ı́ndices numéricos (offsets) y,
por lo tanto son manipulables como un todo con gran eficiencia.
5.10 Listas 30
en una parcela mucho más controlada de la memoria, un bloque nuestro. Mediante los cursores se
dan de alta y de baja celdillas dentro de aquél bloque de memoria.
Pues bien, para dar de alta y de baja celdillas dentro de nuestra memoria, necesitaremos dos
procedimientos internos nuestros new y dispose que desarrollaremos dentro de la implementación
de la lista. El primero rastreará nuestra memoria y buscará una celdilla libre que ofrecer al
solicitante (dándola a la vez de baja de las celdillas libres). El segundo procedimiento delete
hará lo contrario: liberará, dará de alta como libre, la celdilla que se le indique.
Para poder gestionar los cursores es conveniente una estructura semejante a:
1 int maxelem;
2 struct Celdilla {
3 TBase elemento;
4 int sigui;
5 };
6 struct Bloque {
7 int primero, primerVacio, longitud;
8 Celdilla datos[maxelem]; // ó *datos inicializando antes
9 };
de manera que en primerVacio se guarda la posición de la primera celdilla fı́sica del array datos
que está disponible para ser reutilizada. Ahora bien, cada celdilla referencia a su vez a una siguiente
celdilla mediante sigui, de esta forma las celdillas vacı́as quedan encadenadas unas a otras desde
la primero, como una cadena de latas vacı́as. Al construir inicialmente nuestro objeto TLista,
deberemos poner todos los elementos a vacı́o. Para ello enlazamos a cada uno con el siguiente
(excepto el último, que enlaza con −1) y ponemos primerVacio apuntando al primero (0). Y
ponemos también entonces primero a −1 (nada). Ver Figura 13.
primero lUltimaVisita
0 1 2 3 4 5
primero = 1
primVacio = 2 -1 3 5 0 -1 4
lUltimaVisita = 3
posUltimaVisita = 2 primVacio
Figura 13: Implementación en memoria interna con cursores del TDA lista.
Se dejan como ejercicios las implementaciones del TDA lista como array y cursores.
lUltimaVisita
Se pueden tratar como listas también a los ficheros que tendrı́an ası́ caracterı́sticas muy
interesantes en cuanto a su capacidad y persistencia, pero en estos casos será necesaria una imple-
mentación con cursores en la que el bloque de celdillas será el fichero completo. Una ventaja extra
de los ficheros es que, al contrario de los cursores en memoria interna, no tienen por qué estar
acotados.
Esta implementación, aunque es muy adecuada para frecuentes inserciones/borrados y para
cuando se quiere liberar el máximo tamaño, tiene el grave problema de que si la lista crece mucho,
los accesos a los elementos se ralentizan, en particular, por ejemplo, los añadidos. Nótese que cada
vez que se quiere acceder a un elemento en la posición p hay que recorrer con un bucle del tipo:
1 tmp = l;
2 while (tmp != 0 && i < p) {
3 tmp = tmp->sigui;
4 ++i;
5 };
6 // usar *tmp
que conforme p aumente es más lento. Para insertar o borrar nos debemos detener en el nodo
anterior a p.
Claro, que si sólo recordamos el enlace a último nodo visitado, no servirá de mucho excepto que
nuestro único método de acceso sea “siguiente” (secuenciales) cosa que no es ası́. Los accesos son
a posiciones. Ası́ tendrı́amos que guardar no sólo ese enlace, sino también la posición en la que
éste nodo está.
1 struct nodo {
2 dato;
3 nodo *sigui;
4 };
5 nodo *datos;
6 nodo *lUltimaVisita;
7 int posUltimaVisita; // número de la posición de la última visita
Esta primera optimización, como hemos dicho, afecta enormemente los recorridos secuenciales del
tipo
1 for (n=l.Longitud(), i=1; i <= n; ++i)
2 // procesa l.Elemento(i);
lUltimaVisita
posUltimaVisita = 3
Ahora, bien, ya animados a optimizar, ya que esta primera optimización ha sido tan eficiente
y fácil, quisiéramos poder mejorar no sólo los accesos secuenciales. Con este método los accesos a
elementos en orden creciente son más rápidos, pero, los accesos a nodos anteriores no mejoran en
nada. En otras palabras, un acceso inverso, hacia atrás, no sólo no mejora nada sino que al tener
que estar actualizando nuestras marcas, resulta aún más penoso.
Solución: tener enlaces no sólo hacia adelante, sino también hacia atrás. Los nodos tendrı́an
un enlace sigui también un enlace ante. Esto permitirá varias cosas:
mejora los recorridos inversos
mejora los accesos a puntos intermedios ya que se elije el comienzo del recorrido de entre
tres puntos: el principio de la lista, el punto de la última visita y el final de la misma, lo que,
en promedio, divide por cuatro los recorridos. Ver Figura 16.
Simplifica la lógica de la inserción y borrado al poderse hacer todo desde el mismo punto
afectado, aunque aumenta el número de actualizaciones ya que intervienen más punteros.
Esto simplificación se nota sobre todo en que bastará con un procedimiento que devuelva el
nodo p-ésimo para todas las operaciones.
A B C D
lUltimaVisita
posUltimaVisita = 3
Se dejan como ejercicios las implementaciones del TDA lista en forma no acotada y sus
optimizaciones.
5.10.5. Listas.Ejercicios
. 10 Desarrollar la interfaz del TAD TLista posicional. Hacer la implementación mediante
1. arrays,
2. cursores
3. lista de nodos dinámicamente enlazados simple
4. primera optimización de la lista de nodos
5. segunda optimización de la lista de nodos
5.10 Listas 33
. 12 Desarrollar el TAD polinomio sobre el TAD TLista, esto es, en la implementación, los
datos se guardarán sobre una lista. Ası́ la base de la lista serán el grado/coeficiente de cada
monomio
5.11 Conjuntos 34
5.11. Conjuntos
La caracterı́stica fundamental de los conjuntos es su ausencia de estructura. Como ya se
estudió en matemáticas, cuando se habla de conjuntos únicamente se trata de agrupar elementos
bajo un concepto simple, un atributo. En el caso de los TDAs conjuntos sólo tendremos pues
que preocuparnos de Añadir, de Borrar y de Leer la información de un elemnto para agotar los
requisitos de un conjunto. Sin embargo, surge inmediatamente el inconveniente práctico de la
necesaria iteración (ya sea para copiarlo en otro, ya sea para presentar quizás, su contenido, o sólo
por observar, ver desfilar, sus componentes con otro criterio).
Se habla también de Tablas, que son conjuntos en los que se diferencia una clave (única) por
elemento. Una clave identificativa para cada elemento. Es difı́cil encontrar la diferencia entre Tabla
y Diccionario.
En C++ podrı́amos definir una tabla como:
1 typedef char TClave[16];
2 typedef struct TBase{
3 TClave clave;
4 ...
5 };
7 class TTabla {
8 public:
9 TTabla();
10 ~TTabla(void);
11 void Anyadir(TClave k, TBase x);
12 void Borrar(TClave k);
13 bool EstaEn(TClave k, TBase &devolver);
14 private:
15 ...
16 };
En el caso de la tabla, al no haber primero ni estructura sobre la que ver los elementos, por
ejemplo como Izda()/Dcha(), Elemento(pos), Desapilar(), etc. que tienen los demás TDAs
es posible algún tipo de iteración sobre los elementos. En realidad, como en las pilas o colas,
la iteración no es precisamente la operación más frecuente ni necesaria, pero de cara a “ver” la
estructura, como hemos dicho antes, sı́ es importante. Particularmente veremos cómo se podrı́a
definir una interfaz adecuada para las tablas.
31
132
233 31
334
455
536
…
Figura 17: Colisión de múltiples valores de clave en un valor único final con el hashing
h(x) = x mód 101.
1
0.8
0.6
0.4
0.2
10 20 30 40
(Algo que demostró Feller en 1950. Ver [Knu73, p. 553] ó [Kru88]). Concretamente, si selec-
cionamos una función hash al azar que aplica 23 claves en una tabla de 365 celdas, la probabilidad
de que no haya dos coincidencias de las claves en la misma posición es de sólo f (23) = 0,4927
ó f (22) = 0,5243).
De hecho
n
Y 365 − i + 1
f (n) =
i=2
365
se puede expandir a:
(−1)n−1 Pochhammer(−364, n − 1)
365n−1
5.11 Conjuntos 36
con
Γ(a + n)
Pochhammer(a, n) ≡ (a)n ≡ (a)n̄ = ,
Γ(a)
función que aparece en la expansión de funciones hipergeométricas y que tiene valor definido aún
cuando la función Γ sea infinito en ella. Ver Abramowitz and Stegun 1972, p. 256; Spanier 1987;
Koepf 1998, p. 5.
o, en general:
Cifras(N, desde, hasta) -> (N / (desde-1)) % hasta
Selección de dı́gitos Si se tienen claves con muchos dı́gitos (números de teléfono, por ejemplo),
es interesante hacer una selección de los dı́gitos más cambiantes, los últimos, probáblemente,
para evitar las colisiones, ya que en determinadas secuencias de claves (números de teléfonos,
por ejemplo) se suelen repetir insistentemente partes de la clave.
Plegado (“folding”) Consiste en la suma de los dı́gitos (o caracteres):
Problemas Con la prueba lineal se da un fenómeno indeseable: Cualquier clave que colisione
con otra tendrá que colisionar necesariamene con todas con las que ya haya también colisionado la
anterior antes de encontrar un lugar libre. Esto provoca lo que se llama una agrupación primaria
(“primary clustering”)9 .
Considérese por ejemplo la Figura 19, inicialmente, con la tabla vacı́a, la probabilidad de
a b c d e
que una clave cualquiera se inserte en la posición ‘b’ es de 1/N . Una vez llenada la celda ‘a’, la
probabilidad de que ‘b’ se llene se ha duplicado, ya que también los elementos que vayan a ‘a’
terminan en ‘b’. Una vez llenado ‘b’, la probabilidad de que ‘e’ se llene es 5/N . Se trata pues de
que mientras más larga es la cadena, más larga aún tiende a hacerse (efecto “bola de nieve”). Es
pues un problema de inestabilidad en el reparto de los elementos.
Soluciones Se pueden adoptar muchas técnicas para disminuir al máximo la ineficiencia del hash
cerrado conforme se va llenando la tabla, sobre todo en lo referente a la formación de agrupaciones.
Las alternativas más importantes la prueba lineal son:
8 Realmente la mayorı́a de los autores llaman a la resolución de colisiones lineal hashing lineal y reservan la
palabra rehashing para el caso en que se usen distintas funciones posteriores de hashing. Como veremos más
adelante.
9 Los nombres de primario o n-ario para el clustering vienen de que la cadena (chain) tiene su forma establecida
Doble rehashing Para evitar la agrupación se afina en el cálculo de nuevas direcciones. Hay
muchas formas; la técnica de rehashing doble utiliza una segunda función de cálculo de la
dirección para obtener la segunda dirección a considerar. Si la distribución inicial está sufi-
cientemente dispersa, no serı́a necesario una función independiente, pero es esto lo que va a
mantener la dispersión, la independencia de las posiciones ya encontradas en el cálculo de
las nuevas.
El rehashing doble utiliza la misma técnica que el lineal pero la función hi (x) = (h(x) + i ×
u) mód N . La elección de u el tamaño de los sucesivos saltos y de N , el tamaño de la tabla,
deben hacerse con cuidado. Evidentemente valores como u = 0 ó u = 2N son inaceptables.
Es importante que u y N sean primos relativos (no tengan factores comunes). Esto se puede
conseguir haciendo N primo y u < N .
Prueba cuadrática En vez de posiciones consecutivas se hacen aumentos cuadráticos hi (x) =
(h0 + i2 ) mód N . NO prueba, sin embargo, toda la tabla. Si N = 2k (potencia de dos), se
probarı́an especialmente pocas posiciones. Ahora bien, supongamos que N es primo, y que
vamos calculando nuevas posiciones, desde la prueba i a la prueba j en que por fin vuelve a
coincidirse con la posición calculada en la prueba i. Entonces:
y ya que N es primo, debe dividir a algún factor de los dos. Si divide a (j − i), sólo lo
hará cuando j diste de i en N pruebas (o un múltiplo de N : entonces j = cN + i, y
j 2 = c2 N 2 + 2cN i + i2 que en módulo N nos da
leatorios queda perfectamente definida y repetible. Esto es la secuencia es sólo función de la semilla.
5.11 Conjuntos 39
forma que siempre se produzca la misma secuencia. Este tipo de rehashing tiene la ventaja
de eliminar las agrupaciones (clusterings) primarias y secundarias, pero el inconveniente de
ser de más complejidad y de que con un generador tı́pico de números aleatorios se repetirán
localizaciones y no se tiene, en pocas pruebas por qué visitar toda la tabla.
Una variante de este mecanismo es la de tomar una de las N ! permutaciones posible de
secuencias para nuentra tabla de N elementos. Mediante esta alternativa se evitan clustering,
repeticiones, etc. Ver [HS87] (págs. 452+), [AHU83] (págs. 122+), [Har89] (Chapter 9).
Figura 20: Hash abierto. Los valores de hash coincidentes se encadenan sin lı́mite de
capacidad.
o no. La búsqueda posterior mejorarı́a, lógicamente, si fuese ordenada, como vimos en el tema de
listas, pero, dado que estas listas son muy cortas, no merece la pena mantenerlas ordenadas. Una
buena elección para el tamaño de la tabla hash es de un décimo del total de elementos esperados;
entonces el promedio de longitud de las listas serı́a de 10.
5.11.7. Complejidades
Se pueden calcular de una manera relativamente fácil las complejidades de los rehashing
aleatorios, en los que la posición siguiente, en cada prueba se encuentra con toda la tabla, como
posible diana y con la misma probabilidad para coda celda. Veremos, en este apartado cómo estimar
estas complejidades y las de los demás métodos de hashing y rehashing, en forma comparativa.
Una medida de la probabilidad de colisión la da el factor de carga α:
núm. celdas ocup.
α=
N
Con las técnicas de dirección abierta, la cantidad de comparaciones necesarias para encontrar
celdas libres aumenta rápidamente cuando α → 1, sin embargo, en el encadenamiento externo
del hashing abierto el número de comparaciones necesarias depende directamente del número de
colisiones ocurridas. Concretamente, para una celda i con una lista externa con ni elementos se
habrán dado ni colisiones y la complejidad de la búsqueda (si la inserción no ha sido ordenada en la
lista) es proporcional a ni . Ası́ dado que el tamaño de la tabla es fijo, en el hash de encadenamiento
externo la complejidad del acceso depende, en promedio, directamente del número de colisiones
totales.
5.11 Conjuntos 40
Notaciones Sea HT (0 : N − 1) una tabla hash con N celdas. Sea h una función hash uniforme
de rango [0, N − 1]. Si se insertan n identificadores x1 , x2 , . . . , xn en la tabla, habrá N n posibles
secuencias h(x1 ), h(x2 ), . . . , h(xn ) de hashings igualmente probables según los posibles órdenes de
llegada11 . Sea
S(α)
el número de comparaciones de identificadores esperadas para localizar el identificador xi (1 ≤
i ≤ n) y por tanto S(α) es el promedio de comparaciones necesarias para localizar cualquier xj
existente. Es por lo que no se espera dependencia del xi que sea, sino tan sólo de α. Sea igualmente
U (α)
pero
∞
X 1
αxα−1 = ,
α=1
(1 − α)2
nos queda,
1
UHΑL UHΑL=
H1 - ΑL
10
α S(α) 3
0.1 1.05 2.5
0.25 1.15
2
0.5 1.39
0.75 1.85 1.5
0.9 2.56 1
0.95 3.15 0.5
0.99 4.66
0.2 0.4 0.6 0.8 1
Nótese que una tabla al 90 % llena requiere ¡tan sólo 2.56 pruebas! para localizar un elemento.
El estudio anterior (que se puede encontrar, por ejemplo en [Kru88]), suponen una distribu-
ción uniforme (aleatoriamente uniforme) de cada dirección hash en cada intento, lo que ha hecho
relativamente fácil el estudio. Los métodos reales más utilizados, como el de rehashing lineal, tienen
un comportamiento algo peor, y la evaluación del mismo es más complicada.
En el caso del encadenamiento externo, la búsqueda infructuosa requiere un número pro-
medio de pasos
Uexterno (α) ≈ α
pero téngase en cuenta que aquı́ α = n/N puede ser mayor que 1. Si se trata de localizar exito-
samente el elemento, suponiendo listas encadenadas desordenadas, de longitud promedio α todas
ellas, la posición de aparición del elemento en la lista requerirá longitud-lista/2 pasos, de modo
que el todal de pasos para el encuentro exitoso será:
α
1+ .
2
En el caso lineal, el estudio es complicado (ver [Knu73]); tan sólo exponemos los resultados
pesimistas aproximados, pues los reales son algo mejores:
3
α S(α)
0.1 1.06 2.5
0.25 1.17 2
1 1
S(α) ≈ 1+ 0.5 1.50 1.5
2 1−α
0.75 2.50
1
0.9 5.50
0.95 10.50 0.5
que vemos que tiene un comportamiento muy bueno excepto que la tabla esté prácticamente llena.
5.11 Conjuntos 42
En resumen:
Tipo ≈ U (α) ≈ S(α)
1 1 1 1
Lineal 1+ 1+
2 (1 − α)2 2 1−α
1 1
Aleatorio − loge (1 − α)
1−α α
α
Externo α 1+
2
Ver Figura 21.
10
Slineal(α)
8
Ulineal(α)
6 Saleatorio(α)
Ualeatorio(α)
4
0
0.0 0.2 0.4 0.6 0.8 1.0
Sexterno(α) Uexterno(α)
procedimiento buscador de la celda para una clave de forma que además diga, si se ha
localizado un elemento, si es el último de una cadena, de forma que esta información la
pueda recoger el método de borrado para marcar esa posición no a “borrado” sino a “vacı́o”,
recuperando ası́ posiciones vacı́as y acortando las agrupaciones.
. 18 Implementar el TDA TTabla, tal y como se definió inicialmente, mediante el uso de técnicas
hashing (hash abierto), suponiendo para esto que el tipo base de la tabla admite directamente
una función int hash(TBase).
. 19 Desarrollar una función que devuelva los dı́gitos i al j de un número positivo cualquiera
dn dn−1 . . . d2 d1
. 20 Implementar una técnica de rehashing cuadrático examinando las posiciones h(x), (h(x) +
i2 ) mód N , y (h(x) − i2 ) mód N con 1 ≤ i ≤ (N − 1)/2 y N un número primo de la forma
4j + 3. Comprobar que con estas funciones de rehashing se examinan todas las posiciones de
la tabla de tamaño N .
. 21 Implementar una tabla hash de encadenamiento externo de sı́mbolos, esto es, la clave será un
array de caracteres.
. 24 En la paradoja de los cumpleaños se puede comprender mejor el hecho de ser llamada para-
doja, viendo las respuestas a
. 26 Otro método de mantener las listas de desbordamiento es permitiendo que el primer elemento
de ellas esté en el propio array. Estudiar la mejora de este método en cuanto a complejidad
espacial y ver cómo hay que modificar el algoritmo de mantenimiento.
. 27 Suponiendo un borrado muy infrecuente, evitar la marca de borrado del hashing cerrado
mediante un procedimiento de borrado que mueva el elemento que siga a la posición borrada
al lugar borrado, y ası́ con el resto de la cadena que haya. ¿Cuándo conviene este método?
. 29 Implementar un procedimiento que evalúe U (α) y S(α). Para ello muestrear con 1000 búsque-
das infructuosas (U ) y 1000 búsquedas exitosas (S) para valores de α de 18 , 14 , 34 y 78 (o más
puntos) y graficar los resultados.
NOTA: Para conseguir estos valores de α llenar la tabla con datos variados (un función
aleatoria serı́a aquı́ adecuada) hasta llenados adecuados.
. 30 Desarrollar un tratamiento de ficheros “indexado”. Para ello utilizar la técnica de hashing
desarrollada en el ejercicio 16.
1. Dar un tamaño fijo al fichero.
2. Estructurar una cabecera en el fichero (el registro cero) que mantenga el tamaño actual
del fichero y sólo cuando todas las celdas actuales del fichero estén llenas aumentar
el tamaño de este razonablemente, actualizando entonces el tamaño guardado en la
cabecera.
p = 104 p1 + p0
y
q = 104 q1 + q0
de forma que
1 class Random {
2 public:
3 Random(const unsigned long int s=314159) {
4 pow = 10000; b = 31415821; m = 100000000; a = s;}
5 void Reiniciar(const unsigned long int s=314159) { a=s; }
6 unsigned random(unsigned desde=0, unsigned hasta=100) {
7 a = (mult(a,b)+1) % m;
8 return desde + ((a / pow) * hasta) / pow;
9 }
10 private:
11 unsigned long pow, a, b, m;
12 unsigned long mult(unsigned long p,q);
13 };
19 p1 = p / pow; p0 = p % pow;
20 q1 = q / pow; q0 = q % pow;
21 return (((p0*q1+p1*q0) % pow) * pow+p0*q0) % m;
22 }
Referencias
[AHU83] A. Aho, J. Hopcroft, and J. Ullman. Data Structures and Algorithms. Addison-Wesley,
1983. Traducido al castellano, 1988.
[Har89] Rachel Harrison. Abstract Data Types in Modula-2. John Wiley & Sons, 1989.
[HS87] E. Horowitz and S. Sahni. Fundamentals of Data Structures in Pascal. Computer Science
Press, 1987.
[Knu73] Donald E. Knuth. The Art of Computer Programming. Vol. 3: Searching and Sorting.
Addison-Wesley, Massachusetts, 1973. Traducido al castellano en Ed. Reverté, Barcelona.
[Mar86] Johannes J. Martin. Data types and data structures. Prentice-Hall, 1986.
[Sed88] R. Sedgewick. Algorithms. Addison-Wesley, second edition, 1988. Hay al menos otros
dos tı́tulos de igual contenido “Algorithms in Pascal” y “Algorithms in C” (1990).
[Tuc88] Allen B. Tucker. Computer Science. A second course using Modula-2. McGraw-Hill,
New York, 1988.
[Wir76] Niklaus Wirth. Algorithms + Data Structures = Programs. Prentice-Hall, New York,
1976. Traducción al castellano en Ed. del Castillo, Madrid (1980).
Juan Falgueras
Dpto. Lenguajes y Ciencias de la Computación
Universidad de Málaga
Despacho 3.2.32