Logo Passei Direto
Material

Esta es una vista previa del archivo. Inicie sesión para ver el archivo original

EDI todo/ejercs T-1.pdf
 1 
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Gestión. Curso 2007/2008 
Ejercicios del Tema 1 
 
Análisis de algoritmos iterativos 
1. Determina la complejidad temporal en el caso peor y en el caso mejor de los siguientes algorit-
mos de ordenación. 
 (a) Ordenación por inserción 
void ordenaIns ( int v[], int num ) { 
 int i, j, x; 
 
 for ( i = 1; i < num; i++ ) { 
 x = v[i]; 
 j = i-1; 
 while ( (j >= 0 ) && (v[j] > x) ){ 
 v[j+1] = v[j]; 
 j = j-1; 
 } 
 v[j+1] = x; 
 } 
} 
 
 (b) Ordenación por selección 
void ordenaSel ( int v[], int num ) { 
 int i, j, menor, aux; 
 
 for ( i = 0; i < num; i++ ) { 
 menor = i; 
 for ( j = i+1; j < num; j++ ) 
 if ( v[j] < v[menor] ) 
 menor = j; 
 if ( i != menor ) { 
 aux = v[i]; 
 v[i] = v[menor]; 
 v[menor] = aux; 
 } 
 } 
} 
 (c) Método de la burbuja 
void ordenaBur ( int v[], int num ) { 
 int i, j, aux; 
 bool modificado; 
 i = 0; 
 modificado = true; 
 while ( (i < num-1) && modificado ) { 
 modificado = false; 
 for ( j = num-1; j > i; j-- ) 
 if ( v[j] < v[j-1] ) { 
 aux = v[j]; 
 v[j] = v[j-1]; 
 v[j-1] = aux; 
 modificado = true; 
 } 
 i++; 
 } 
} 
 2 
2. Determina la complejidad temporal en el caso peor del siguiente algoritmo. 
int buscaBin( int v[], int num, int x ) { 
 int izq, der, centro; 
 
 izq = -1; 
 der = num; 
 while ( der != izq+1 ) { 
 centro = (izq+der) / 2; 
 if ( v[centro] <= x ) 
 izq = centro; 
 else 
 der = centro; 
 } 
 return izq; 
} 
3. El siguiente algoritmo de búsqueda decide si un entero dado aparece o no dentro de un vector de 
enteros dado: 
bool busca( int v[], int num, int x ) { 
// Pre.: v es un array de al menos num elementos 
 int j; 
 bool encontrado; 
 
 j = 0; 
 encontrado = false; 
 while ( (j < num) && ! encontrado ) { 
 encontrado = ( v[j] == x ); 
 j++; 
 } 
 return encontrado; 
// Post.: devuelve true si x aparece en v entre las posiciones 0 .. num-1 
// y false en caso contrario 
} 
Analiza la complejidad en promedio del algoritmo, bajo las hipótesis siguientes: (a) la probabilidad de 
que x aparezca en v es un valor constante p, 0 < p < 1; y (b) la probabilidad de que x aparezca en 
la posición i de v (y no en posiciones anteriores) es la misma para cada índice i, 0 ≤ i < num. 
4. Las dos implementaciones siguientes calculan la potencia nm: 
int potencia1(int n, int m) 
{ 
 int p; 
 p=1; 
 while (m>0) 
 { 
 p=p*n; m--; 
 } 
 return p; 
} 
int potencia2(int n, int m) 
{ 
 int p; 
 p=1; 
 while (m>0) 
 { 
 if (m%2!=0) p=p*n; 
 m=m/2; 
 n=n*n; 
 } 
 return p; 
} 
Describir su funcionamiento y determinar su complejidad en el peor caso, ¿cuál es mejor?. 
 3 
5. (Febrero 2003) Describir brevemente lo que hace el siguiente fragmento de código y determinar 
su complejidad asintótica en función de n: 
for (i=1; i<=n; i++){ 
 for (j=1; j<=n-i; j++) cout << " "; 
 for (j=1; j<=i; j++) cout << j; 
 for (j=i-1; j>=1; j--) cout << j; 
 cout << "\n"; 
} 
6. Los algoritmos de ordenación de arrays han sido objeto de estudio meticuloso en el área de la in-
formática, así como la complejidad asociada a los mismos. Los que hemos estudiado nosotros 
hasta el momento son generales en el sentido de que, en principio, sirven para ordenar cualquier 
array de enteros (es muy fácil modificarlos para ordenar arrays de cualquier tipo ordenado). 
Sin embargo, para algunos problemas concretos es posible sacar partido de las particularidades 
del problema. Por ejemplo, si sabemos que el array a ordenar v[1..n] contiene enteros en un rango 
acotado (y relativamente pequeño), n0 ≤ v[i] ≤ n1 para todo i ∈ {1, . . . , n}, podemos utilizar el 
siguiente algoritmo: 
 construir la tabla de frecuencias t asociada a v. Esta tabla no es más que un array t[n0..n1] tal 
que t[i] contiene el número de apariciones del elemento i en el array inicial v 
 a partir de la tabla de frecuencias, obtener la ordenación del vector v. La idea es: colocar al 
principio de v t[n0] elementos n0, a continuación colocar t[n0 +1] elementos n1 y así sucesi-
vamente. 
Por ejemplo, consideremos v = [1, 3, 2, 4, 5, 3, 4, 2, 3, 1, 4, 5] 
Todos los elementos están en el rango [1.,5]. La tabla de frecuencias asociada es: 
1 2 3 4 5
t 2 2 3 3 2
Ahora, colocamos en v en este orden 2 unos, 2 doses, 3 treses, 3 cuatros y 2 cincos, y obtenemos 
el array ordenado: 
v = [1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5] 
Implementar este algoritmo en C++ para vectores de enteros en el rango 0.,999. ¿Cuál es la com-
plejidad del algoritmo en el peor caso? ¿Y en el mejor? Ampliar la funcionalidad de modo que el 
rango de los elementos se determine en tiempo de ejecución y el algoritmo opere de acuerdo con 
este dato. ¿Cuál es la complejidad de este nuevo algoritmo? 
7. (Junio 2003) Hoy es día de excursión para todo el cole y los niños están muy contentos. Como 
son muchísimos niños, para evitar despistes y no perder a ninguno, la señorita encargada los ha 
colocado por orden alfabético. Sin embargo, en un despiste de la “seño”, algunos niños han 
hecho de las suyas y han intercambiado su posición con la de uno de los compañeros de al lado 
(el de la izquierda o el de la derecha). Los niños más revoltosos han repetido este proceso varias 
veces, mientras que hay otros que no se han movido ni una sola vez, como Remedios que es muy 
buena. La señorita se enfada mucho cuando descubre el desaguisado: “He malgastado n*log(n) (uni-
dades de tiempo, siendo n el número de niños) en ordenaros... ¡Os pondré un negativo a todos!”. Pero Remedios, 
que tiene un expediente inmaculado, protesta: “Seño, yo no me he movido ninguna vez. Jaimito es el que 
más veces se ha movido. Yo lo he visto... y se ha movido 4 veces”. 
Casualmente un alumno de EDI que ve lo que ha ocurrido y tranquiliza a la señorita diciéndole 
que la entropía generada por los niños no es excesiva y que de hecho puede diseñar un algoritmo 
de complejidad lineal para recomponer la ordenación. Asumiendo que los niños están representa-
dos en un vector diseña e implementa un algoritmo que reconstruya la ordenación en tiempo 
lineal. Por simplicidad, puedes suponer que cada niño está representado por un entero con res-
pecto al que se ordena. Razona que el algoritmo construido es de complejidad lineal. 
 4 
Órdenes de Complejidad 
8. Demostrar: 
a) f(n) ∈ O(g(n)) ⇔ O(f(n)) ⊆ O(g(n)) 
b) O(f(n)) = O(g(n)) ⇔ f(n) ∈ O(g(n)) y g(n) ∈ O(f(n)) 
d) O(c · f(n)) = O(f(n)), siendo c > 0. 
9. Demostrar la regla de la suma para el orden exacto: 
Θ(f(n) + g(n)) = Θ(max(f(n), g(n))) 
10. Demostrar las siguientes inclusiones estrictas: 
O(1) ⊂ O(log(n)) ⊂ O(n) ⊂ O(n ¢ log(n)) ⊂ O(n2) ⊂ O(n3) ⊂ . . . ⊂ O(nk) ⊂ O(2n) ⊂ O(n!) 
11. Demostrar la propiedad de la dualidad: 
f(n) ∈ O(g(n)) ⇔ g(n) ∈ Ω(f(n)) 
12. Demostrar o refutar las siguientes propiedades: 
a) f(n) ∈ O(n2) y g(n) ∈ O(n) ⇒ f(n)/g(n) ∈ O(n) 
b) f(n) ∈ Θ (n2) y g(n) ∈ Θ (n) ⇒ f(n)/g(n) ∈ Θ (n) 
13. Demostrar o refutar las siguientes afirmaciones: 
a) 2n + n99 ∈ O(n99) 
b) 2n + n99 ∈ Ω(n99) 
c) 2n + n99 ∈ Θ(n99) 
d) 2n + n99 ∈ O(2n) 
e) 2n + n99 ∈ Ω(2n) 
f) 2n + n99 ∈ Θ(2n) 
14. Buscar ejemplos de funciones f(n) y g(n) tales que f(n) ∈ O(g(n)) pero f(n) ∉ Ω(g(n)) 
EDI todo/ejercs T-2.pdf
 1 
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Gestión. Curso 2007/2008 
Ejercicios del Tema 2 
Diseño de algoritmos recursivos 
1. Dado un vector de enteros de longitud N, diseñar un algoritmo de complejidad O(N·logN) que 
encuentre el par de enteros más cercanos. ¿Se puede hacer con complejidad O(N)? 
2. Dado un vector de enteros de longitud N, diseñar un algoritmo de complejidad O(N·logN) que 
encuentre el par de enteros más lejanos. ¿Se puede hacer con complejidad O(N)? 
3. Diseñar un algoritmo recursivo que realice
el cambio de base de un número binario dado en su 
correspondiente decimal. 
4. Diseñar un algoritmo recursivo que, dado un array de caracteres, cuente el número de vocales que 
aparecen en él. 
5. Escribir una función recursiva que devuelva el producto de los elementos de un array mayores o 
iguales que un determinado número b 
6. Implementa una función recursiva simple cuadrado que calcule el cuadrado de un número natural 
n, basándote en el siguiente análisis de casos: 
Caso directo: Si n = 0, entonces n2 = 0 
Caso recursivo: Si n > 0, entonces n2 = (n–1)2 + 2*(n–1) + 1 
7. Implementa una función recursiva log que calcule la parte entera de logb n, siendo los datos b y n 
enteros tales que b ≥ 2 ∧ n ≥ 1. El algoritmo obtenido deberá usar solamente las operaciones de 
suma y división entera. 
8. Implementa una función recursiva bin tal que, dado un número natural n, bin(n) sea otro número 
natural cuya representación decimal tenga los mismos dígitos que la representación binaria de n. 
Es decir, debe tenerse: bin(0) = 0; bin(1) = 1; bin(2) = 10; bin(3) = 11; bin(4) = 100; etc. 
9. Implementa una función recursiva doble que satisfaga la siguiente especificación: 
int sumaVec( int v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
// siendo num el número de elementos de v 
 
// Post: devuelve la suma de las componentes de v entre a y b 
} 
Básate en el siguiente análisis de casos: 
Caso directo: a ≥ b 
v[a..b] tiene a lo sumo un elemento. El cálculo de s es simple. 
Caso recursivo: a < b 
v[a..b] tiene al menos dos elementos. Hacemos llamadas recursivas para sumar v[a..m] y 
v[m+1..b], siendo m = (a+b) / 2. 
 2 
10. Implementa un procedimiento recursivo simple dosFib que satisfaga la siguiente especificación 
pre/post: 
void dosFib( int n, int& r, int& s ) { 
// Pre: n >= 0 
// Post: r = fib(n) && s = fib(n+1) 
} 
En la postcondición, fib(n) y fib(n+1) representan los números que ocupan los lugares n y n+1 
en la sucesión de Fibonacci, para la cual suponemos la definición recursiva habitual. 
11. Implementa una función recursiva que calcule el número combinatorio ⎟⎟
⎠
⎞
⎜⎜
⎝
⎛
m
n
 a partir de los datos 
m, n: Integer tales que n ≥ 0 ∧ m ≥ 0. Usa la recurrencia siguiente: 
⎟⎟
⎠
⎞
⎜⎜
⎝
⎛ −
+⎟⎟
⎠
⎞
⎜⎜
⎝
⎛
−
−
=⎟⎟
⎠
⎞
⎜⎜
⎝
⎛
m
n
m
n
m
n 1
1
1
 siendo 0 < m < n 
12. Una palabra se llama palíndroma si la sucesión de sus letras no varía al invertir el orden. Especifica 
e implementa una función recursiva final que decida si una palabra dada, representada como vec-
tor de caracteres, es o no palíndroma. 
13. El problema de las torres de Hanoi consiste en trasladar una torre de n discos de diferentes tama-
ños desde la varilla ini a la varilla fin, con ayuda de la varilla aux. Inicialmente los n discos están 
apilados de mayor a menor, con el más grande en la base. En ningún momento se permite que un 
disco repose sobre otro menor que él. Los movimientos permitidos consisten en desplazar el dis-
co situado en la cima de una de las varillas a la cima de otra, respetando la condición anterior. 
Construye un procedimiento recursivo hanoi tal que la llamada hanoi(n, ini, fin, aux) produzca el 
efecto de escribir una serie de movimientos que represente una solución del problema de Hanoi. 
Supón disponible un procedimiento movimiento(i, j), cuyo efecto es escribir "Movimiento de la va-
rilla i a la varilla j ". 
Análisis de algoritmos recursivos 
14. En cada uno de los casos que siguen, plantea una ley de recurrencia para la función T(n) que mide 
el tiempo de ejecución del algoritmo en el caso peor, y usa el método de desplegado para resolver 
la recurrencia. 
 (a) Función log (ejercicio 2). 
 (b) Función bin (ejercicio 3). 
 (c) Función sumaVec (ejercicio 4). 
 (d) Procedimiento dosFib (ejercicio 5). 
 (e) La función del ejercicio 6. 
 (f) Función palindroma (ejercicio 7). 
 (g) Procedimiento hanoi (ejercicio 8). 
15. Aplica las reglas de análisis para dos tipos comunes de recurrencia a los algoritmos recursivos del 
ejercicio anterior. En cada caso, deberás determinar si el tamaño de los datos del problema decre-
ce por sustracción o por división, así como los parámetros relevantes para el análisis. 
 3 
16. En cada caso, calcula a partir de las recurrencias el orden de magnitud de T(n). Hazlo aplicando 
las reglas de análisis para dos tipos comunes de recurrencia. 
 (a) T(1) = c1; T(n) = 4 ⋅ T(n/2) + n, si n > 1 
 (b) T(1) = c1; T(n) = 4 ⋅ T(n/2) + n2, si n > 1 
 (c) T(1) = c1; T(n) = 4 ⋅ T(n/2) + n3, si n > 1 
17. Usa el método de desplegado para estimar el orden de magnitud de T(n), suponiendo que T obe-
dezca la siguiente recurrencia: 
 T(1) = 1; T(n) = 2 ⋅ T(n/2) + n ⋅ log n, si n > 1 
¿Pueden aplicarse en este caso las reglas de análisis para dos tipos comunes de recurrencia? 
¿Por qué? 
Eliminación de la recursión final 
18. Aplica la transformación de recursivo final a iterativo sobre la función palindroma del ejercicio 7. 
19. La primera versión (la que aparece a la izquierda) de la búsqueda binaria es la misma que aparecía 
en el ejercicio 6 de la hoja 1 de ejercicios, mientras que la segunda (la que aparece a la derecha), 
vista en teoría, es el resultado de transformar a iterativo la versión recursiva de este algoritmo. 
¿En qué se diferencian estos dos algoritmos iterativos? 
Escribe un algoritmo recursivo final con la misma idea de la primera versión iterativa. 
 
int buscaBin( TElem v[], int num, TElem x ) 
{ 
 int izq, der, centro; 
 
 izq = -1; 
 der = num; 
 while ( der != izq+1 ) { 
 centro = (izq+der) / 2; 
 if ( v[centro] <= x ) 
 izq = centro; 
 else 
 der = centro; 
 } 
 return izq; 
} 
 
int buscaBin( TElem v[], int num, TElem x ) 
{ 
 int a, b, p, m; 
 
 a = 0; 
 b = num-1; 
 while ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 a = m+1; 
 else 
 b = m-1; 
 } 
 p = a - 1; 
 return p; 
} 
 
 4 
Técnicas de generalización 
20. Se trata de obtener un algoritmo recursivo de complejidad lineal que dado un polinomio de coefi-
cientes enteros, representado como un vector, y el valor de la incógnita, calcule el valor del 
polinomio. En el vector, de tipo int v[ ], se almacenan los coeficientes del polinomio ordenados 
por exponentes: en la posición 0 el coeficiente de x0, en la posición 1 el coeficiente de x, y así su-
cesivamente. 
Idea: Implementa primero una función recursiva evaluaGen de complejidad cuadrática donde las 
potencias de x se obtengan de forma iterativa. A continuación, plantea otra generalización eva-
luaEfi que permita obtener el algoritmo de complejidad lineal, y escribe su implementación 
transformando el código de evaluaGen. Puede conseguirse recursión final. 
21. Comprueba que el procedimiento combiGen especificado como sigue es una generalización de la 
función combi del ejercicio 6, y que combiGen admite un algoritmo recursivo simple, más eficiente 
que la recursión doble de combi, implementándola. 
 
void combiGen ( int a, int b, int v[], int num ) { 
// Pre: 0 <= b <= a && b < num 
// Post: para cada i entre 0 y b se tiene v[i] = ⎟⎟
⎠
⎞
⎜⎜
⎝
⎛
i
a
} 
 
22. Especifica e implementa un algoritmo recursivo que dado un vector de enteros calcule el número 
de posiciones de dicho vector que cumplan la condición de que la suma de las componentes a su 
izquierda coincida con la suma de las componentes a su derecha. Utilizando el método de des-
pliegue de recurrencias, analiza la complejidad temporal en el caso peor del algoritmo obtenido. 
23. Especifica e implementa un algoritmo recursivo que dado un vector de enteros lo reorganice de 
forma que los valores pares ocupen la parte izquierda del vector y los valores impares la parte de-
recha. Utilizando el método de despliegue de recurrencias, analiza la complejidad temporal en el 
caso peor del algoritmo obtenido. 
24. Especifica e implementa un algoritmo recursivo que
dado un vector de enteros determine si el 
vector tiene forma de montaña. Un vector tiene forma de montaña si contiene una secuencia estric-
tamente creciente seguida de una secuencia estrictamente decreciente −una de las dos secuencias 
puede ser vacía–. Utilizando el método de despliegue de recurrencias, analiza la complejidad tem-
poral en el caso peor del algoritmo obtenido. 
25. Especifica e implementa un algoritmo recursivo que dado un vector v de enteros, que viene dado 
en orden estrictamente creciente, determine si el vector contiene alguna posición i que cumpla 
v[i]=i. Utilizando el método de despliegue de recurrencias, analiza la complejidad temporal en el 
caso peor del algoritmo obtenido. 
26. Especifica e implementa un algoritmo recursivo que dados dos vectores u, v de enteros, ordena-
dos en orden estrictamente creciente, obtenga el número de elementos que aparecen en los dos 
vectores. Utilizando el método de despliegue de recurrencias, analiza la complejidad temporal en 
el caso peor del algoritmo obtenido. 
 5 
27. Especifica e implementa un algoritmo recursivo que dado un vector v de enteros determine si 
contiene un punto de inflexión. Un punto de inflexión es una posición del vector tal que todas las 
componentes que aparecen a su izquierda son negativas, y todas las que aparecen a su derecha 
son positivas o 0. Si el vector contiene punto de inflexión la función devolverá su posición, y si 
no lo contiene devolverá –1. Utilizando el método de despliegue de recurrencias, analiza la com-
plejidad temporal en el caso peor del algoritmo obtenido. 
28. Especifica e implementa un algoritmo recursivo que dado un vector v de enteros determine si el 
vector tiene la forma 1n 0n 0n 1n, es decir: un cierto número n de unos, seguidos del doble (2n) de 
ceros, seguidos a su vez de otros n unos. Por ejemplo, un vector de tamaño 8 con las componen-
tes [1,1,0,0,0,0,1,1] cumpliría lo anterior. Utilizando el método de despliegue de recurrencias, 
analiza la complejidad temporal en el caso peor del algoritmo obtenido. 
29. Especifica e implementa un algoritmo recursivo que dado un vector v de enteros determine si el 
vector representa una escalera con peldaños de ancho estrictamente creciente, donde: 
— Un peldaño queda constituido por una secuencia de apariciones consecutivas del mismo ele-
mento. Por ejemplo, el vector [1, 6, 6, 6, 2, 2, 5] tiene 4 peldaños. 
— El ancho de un peldaño es el número de apariciones consecutivas del elemento que constituye 
el peldaño. Por ejemplo, los anchos de los 4 peldaños del vector anterior son 1, 3, 2 y 1 (por 
orden de aparición). 
— Un vector forma una escalera con peldaños de ancho estrictamente creciente si cada peldaño 
es más ancho que el anterior, como, por ejemplo, el vector [3, 3, 1, 1, 1, 1, 5, 5, 5, 5, 5, 5]. 
Utilizando el método de despliegue de recurrencias, analiza la complejidad temporal en el caso 
peor del algoritmo obtenido. 
30. Especifica e implementa un algoritmo recursivo que dado un vector u de enteros obtenga la lon-
gitud de la meseta más larga. Una meseta es un conjunto de posiciones consecutivas del vector que 
contienen el mismo valor. Utilizando el método de despliegue de recurrencias, analiza la comple-
jidad temporal en el caso peor del algoritmo obtenido. 
 
 
 
EDI todo/ejercs T-3.pdf
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Gestión. Curso 2007/2008 
Ejercicios del Tema 3 
Las implementaciones de los TAD se realizarán como clases de C++. En cada caso, se formulará el in-
variante de la representación y las especificaciones pre/post de los procedimientos y funciones que 
realicen las operaciones del TAD. En el caso de los TADs genéricos, la implementación se realizará 
mediante clases parametrizadas (templates o plantillas). 
 
1. Desarrolla un TAD TComplejo que ofrezca el tipo TComplejo junto con las operaciones adecuadas 
para construir números complejos y calcular con ellos. 
2. Desarrolla un TAD TMonomio para representar los monomios como coeficiente y grado, con las 
operaciones de creación, consultar el grado y consultar el coeficiente. 
A continuación, utilizando el TAD TMonomio desarrolla un TAD TPolinomio que ofrezca el tipo de 
los polinomios, con operaciones para crear el polinomio nulo, añadir un nuevo monomio a un 
polinomio, consultar el coeficiente de un determinado grado, consultar el grado del polinomio, 
sumar polinomios y evaluar un polinomio para un valor dado de la indeterminada. 
P ara el TAD TPolinomio construye dos implementaciones: 
— representando los polinomios mediante un vector de monomios ordenados por el grado; y 
— representando los polinomios mediante una lista enlazada de monomios ordenados por el gra-
do. 
3. ñade a la clase TPilaDinamica<TElem> dos nuevas operaciones: A 
— fondo: devuelve el elemento del fondo de una pila no vacía. 
— inversa: devuelve una nueva pila con los elementos de la pila original apilados en orden inverso. 
4. Las pilas formadas por números naturales del intervalo [0..N–1] (para cierto N ≥ 2 fijado de an-
temano) se pueden representar por medio de números, entendiendo que un número natural P 
cuya representación en base N tenga “1” como dígito de mayor peso representa la pila formada 
por los restantes dígitos de la representación de P en base N (excepto el de mayor peso), siendo 
la cima el dígito de menor peso. Por ejemplo, si N = 10, el número 1073 representa la pila que 
contiene los números 0, 7, 3, con 0 en la base y 3 en la cima. Implementa el TAD TPilaNat con 
esta representación. Comprueba que esta representación permite implementar todas las operacio-
nes de las pilas con coste O(1). 
5. Desarrolla un TAD genérico TDosPilas<TElem> que representa una pareja de pilas que pueden 
anejarse independientemente del modo usual. m 
— Desarrolla una implementación estática de este TAD, representando la pareja de pilas con 
ayuda de un único vector de almacenamiento. Organiza la implementación de modo que las 
dos pilas crezcan en sentidos opuestos, cada una desde uno de los dos extremos del vector. 
— Desarrolla una segunda implementación utilizando la clase TPilaDinamica<TElem>. 
6. Especifica e implementa un TAD genérico TLista<TElem> que represente a las listas de elemen-
tos con las siguientes operaciones: 
— Nuevo: crea una lista vacía. 
— Cons: añade un elemento al principio de la lista. 
— ponDr: añade un elemento al final de la lista. 
— primero: devuelve el primer elemento de la lista. 
— resto: elimina el primer elemento de la lista. 
— ultimo: devuelve el último elemento de la lista. 
— inicio: elimina el último elemento de la lista. 
— concatena: añade al final de una lista los elementos de otra. 
 1 
 2 
— esVacio: determina si la lista está vacía. 
— numElem: devuelve el número de elemento de la lista 
— elemEn: devuelve el elemento que está en una cierta posición de la lista, identificada por su 
número de orden. 
La lista se debe implementar como una lista enlazada. 
7. xplica razonadamente qué mostrará este programa por pantalla. E 
typedef TPilaDinamica< TPilaDinamica<int> > TPilas; 
typedef TPilaDinamica< TPilaDinamica<int>* > TPuntPilas; 
 
 
TPilas pilas ( int max ) { 
 TPilas res; 
 TPilaDinamica<int> pila; 
 
 for ( int i = 0; i < max; i++ ) { 
 pila = TPilaDinamica<int>(); 
 for ( int j = 0; j < i; j++ ) 
 pila.apila(j); 
 res.apila(pila); 
 } 
 
 return res; 
} 
 
 
int suma ( TPilas pilaDePilas ) { 
 TPilaDinamica< int > pila; 
 int res; 
 
 res = 0; 
 while ( ! pilaDePilas.esVacio() ) { 
 pila = pilaDePilas.cima(); 
 pilaDePilas.desapila(); 
 while ( ! pila.esVacio() ) { 
 res += pila.cima(); 
 pila.desapila(); 
 } 
 } 
 
 return res; 
} 
 
TPuntPilas pilas2 ( int max ) { 
 TPuntPilas res; 
 TPilaDinamica<int>* pila; 
 
 for ( int i = 0; i < max; i++ ) { 
 pila = new TPilaDinamica<int>(); 
 for ( int j = 0; j < i; j++ ) 
 pila->apila(j); 
 res.apila(pila);
} 
 
 return res; 
} 
 
 
int suma2 ( TPuntPilas pilaDePilas ) 
{ 
 TPilaDinamica< int >* pila; 
 int res; 
 
 res = 0; 
 while ( ! pilaDePilas.esVacio() ) { 
 pila = pilaDePilas.cima(); 
 pilaDePilas.desapila(); 
 while ( ! pila->esVacio() ) { 
 res += pila->cima(); 
 pila->desapila(); 
 } 
 } 
 
 return res; 
} 
 
 
 
int main(int argc, char* argv[]) 
{ 
 TPilas pilaDePilas; 
 TPuntPilas pilaDePilas2; 
 
 pilaDePilas = pilas( 4 ); 
 cout << suma( pilaDePilas ) + suma( pilaDePilas ) << endl; 
 
 pilaDePilas2 = pilas2( 4 ); 
 cout << suma2( pilaDePilas2 ) + suma2( pilaDePilas2 ) << endl; 
} 
		Estructuras de Datos y de la InformaciónIngeniería Técnica en Informática de Gestión. Curso 2007/2008Ejercicios del Tema 3
EDI todo/ejercs T-4.pdf
 1 
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Gestión. Curso 2007/2008 
Ejercicios del Tema 4 
1. Transforma a iterativo los siguiente algoritmos recursivos: 
 (a) Función log del ejercicio 2 de la hoja 2. 
 (b) Procedimiento dosFib del ejercicio 5 de la hoja 2. 
 (c) Función del ejercicio 17 de la hoja 2. 
Aplica en cada caso la técnica de transformación que conlleve un menor consumo de espacio en 
el algoritmo iterativo. 
2. Una frase se llama palíndroma si la sucesión de caracteres obtenida al recorrerla de izquierda a de-
recha (ignorando los blancos) es la misma que si el recorrido se hace de derecha a izquierda. Esto 
sucede, por ejemplo, con la socorrida frase “dábale arroz a la zorra el abad”. Construye una fun-
ción iterativa ejecutable en tiempo lineal, que decida si una frase dada en forma de cola de caracteres 
es o no palíndroma. El algoritmo utilizará una pila de caracteres, declarada localmente. 
3. El agente 0069 ha inventado un nuevo método de codificación de mensajes secretos. El mensaje 
original X se codifica en dos etapas: 
— Primero, X se transforma en X’ reemplazando cada sucesión de caracteres consecutivos que 
no sean vocales por su imagen especular. 
— A continuación, X’ se transforma en la sucesión de caracteres X’’ obtenida al ir tomando suce-
sivamente: el primer carácter de X’; luego el último; luego el segundo; luego el penúltimo; etc. 
Ejemplo: para X = “Bond, James Bond”, resultan: 
X’ = “BoJ ,dnameB sodn” y X’’ = “BnodJo s, dBneam” 
Construye los algoritmos de codificación y descodificación de mensajes y analiza su complejidad, 
utilizando pilas y colas. Supón que el mensaje inicial viene dado como una cola de caracteres. 
4. Añade las siguientes operaciones a la clase TListaDinamica<TElem>: 
— ==: comparación sobre igualdad entre listas 
— <=: comparación de orden entre listas 
— ordenada: determina si una lista está ordenada 
— insertaOrd: inserta un elemento delante del primero que es mayor que él. 
5. Resuelve los problemas que siguen aplicando el esquema de recorrido de secuencias. En cada ca-
so, el contenido de la secuencia debe quedar inalterado. 
 (a) Contar el número de apariciones de ‘a’ en una secuencia de caracteres dada. 
 (b) Dada una secuencia de enteros, contar cuántas posiciones hay en ella tales que el entero que 
aparece en esa posición es igual a la suma de todos los precedentes. 
6. Resuelve los problemas que siguen aplicando el esquema de búsqueda secuencial. 
 (a) Buscar la primera aparición de ‘b’ en una secuencia de caracteres dada. 
 (b) Dada una secuencia de enteros, buscar la primera posición ocupada por un número que sea 
menor que todos los anteriores. 
7. Algunos problemas de procesamiento de secuencias requieren modificar y/o combinar los es-
quemas de recorrido y búsqueda secuencial. Resuelve los casos siguientes: 
 (a) Dada una secuencia de caracteres, copiarla en otra eliminando los blancos múltiples. 
 (b) Contar el número de caracteres anteriores y posteriores a la primera aparición de ‘a’ en una 
secuencia de caracteres dada. 
 (c) Contar el número de parejas de vocales consecutivas que aparecen en una secuencia de ca-
racteres dada. 
 2 
8. Una expresión aritmética construida con los operadores binarios ‘+’, ‘–’, ‘*’, ‘/’ y operandos (re-
presentados cada uno por un solo carácter) se dice que está en forma postfija si es o bien un solo 
operando o dos expresiones en forma postfija una tras otra, seguidas inmediatamente de un ope-
rador. Lo que sigue es un ejemplo de una expresión escrita en la notación infija habitual, junto 
con su forma postfija: 
 Forma infija: (A/(B–C))*(D+E)) Forma postfija: ABC–/DE+* 
Diseña un algoritmo iterativo que calcule el valor de una expresión dada en forma postfija por el 
siguiente método: se inicializa una pila vacía de números y se van recorriendo de izquierda a dere-
cha los caracteres de la expresión. Cada vez que se pasa por un operando, se apila su valor. Cada 
vez que se pasa por un operador, se desapilan los dos números más altos de la pila, se componen 
con el operador, y se apila el resultado. Al acabar el proceso, la pila contiene un solo número, que 
es el valor de la expresión. Representa la expresión dada como secuencia de caracteres, y supón 
disponible una función valor que asocie a cada operando su valor numérico. 
9. Dado un número natural N ≥ 2, se llaman números afortunados a los que resultan de ejecutar el si-
guiente proceso: se comienza generando una cola que contiene los números desde 1 hasta N, en 
este orden; se elimina de la cola un número de cada 2 (es decir, los números 1, 3, 5, etc.); de la 
nueva cola, se elimina ahora un número de cada 3; etc. El proceso termina cuando se va a elimi-
nar un número de cada m y el tamaño de la cola es menor que m. Los números que queden en la 
cola en este momento son los afortunados. Diseña un procedimiento que reciba N como pará-
metro y produzca una secuencia formada por los números afortunados resultantes. 
(Indicación: para eliminar de una cola de n números un número de cada m, hay que reiterar n veces 
el siguiente proceso: extraer el primer número de la cola, y añadirlo al final de la misma, salvo si le 
tocaba ser eliminado.) 
10. Añade las siguientes operaciones a la clase TSecuenciaDinamica<TElem>: 
— busca: operación que busca una secuencia s’ dentro de otra secuencia s. La búsqueda se realiza 
en s, a partir de la posición actual del punto de interés, y los elementos a buscar son aquellos 
que aparecen a la derecha del punto de interés de s’. Esta operación modifica el punto de inte-
rés de s, colocándolo delante de la primera aparición de la subsecuencia buscada, o al final del 
todo si la subsecuencia no se ha encontrado. La operación no está definida si el punto de inte-
rés de s’ está a la derecha del todo –no hay nada que buscar–. 
Por ejemplo, si trabajásemos con secuencias de caracteres: 
 
— borraSec: operación que borra todas las apariciones de una secuencia s’ dentro de otra secuencia 
s. La búsqueda de los elementos a eliminar se realiza de la misma forma que en la operación 
busca, es decir, se busca en s a partir de la posición del punto de interés, y la subsecuencia a 
eliminar es la compuesta por los elementos de s’ a la derecha del punto de interés. Igualmente, 
la operación no está definida si el punto de interés de s’ se encuentra al final. 
11. Implementa el TAD secuencia utilizando como tipo representante una par de listas, según las in-
dicaciones vistas en clase. Para mejorar la eficiencia de algunas operaciones, añade a la 
implementación de las listas una operación de concatenación por la izquierda. 
s : Una casa al borde del mar, un gran mar, en la playa
s’ : La mar
s : Una casa al borde del mar, un gran mar, en la playa
s’ : La mar
Entrada:
Salida:
EDI todo/ejercs T-5.pdf
 1 
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Sistemas. Curso 2007/2008 
Ejercicios del tema 5 
 
Arboles 
1. Añade las siguientes operaciones a la clase TArbinDinamico<TElem>, analizando su complejidad. 
– talla: Arbin[Elem] → Nat 
Calcula la talla de un árbol binario, definida
como el número de nodos de la rama más 
larga. 
– numNodos: Arbin[Elem] → Nat 
Calcula el número de nodos de un árbol binario. 
– numHojas: Arbin[Elem] → Nat 
Calcula el número de hojas de un árbol binario. 
– espejo: Arbin[Elem] → Arbin[Elem] 
Construye la imagen especular de un árbol binario. 
– frontera: Arbin[Elem] → Sec[Elem] 
Obtiene la secuencia formada por los elementos almacenados en las hojas de un árbol 
binario, tomados de izquierda a derecha. 
– ==: (Arbin[Elem], Arbin[Elem]) → Bool 
Determina si dos árboles binarios son iguales. 
2. Un árbol de codificación es un árbol binario a que almacena en cada una de sus hojas un carácter di-
ferente. La información almacenada en los nodos internos se considera irrelevante. Si un cierto 
carácter c se encuentra almacenado en la hoja de posición α, se considera que α es el código asig-
nado a c por el árbol de codificación a. Más en general, el código de cualquier cadena de 
caracteres dada se puede construir concatenando los códigos de los caracteres que la forman, res-
petando su orden de aparición. 
 (a) Dibuja el árbol de codificación correspondiente al código siguiente: 
 
Carácter Código 
‘A’ 1.1 
‘T’ 1.2 
‘G’ 2.1.1 
‘R’ 2.1.2 
‘E’ 2.2 
 
 (b) Construye el resultado de codificar la cadena de caracteres “RETA” utilizando el código 
representado por el árbol de codificación anterior. 
(c) Descifra 1.2.1.1.2.1.2.1.2.1.1 usando el código que estamos utilizando en estos ejemplos, 
construyendo la cadena de caracteres correspondiente. 
(d) Desarrolla un módulo que implemente la clase TArbCod que representa a los árboles de co-
dificación descritos en el ejercicio anterior, equipada con las siguientes operaciones 
– Nuevo: → ArbCod 
Genera un árbol de codificación vacío. 
 2 
 
– Inserta: (ArbCod, Car, Sec[Nat]) – → ArbCod 
Dado un árbol de codificación, un carácter y un código, representado como una secuen-
cia de [1,2], añade el carácter al árbol en el lugar indicado por la secuencia. La operación 
no está definida si el carácter ya formaba parte del árbol. 
– codifica: (ArbCod, Cadena) – → Sec[Nat] 
Construye el código de una cadena. La operación no está definida si la cadena contiene 
algún carácter que no se encuentra codificado en el árbol. 
– decodifica: (ArbCod, Sec[Nat]) – → Cadena 
Construye una cadena a partir de su código. La operación no está definida si no es posi-
ble decodificar toda la entrada. 
3. Modifica las implementaciones desarrolladas en clase para los árboles de búsqueda, de manera 
que las operaciones busca, inserta y borra sean iterativas en lugar de recursivas. 
4. Añade las siguientes operaciones a la clase TArbus<TClave, TValor>, analizando su complejidad. 
– consultaK: (Arbus[Clave,Valor], Nat) → Clave 
Obtiene la k-ésima clave de un árbol de búsqueda, considerando que en un árbol con n 
elementos k=0 corresponde a la menor clave y k=n–1 a la mayor. 
– recorreRango: (Arbus[Clave,Valor], Clave, Clave) → Sec[Valor] 
Dadas dos claves a y b devuelve una secuencia con los valores asociados a las claves que 
están en el intervalo [a .. b ]. 
 
En los dos siguientes ejercicios debes suponer que la clase TArbus<TClave,TValor> está equipada con la 
operación: 
– recorreClaveValor: Arbus[Clave,Valor] → Sec[Pareja[Clave,Valor]] 
Obtiene una secuencia, ordenada por clave, con todas las parejas de clave, valor almacenadas en 
el árbol de búsqueda. 
5. El problema de las concordancias consiste en lo siguiente: Dado un texto, se trata de contar el nú-
mero de veces que aparece en él cada palabra, y producir un listado ordenado alfabéticamente por 
palabras, donde cada palabra aparece acompañada del número de veces que ha aparecido en el 
texto. Suponemos que el texto a analizar viene dado como secuencia de tipo Sec[string], donde ca-
da elemento de la secuencia es una palabra. Se pide construir un algoritmo que resuelva el 
problema con ayuda de un árbol de búsqueda de tipo Arbus[string, int], y analizar su complejidad. 
El listado pedido se dará como secuencia de parejas, de tipo Sec[Pareja[string, int]]. 
6. Dado un texto organizado por líneas, el problema de las referencias cruzadas pide producir un lista-
do ordenado alfabéticamente por palabras, donde cada palabra del texto vaya acompañada de una 
lista de referencias, que contendrá los números de todas las líneas del texto en las que aparece la pa-
labra en cuestión (con posibles repeticiones si la palabra aparece varias veces en una misma línea). 
Suponiendo que el texto a analizar venga dado como secuencia de tipo Sec[Sec[string]] –secuencia 
de líneas representadas como secuencias de palabras–, construye un algoritmo que resuelve el 
problema con ayuda de un árbol de búsqueda de tipo Arbus[string, Sec[int]], y analiza su compleji-
dad. El listado pedido se dará como secuencia de parejas, de tipo Sec[Pareja[string, Sec[int]]]. 
7. Añade a la clase TArbinDinamico<TElem> una operación que determine si un árbol binario es un 
montículo. Analiza su complejidad. 
8. Añade a la clase TArbinDinamico<TElem> una operación maxNivel que obtenga el máximo número 
de nodos de un nivel del árbol, esto es, el número de nodos del “nivel más ancho”. Analiza su 
complejidad. 
 3 
9. Se trata de modificar la implementación de la clase TArbus<TClave,TValor> para permitir que en 
un árbol de búsqueda se puedan almacenar distintos valores con la misma clave, lo cual implica 
cambios en el comportamiento de algunas operaciones de los árboles: 
— Al insertar un par <clave, valor>, si la clave ya se encontrase en el árbol, en lugar de sustituir 
el valor antiguo por el nuevo, se asociará el valor adicional con la clave. 
— La operación de consulta, en lugar de devolver el valor asociado con una clave dada, devol-
verá una secuencia con los valores asociados con dicha clave. La secuencia estará ordenada 
según el orden en el que se insertaron los valores en el árbol. 
— La operación de borrado deberá eliminar todos los valores asociados con la clave dada. 
Implementa en C++: 
— Los cambios necesarios en la estructura de datos para permitir la implementación eficiente 
de las operaciones de este nuevo TAD. En la estructura de datos sólo puedes utilizar tipos 
predefinidos de C++; está prohibido el uso de otros TADs. 
— Las operaciones de inserción y consulta, indicando razonadamente la complejidad temporal 
de las implementaciones obtenidas. 
10. En la clase TArbinDinamico<TElem> añade una constructora con el siguiente perfil 
template <class TElem> 
TArbinDinamico::TArbinDinamico (TElem v[], int num ); 
donde num es el número de elementos del array v. La constructora debe generar un árbol semi-
completo con los elementos del array. Indica razonadamente la complejidad temporal de la 
implementación obtenida. 
EDI todo/ejercs T-6.pdf
 1 
Estructuras de Datos y de la Información 
Ingeniería Técnica en Informática de Gestión. Curso 2007/2008 
Hoja de ejercicios número 6 
Tablas 
1. Se trata de enriquecer la clase TTablaAbierta<TClave,TValor> añadiéndole operaciones de recorri-
do que permitan visitar todos los nodos de la tabla sin importar el orden: 
— reinicia, sitúa el punto de interés al “principio” de la tabla. La especificación no fija cuál ha 
de ser el principio de una tabla. 
— esFin, consulta si el punto de interés está al final de la tabla. Si la tabla está vacía esFin ha de 
ser cierto. 
— actual, devuelve una pareja TPareja<TClave,TValor> con el contenido del nodo situado a 
continuación del punto de interés. Es un error consultar por actual cuando esFin es cierto. 
— avanza, mueve una posición hacia delante el punto de interés. Es un error ejecutar avanza 
cuando esFin es cierto. 
Estas operaciones se deben implementar de forma que un bucle como este 
tabla.reinicia(); 
while ( ! tabla.esFin() ) { 
 cout << tabla.actual(); 
 tabla.avanza(); 
} 
recorra todos los elementos almacenados en la tabla. 
Indica qué cambios sería necesario
realizar en la estructura de datos de la clase TTablaAbier-
ta<TClave,TValor> para poder implementar estas operaciones de manera eficiente, sin que por 
ello se perjudique la complejidad de las otras operaciones de las tablas. Describe cómo implemen-
tarías las nuevas operaciones, qué cambios sería necesario realizar en las otras operaciones de las 
tablas y razona la complejidad temporal que obtendrías. 
2. Se trata de desarrollar un TAD TABLA-ORD[C :: ORD, V :: ANY] que represente a las tablas 
ordenadas. Este TAD ofrecerá las siguientes operaciones: 
— Nuevo: crea una tabla vacía. 
— Inserta: añade una clave y un valor a una tabla. Si en la tabla ya se encontrase la clave insertada, 
se sustituirá el valor antiguo por el nuevo. 
— borra: dada una clave y una tabla, elimina dicha clave, y su valor asociado, de la tabla. Si la clave 
no se encuentra en la tabla, se devolverá la tabla sin modificar. 
— está: determina si una clave se encuentra en una tabla dada. 
— consulta: dada una tabla y una clave, obtiene el valor asociado a dicha clave. Es un error consul-
tar por el valor de una clave que no haya sido insertada previamente. 
— minClave: obtiene la menor clave almacena en la tabla. Es un error consultar por la mínima cla-
ve de una tabla vacía. 
— esVacío: determina si una tabla está vacía. 
— enumera: devuelve una secuencia de pares (clave, valor) con el contenido de la tabla ordenado 
por claves. 
Desarrolla en C++ una clase TTablaOrd<TClave,TValor> que implemente este TAD de forma 
que la complejidad de enumera sea O(n), siendo n el número de datos almacenados en la tabla, y se 
perjudique lo menos posible a la complejidad de las otras operaciones. Analiza la complejidad 
temporal de las operaciones. 
Idea: cada nodo de una lista de colisiones formará parte de dos listas, la propia lista de colisiones 
y una lista ordenada doblemente enlazada que contenga todos los nodos de la tabla. De esta for-
ma, cada nodo contendrá tres punteros: el siguiente en la lista de colisiones, además de el 
siguiente y el anterior en la lista ordenada. 
 2 
3. Se trata de desarrollar un TAD MCJTO[A :: ORD] que represente a los multiconjuntos. Un mul-
ticonjunto es un conjunto donde puede haber elementos repetidos. Este TAD ofrecerá las 
siguientes operaciones: 
— Nuevo: crea un multiconjunto vacío. 
— Inserta: añade un elemento al multiconjunto. 
— borra: elimina un elemento del multiconjunto. 
— está: determina si un elemento pertenece al multiconjunto. 
— min: devuelve el menor elemento de entre los que contiene el multiconjunto. 
— borraMin: elimina una aparición del menor elemento del multiconjunto. 
— esVacío: determina si un multiconjunto está vacío. 
Desarrolla en C++ una clase TMultiCjto<TElem> que implemente este TAD intentando minimi-
zar la complejidad temporal de las operaciones. 
Idea: representa los multiconjuntos como tablas ordenadas utilizando los elementos como claves 
y el número de apariciones como valor. 
4. Resuelve de nuevo el problema de las concordancias (cfr. ejercicio 5 de la hoja 5), utilizando en lugar 
de un árbol de búsqueda una tabla ordenada del tipo que hemos considerado en el ejercicio 2. 
Analiza el tiempo de ejecución del algoritmo que obtengas. 
5. Diseña un procedimiento que “limpie” una tabla cerrada, de tal manera que las posiciones borra-
das se eliminen y las restantes informaciones se reubiquen en la tabla. Todas las informaciones 
correspondientes a claves sinónimas deberán quedar situadas en posiciones consecutivas de la su-
cesión de pruebas, a partir de la posición primaria que corresponda. 
6. Modifica la implementación de la operación de inserción en las tablas cerradas vista en clase para 
que cuando la tasa de ocupación supere un cierto valor fijado de antemano, se aumente la capaci-
dad del array, se reubiquen los datos adecuadamente y se “limpie” la tabla. 
Aplicaciones 
Para los siguientes ejercicios has de suponer que todos los TADs vistos en clase están equipados con 
la operación numElem que devuelve el número de elementos de la estructura en cuestión. 
Los árboles de búsqueda, además de la operación recorre que obtiene una secuencia con los valores 
del árbol, estarán equipados también con estas dos operaciones de recorrido: 
TSecuenciaDinamica<TClave> recorreClave( ) const; 
// Pre : true 
// Post : devuelve las claves del árbol ordenadas 
TSecuenciaDinamica< TPareja<TClave,TValor> > recorreClaveValor( ) const; 
// Pre : true 
// Post : devuelve parejas con las claves y los valores del árbol, ordenadas por clave 
Debes considerar así mismo que las tablas (abiertas y cerradas) están equipadas con las siguientes 
operaciones: 
TSecuenciaDinamica< TPareja<TClave, TValor> > enumera( ) const; 
// Pre: true 
// Post: Devuelve una secuencia de parejas con los elementos de la tabla 
TSecuenciaDinamica<TClave> enumeraClave( ) const; 
// Pre: true 
// Post: Devuelve una secuencia con las claves de la tabla 
TSecuenciaDinamica<TValor> enumeraValor( ) const; 
// Pre: true 
// Post: Devuelve una secuencia con los valores de la tabla 
 3 
 
 
7. En este ejercicio se trata de desarrollar un sistema informático que modele el comportamiento de 
un consultorio médico. Para ello suponemos disponibles los módulos que implementan a las siguien-
tes clases, todas ellas equipadas con operaciones de igualdad y orden: 
— TMedico que sirve para almacenar y gestionar la información sobre un médico del consultorio. 
— TPaciente que sirve para almacenar y gestionar la información sobre un paciente. 
Nosotros nos encargaremos de la implementación de la clase TConsultorio, cuya misión es gestio-
nar la información sobre los médicos y las citas de los pacientes. Esta clase ofrece las siguientes 
operaciones públicas: 
— Nuevo: Genera un consultorio vacío sin ninguna información. 
— NuevoMédico: Altera un consultorio, dando de alta a un nuevo médico que antes no figuraba en 
el consultorio. 
— PideConsulta: Altera un consultorio, haciendo que un paciente se ponga a la espera para ser 
atendido por un médico, el cual debe estar dado de alta en el consultorio. 
— siguientePaciente: Consulta el paciente a quien le toca el turno para ser atendido por un médico; 
éste debe estar dado de alta, y debe tener algún paciente que le haya pedido consulta. 
— atiendeConsulta: Modifica un consultorio, eliminando el paciente al que le toque el turno para 
ser atendido por un médico; éste debe estar dado de alta, y debe tener algún paciente que le 
haya pedido consulta. 
— tienePacientes: Reconoce si hay o no pacientes a la espera de ser atendidos por un médico, el cual 
debe estar dado de alta. 
Desarrolla en C++ una implementación de la clase TConsultorio basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
 
8. En este ejercicio se trata de desarrollar un sistema informático que modele el comportamiento de 
un mercado de trabajo simplificado, donde las personas pueden ser contratadas y despedidas por 
empresas. Para ello suponemos disponibles los módulos que implementan a las siguientes clases, 
todas ellas equipadas con operaciones de igualdad y orden: 
— TPersona que sirve para almacenar y gestionar la información sobre una persona. 
— TEmpresa que sirve para almacenar y gestionar la información sobre una empresa. 
Nosotros nos encargaremos de la implementación de la clase TMercado, cuya misión es gestionar 
la información sobre el mercado de trabajo. Esta clase ofrece las siguientes operaciones públicas: 
— Nuevo: Genera un mercado vacío, sin ninguna información. 
— Contrata: Altera un mercado, efectuando la contratación de cierta persona como empleado de 
cierta empresa. 
— despide: Altera un mercado, efectuando el despido de cierta persona que era antes empleado de 
cierta empresa. 
— empleados: Consulta los empleados de una empresa, devolviendo el resultado como secuencia 
ordenada de personas. 
— esEmpleado: Averigua si es cierto o no que una persona dada es empleado
de una empresa dada. 
— esPluriempleado: Averigua si es cierto o no que una persona es empleado de más de una empre-
sa. 
Desarrolla en C++ una implementación de la clase TMercado basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
 4 
9. Nos han encargado desarrollar un sistema informático que se ocupe del almacenamiento y la ges-
tión de citas literarias. La clase TCultura será la encargada de gestionar la información de una 
colección de citas, ofreciendo a sus clientes las siguientes operaciones públicas: 
— Nuevo: inicializa una colección de citas vacía. 
— Inserta: añade una cita a una colección de citas. Una cita viene dada como una cadena (string) 
y se puede suponer que no se insertarán citas repetidas. 
— elimina: dada una secuencia de palabras, que se pueden suponer no repetidas, elimina las ci-
tas que contengan todas esas palabras. 
— consulta: dada una secuencia de palabras, que se pueden suponer no repetidas, devuelve una 
secuencia sin repetición con las citas que contienen dichas palabras. 
— consultaAprox: dada una secuencia de palabras no repetidas y un número n menor o igual 
que la longitud de la secuencia, devuelve una secuencia sin repetición con las citas que con-
tienen al menos n palabras de la consulta. 
Desarrolla en C++ una implementación de la clase TCultura basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
Idea: se puede utilizar una tabla donde cada palabra (clave) lleva asociada la lista de citas donde 
esa palabra aparece (valor). Para evitar repetir las citas se pueden utilizar punteros. 
 
10. Nos han encargado desarrollar un sistema informático que se ocupe de la gestión de las notas de 
los alumnos de la Escuela de Informática de San Petersburgo. Para ello suponemos disponibles 
los módulos que implementan a las siguientes clases: 
— TAsignatura, que sirve para almacenar y gestionar la información sobre una asignatura: el 
nombre de la asignatura, el profesor, el curso en el cual se imparte, …. Suponemos que está 
equipado con operaciones de igualdad y orden. 
— TAlumno, que sirve para almacenar y gestionar la información sobre un alumno: su nombre, 
DNI, domicilio, …. Suponemos también que está equipado con operaciones de igualdad y 
orden. 
— TNota, que sirve para representar las calificaciones obtenidas por los alumnos en las asigna-
turas: sin calificar, no presentado, suspenso, aprobado, notable y sobresaliente. 
Nosotros nos encargaremos de implementar la clase TTablon que ofrece las siguientes operaciones 
públicas: 
— Nuevo: inicializa un Tablon vacío. 
— InsertaAsignatura: añade a un Tablon una asignatura, junto con la lista –o secuencia– de los 
alumnos matriculados en ella. 
— Califica: añade a un Tablón la información sobre la nota de un alumno en una asignatura de-
terminada. 
— listaNotas: devuelve una secuencia ordenada con los alumnos que han aprobado una asigna-
tura, junto con sus notas correspondientes. 
— pendientes: devuelve una secuencia con las asignaturas que un determinado alumno no ha 
aprobado todavía. 
— junio: modifica un Tablon eliminando de él la información relativa a las asignaturas aproba-
das por los alumnos. De esta forma, en el Tablon sólo quedará información sobre las 
asignaturas pendientes de los alumnos. 
Desarrolla en C++ una implementación de la clase TTablon basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
 5 
11. Nos han encargado desarrollar un sistema informático que se ocupe de la gestión de una tienda 
que vende libros por Internet. Para ello suponemos disponibles los módulos que implementan a 
las siguientes clases, todas ellas equipadas con operaciones de igualdad y orden: 
— TCliente, que sirve para almacenar y gestionar la información sobre un cliente: su nombre, 
dirección, …. 
— TLibro, que sirve para almacenar y gestionar la información sobre un libro: su título, autor, 
tema, ISBN, …. 
— TPais, que sirve para representar los países a los que pertenecen los clientes. Suponemos 
que el TCliente está equipada con la observadora país que devuelve el país donde reside el 
cliente. 
— TTema, que sirve para representar los temas de los libros. Suponemos que la clase TLibro 
está equipada con la observadora tema que devuelve el tema al que pertenece el libro. 
Nosotros nos encargaremos de la implementación de la clase TTienda, cuya misión es gestionar la 
información sobre los libros disponibles y sobre los pedidos de los clientes. Esta clase ofrece las 
siguientes operaciones públicas: 
— Nuevo: inicializa una Tienda vacía. 
— InsertaLibro: añade a una Tienda un número determinado de ejemplares de un cierto libro. El li-
bro podía estar ya disponible, con lo que esta operación sólo modificará el número de 
ejemplares, o puede que se trate de un libro nuevo. 
— Pedido: esta operación añade a una Tienda la información de un pedido que ha realizado un 
cliente determinado. La información sobre el pedido está dada como una secuencia de libros. 
Aunque el envío de los libros no se realiza automáticamente al recibirse el pedido, los libros 
solicitados se consideran reservados, por lo que esta operación se deberá encargar de actualizar 
adecuadamente el número de ejemplares disponibles. También puede ocurrir que no haya exis-
tencias de alguno de los libros solicitados, en cuyo caso se retendrá la información sobre el 
pedido del libro o libros en cuestión, para así poder atenderlo en cuanto lleguen más ejempla-
res. Al insertarse nuevos ejemplares de un libro agotado para el que existen pedidos 
pendientes, dichos ejemplares se asignarán respetando el orden en el que se realizaron los pe-
didos. 
— envíoLibros: esta operación consulta y modifica una Tienda para obtener la información sobre 
los pedidos pendientes de envío que tienen como destino a un país determinado, y darlos por 
realizados. Esta operación devuelve una secuencia donde cada elemento contiene los datos de 
un cliente y la secuencia de libros que ese cliente ha pedido (y tiene reservados). Nótese que 
entre los libros a enviar no se pueden incluir aquellos pedidos que están pendientes debido a la 
falta de ejemplares. Nótese asimismo que entre dos envíos sucesivos al mismo país, cada clien-
te puede haber realizado más de un pedido. 
— envíoCatálogo: periódicamente, la librería edita catálogos con las novedades editoriales de un 
cierto tema. Esta operación sirve para consultar una Tienda y obtener la lista de clientes intere-
sados en el tema en cuestión. Se considera que un cliente está interesado en un cierto tema si 
ha comprado más de minLibros libros de dicho tema. 
Desarrolla en C++ una implementación de la clase TTienda basada en otros TADs conocidos, op-
timizando la complejidad temporal de las operaciones. 
 6 
12. Nos han encargado desarrollar un sistema informático que se ocupe de la gestión de los pedidos 
en la pizzería la Pizza Veloz. Para ello suponemos disponibles los módulos que implementan a las 
siguientes clases: 
— TZona, que sirve para representar una zona de la ciudad, y que se utiliza para organizar los en-
víos por zonas. 
— TDireccion, que sirve para representar una dirección concreta. Suponemos que TDireccion ofrece 
una operación zona que devuelve un objeto de tipo TZona con la zona a la que pertenece dicha 
dirección. 
— TPizza, que sirve para representar un tipo de pizza de entre las que se preparan en la Pizza Ve-
loz. 
— THora, que sirve para representar una hora del día. 
— TPedido, que sirve para representar la información sobre un pedido y que está equipado con las 
operaciones: 
— direccion, que devuelve un objeto TDireccion con el destino del envío. 
— cargamento, que devuelve una secuencia de objetos TPizza con las pizzas a enviar. 
— hora, que devuelve la hora en la que se realizó el pedido. 
Nosotros nos encargaremos de la implementación de la clase TPizzeria, cuya misión es
gestionar 
la información sobre los pedidos pendientes. En concreto, esta clase estará equipada con las si-
guientes operaciones públicas: 
— Nuevo: inicializa una TPizzeria vacía. 
— Inserta: añade a una TPizzeria un nuevo pedido (TPedido). 
— comanda: esta operación proporciona la información necesaria para que un motorista cargue su 
moto y atienda a un cierto número de pedidos. El resultado de esta operación será una se-
cuencia de pares <dirección, secuencia de pizzas> que se obtendrá de la siguiente forma: 
— el pedido pendiente más antiguo pasa directamente a formar parte del resultado, 
— la comanda se completa con otros pedidos pendientes de la misma zona que el primero, te-
niendo en cuenta que: (1) se han de atender antes a los pedidos más antiguos, (2) no se 
debe superar el número de maxPizzas que caben en una moto, y (3) no se pueden fraccionar 
pedidos. De esta forma, se van considerando sucesivamente, por orden cronológico, los 
pedidos pendientes a la zona en cuestión e insertándolos en el resultado siempre que no se 
supere el número de maxPizzas. Este proceso se detiene cuando se ha conseguido exacta-
mente una comanda con maxPizzas o cuando se han considerado todos los pedidos 
pendientes de envío a dicha zona. Se puede suponer, por último, que ningún pedido con-
tiene más de maxPizzas. 
Desarrolla en C++ una implementación de la clase TPizzeria basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
13. Nos han encargado desarrollar un sistema informático que se ocupe de la gestión de la reserva de 
películas en un pequeño video club. Para ello suponemos disponibles los módulos que implemen-
tan a las siguientes clases, todas ellas equipadas con operaciones de igualdad y orden: 
— TPelícula, que sirve para representar una de las películas del video club. 
— TCliente, que sirve para representar un socio del video club. 
Nosotros nos encargaremos de la clase TReservas, cuya misión es gestionar la información sobre 
las películas reservadas. Esta clase ofrecerá las siguientes operaciones públicas: 
— nuevo: inicializa un TReservas vacío. 
— reserva: establece que un cliente ha reservado una cierta película. 
— reservasPendientes: consulta cuántas reservas pendientes hay para una cierta película. 
— películasReservadas: consulta qué películas tiene reservadas un cierto cliente. 
— primero: consulta quién es el primer cliente que tiene reservada una cierta película. 
— alquila: elimina la reserva más antigua de una cierta película. 
Desarrolla en C++ una implementación de la clase TReservas basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. 
 7 
14. Nos han encargado desarrollar un sistema informático que se ocupe de las estadísticas sobre las 
medallas obtenidas en las Olimpiadas. Para ello suponemos disponibles los módulos que imple-
mentan a las siguientes clases: 
— TPais, que sirve para representar un país. 
— TDeporte, que sirve para representar un deporte olímpico. 
— TPrueba, que sirve para representar una prueba concreta de un cierto deporte y que está equi-
pada con la operación deporte que devuelve un objeto TDeporte con el deporte al que pertenece 
dicha prueba. 
— TAtleta, que sirve para representar a un atleta de los que participan en las Olimpiadas y que es-
tá equipada con la operación pais que devuelve un objeto TPais con el país al que pertenece el 
atleta. 
Nosotros nos encargaremos de la clase TMedallero, cuya misión es gestionar la información sobre 
el número de medallas obtenido por los países participantes en las Olimpiadas, y de la clase TMe-
dallas que representa el número de medallas obtenido por un país concreto (cuántas de oro, 
cuántas de plata y cuántas de bronce). La clase TMedallero estará equipada con las siguientes ope-
raciones públicas: 
— Nuevo: inicializa un TMedallero vacío. 
— clasificacion: añade a un TMedallero la información sobre la clasificación de una prueba, dada 
como un objeto TPrueba y una secuencia de objetos TAtleta ordenada por el puesto obtenido 
en dicha prueba. 
— medalleroDeporte: para un deporte dado devuelve una secuencia de pares <TPais, TMedallas> con 
los países que han obtenido alguna medalla en dicho deporte, ordenada por el número de me-
dallas. 
— medallero: devuelve una secuencia de pares <TPais, TMedallas> con los países que han obtenido 
alguna medalla, ordenada por el número de medallas. 
Para obtener el orden en el medallero, se consideran primero el número de medallas de oro, si 
éstas coinciden, se comparan entonces las de plata, y si también coinciden éstas, las de bronce. 
Si coinciden el número de medallas de oro, plata y bronce, entonces no importa el orden. 
Desarrolla en C++ una implementación de las clases TMedallas y TMedallero basada en otros 
TADs conocidos, optimizando la complejidad temporal de las operaciones. 
15. Nos han encargado desarrollar un revisor de textos. Nuestro sistema gestionará una colección 
con palabras del castellano y posibles formas de escribirlas mal, suponiendo que una palabra mal 
escrita se corresponde con una única palabra correcta. El proceso de corrección consistirá en, da-
do un texto, revisarlo palabra por palabra, de forma que si la palabra es correcta entonces se deja 
como está, si no es correcta pero sabemos a qué palabra correcta se refiere la sustituimos, y si el 
sistema no la conoce se deja tal cual. 
La clase TCorrector que implementa el sistema ofrecerá las siguientes operaciones públicas: 
— nuevo: inicializa un TCorrector vacío. 
— insertaPalabra: añade una nueva palabra correcta. 
— insertaError: añade una nueva forma de escribir mal una cierta palabra, es decir, asocia una 
nueva palabra incorrecta con una correcta. 
— revisa: dado un texto representado por una secuencia de palabras, le aplica el proceso de co-
rrección antes descrito, devolviendo el texto corregido junto con una secuencia que 
contenga las palabras que el sistema desconoce –no sabe si son correctas o incorrectas–. 
— diccionario: devuelve una secuencia ordenada alfabéticamente que contenga cada palabra co-
rrecta y las formas posibles de escribirla mal. 
Desarrolla en C++ una implementación de la clase TCorrector basada en otros TADs conocidos, 
optimizando la complejidad temporal de las operaciones. En la implementación está prohibido el 
acceso a los tipos representantes de los TADs que se usen. 
Para cada operación, indica la complejidad temporal que has obtenido. 
 8 
16. Nos han encargado desarrollar un sistema informático que gestione un foro de mensajes. En el 
foro hay dos tipos de mensajes: los que inician una “línea de discusión”, es decir, aquellos mensa-
jes nuevos que no responden a un mensaje anterior; y los que responden a un mensaje previo (ya 
sea una respuesta a otra respuesta o una respuesta a un mensaje inicial). 
La clase TForo que implementa el sistema ofrecerá las siguientes operaciones públicas: 
— una constructora sin parámetros que inicialice un TForo vacío. 
— insertaMensaje: añade un mensaje que inicia una línea de discusión. 
— insertaRespuesta: inserta un mensaje a como respuesta directa a un mensaje previo b. 
— iniciales: obtiene los mensajes almacenados en el foro que han iniciado una línea de discu-
sión. 
— respuestas: obtiene las respuestas directas a un mensaje dado. 
— mensajes: obtiene todos los mensajes del foro. 
Escribe la definición de la clase TMensaje (sólo la interfaz) y desarrolla en C++ una implementa-
ción de la clase TForo basada en otros TADs conocidos, optimizando la complejidad temporal de 
las operaciones. Si en la clase TMensaje decides incluir alguna operación no trivial, entonces has de 
implementar también dicha operación. 
Para cada operación, indica la complejidad temporal que has obtenido. 
17. Nos han encargado desarrollar un sistema informático que gestione un sistema de indexación de 
documentos mediante palabras clave relacionadas. Cuando se inserta un documento, se le asocia
una palabra clave que luego permitirá recuperarlo (decimos entonces que el documento está in-
dexado por esa palabra clave). La misma palabra clave puede tener varios documentos asociados. 
Por otra parte, las palabras clave están relacionadas entre sí por relaciones de dependencia, de 
forma que si la palabra clave c1 depende de la palabra clave c2, entonces cada vez que se consulte 
por c1 se deberán recuperar también los documentos asociados con c2. La relación de dependen-
cia es transitiva (si c1 depende de c2 y c2 depende de c3 entonces c1 depende de c3 ) y antisimétrica 
(si c1 depende de c2 entonces c2 no puede depender de c1). 
La clase TGestor que implementa el sistema ofrecerá las siguientes operaciones públicas: 
— una constructora sin parámetros que inicialice un TGestor vacío. 
— insertaClaves: añade una relación de dependencia entre dos claves dadas. No es necesario que 
las claves hayan sido insertadas previamente en el sistema. Se valorará que esta operación 
compruebe que no se producen ciclos en la relación de dependencia entre claves. 
— insertaDocumento: añade un documento con una palabra clave asociada. No es necesario que 
la clave haya sido insertada previamente. 
— consulta: dada una cierta palabra clave, recupera sus documentos asociados junto con los do-
cumentos asociados con las claves de las que depende directa o indirectamente, si es que 
existen. 
Escribe la definición de la clase TDocumento (sólo la interfaz) y, suponiendo que las palabras clave 
se representan como datos de tipo string, desarrolla en C++ una implementación de la clase TGes-
tor basada en otros TADs conocidos, optimizando la complejidad temporal de las operaciones. Si 
en la clase TDocumento decides incluir alguna operación no trivial, entonces has de implementar 
también dicha operación. 
Para cada operación, indica la complejidad temporal que has obtenido. 
 
 
EDI todo/IntroduccionC++.pdf
 
TEMA 0 
INTRODUCCIÓN AL LENGUAJE C++ 
 
1. El programa “Hola mundo” 
2. Tipos simples 
3. Declaración, inicialización y asignación de variables y constantes 
4. Operadores 
5. Tipos definidos por el programador 
6. Estructuras de control 
7. Procedimientos y funciones 
8. Entrada y salida 
 
 
 
 
Bibliografía: El lenguaje de programación C++. Tercera edición 
Bjarne Stroustrup 
Addison Wesley, 1998 
 
 
Introducción al lenguaje C++ 1 
 
0.1 Hola mundo 
 El programa “Hola mundo” en C++ 
 
#include <iostream> 
using namespace std; 
 
void main( ) 
{ 
 cout << "Hola mundo mundial\n"; 
} 
 
 El programa principal es siempre una función de nombre main. 
 
 Las operaciones de entrada/salida se importan de módulos de la biblioteca 
usando la directiva include. En el ejemplo se ha importado iostream. 
— Los módulos de la biblioteca estándar definen sus identificadores en el espa-
cio de nombres std, por lo tanto, si no se quiere cualificar a los identificado-
res es necesario añadir la sentencia 
using namespace std; 
para así poder escribir cout en lugar de std::cout 
 
 Los delimitadores { } marcan, respectivamente, el principio y el final de una 
función y sirven así mismo para delimitar bloques de código. 
 
 El tipo void representa la ausencia de valor. 
En C++ un procedimiento es una función que devuelve un resultado de tipo 
void. 
 
 Todas las sentencias terminan con el carácter ; 
 
 Las cadenas se escriben entre comillas dobles. 
 
 Para mostrar un mensaje por la salida estándar (la pantalla), se aplica el opera-
dor de inserción << teniendo como primer operando cout (importado de ios-
tream)y como segundo operando el dato que se pretende mostrar. 
 
 
Introducción al lenguaje C++ 2 
 
“Hola mundo” (en C++ Builder 5) 
— File | New ... 
— En el cuadro de diálogo New Items se selecciona Console Wizard 
 
 
 
 
— En el diálogo Console Wizard se aceptan las opciones por defecto 
 
 
 
Introducción al lenguaje C++ 3 
 
— Y ya tenemos un programa ejecutable 
//--------------------------------------------------------------------- 
#pragma hdrstop 
//--------------------------------------------------------------------- 
#pragma argsused 
int main(int argc, char* argv[]) 
{ 
 return 0; 
} 
//--------------------------------------------------------------------- 
sin más que seleccionar Run | Run o pulsar F9. 
— Si el programa no se ha compilado previamente, se compilará al seleccionar 
la orden Run. 
Para compilar se utiliza la orden Project | Compile unit (ALT+F9) para compi-
lar el archivo actual o Project | Make nombre_proyecto (CTRL+F9) para compilar 
todos los archivos de un proyecto. 
— Si añadimos a este esqueleto de programa las líneas de nuestro “Hola mun-
do” 
#include <iostream> 
using namespace std; 
 
int main(int argc, char* argv[]) 
{ 
 cout << "Hola mundo\n"; 
 return 0; 
} 
Si ejecutamos ahora este programa, veremos en Windows una consola de 
MS-DOS que se abre y cierra rápidamente. Para que nos dé tiempo a ver el 
mensaje, podemos añadir una operación de lectura 
int main(int argc, char* argv[]) 
{ 
 char c; 
 
 cout << "Hola mundo\n"; 
 cin >> c; 
 return 0; 
} 
 
Introducción al lenguaje C++ 4 
 
0.2 Tipos simples 
 Los tipos simples más utilizados 
 
— char 
caracteres, los literales se escriben entre comillas simples: ’a’ 
 
— int 
números enteros en el intervalo −32768 .. 32767 
 
— double 
números reales en el intervalo 1.7e−308 .. 1.7e+308 
 
— bool 
valores lógicos true | false 
 
— void 
el tipo del resultado de las funciones que no devuelven resultado 
(i.e., los procedimientos) 
 
Introducción al lenguaje C++ 5 
 
0.3 Declaración, inicialización y asignación de variables 
y constantes 
 Sintaxis 
 tipo lista_de_variables; 
donde lista_de_variables se compone de uno o más identificadores separados 
por comas. 
En los identificadores se distinguen mayúsculas de minúsculas. 
 
 Una declaración puede aparecer en cualquier lugar de un programa, su ámbito 
será local si se declara dentro de una función y global si se declara al nivel más 
externo. 
En general siempre declararemos las variables locales al principio del cuerpo 
de las funciones. 
 
 = es el operador de asignación 
 
 Las variables se pueden inicializar al ser declaradas 
 
 Las constantes se declaran como las variables pero precedidas de la palabra re-
servada const 
 
 Ejemplo 
 
int global = 0; // variable global inicializada 
 
void main( ) 
{ 
 int local1, local2 = 2; // variables locales a la función main 
 const int uno = 1, dos = 2; // declaración de constantes 
 
 local1 = uno; // asignación 
 
 bool b; // evitaremos estas declaraciones 
 b = true; 
} 
 
Introducción al lenguaje C++ 6 
 
0.4 Operadores 
0.4.1 Operadores aritméticos y de manipulación de bits 
 
 Actúan sobre constantes y variables numéricas 
— +, –, *, / : suma, resta, multiplicación y división 
— % : módulo, que sólo está definido para los enteros (int o long) 
— &, |, ^, <<, >> : operaciones bit a bit y-lógica, o-lógica, o-exclusiva, despla-
zamiento a la izquierda y desplazamiento a la derecha 
 
 Cada uno de estos operadores lleva asociado un operador de asignación de la for-
ma op= tal que 
 variable op= expresión 
es equivalente a 
 variable = variable op expresión 
 
 Ejemplo 
 
void main( ) 
{ 
 const int tres = 3; 
 int n = 289898454 % tres; 
 double f = 1.34e-6, g = 6.7; 
 double h = f + g * f; 
 
 n += tres; 
} 
 
 
 
 
Introducción al lenguaje C++ 7 
 
0.4.2 Operadores relacionales y lógicos 
 
 Operadores relacionales 
— ==, != : igual, diferente 
— >, >=, <, <= : mayor, mayor o igual, menor, menor o igual 
 
 Operadores lógicos 
— && : conjunción 
— || : disyunción 
— ! : negación 
 
 Ejemplo 
 
void main( ) 
{ 
 double f = 1.34e-6, g = 1.35e-6; 
 char h, j; 
 bool b = ! (f == g) && (f*3.6 <= g); 
} 
 
 
Introducción
al lenguaje C++ 8 
 
0.4.3 Operadores de incremento y decremento 
 
 Permiten incrementar y decrementar el valor de una variable 
— ++ : incrementa en una unidad 
— – – : decrementa en una unidad 
 
 Tienen dos formas de uso 
— pre-incremento/pre-decremento. 
La variable se incrementa/decrementa antes de evaluar la expresión 
— post-incremento/post-decremento. 
La variable se incrementa/decrementa después de evaluar la expresión 
 
 Ejemplo 
 
void main( ) 
{ 
 int x = 1, y, z; 
 
 ++x; // pre-incremento 
 x++; // post-incremento 
 y = ++x; 
 z = y--; 
} 
 
¿Cuál es el valor de las variables después de la última asignación? 
 
 
Introducción al lenguaje C++ 9 
 
0.5 Tipos definidos por el programador 
 
 Para definir un tipo se utiliza la palabra reservada typedef delante 
 de la declaración de tipo y el nombre del tipo 
typedef declaración_tipo nombre_tipo 
 
 No existe una palabra reservada para introducir la zona de definiciones de tipo 
de un programa. 
Tipos enumerados 
 
 Los tipos enumerados se declaran con la palabra reservada enum seguida de una 
lista de etiquetas encerradas entre llaves { } 
 
 Las etiquetas de un tipo enumerado son en realidad constantes con valor ente-
ro, donde la primera etiqueta toma el valor 0, la segunda el 1, ... 
 
 Ejemplo 
 
typedef int TDiaMes; 
typedef enum {lun, mar, mie, jue, vie, sab, dom} TDiaSemana; 
 
void main( ) 
{ 
 TDiaMes diaMes = 1; 
 TDiaSemana diaSemana = lun; 
 
 diaMes += 2; 
 diaSemana += 2; 
} 
 
 
Introducción al lenguaje C++ 10 
 
Registros 
 
 Los registros se declaran con la palabra reservada struct seguida de la lista de 
declaración de los campos encerrada entre llaves { } y donde los campos se 
separan por ; 
 
struct { 
 nombre_tipo1 nombre_campo1; 
 ... 
 nombre_tipoN nombre_campoN; 
} nombre_tipo 
 
 Ejemplo 
 
typedef int TDiaMes; 
typedef enum {lun, mar, mie, jue, vie, sap, dom} TDiaSemana; 
typedef struct { 
 TDiaMes diaMes; 
 TDiaSemana diaSemana; 
} TDia; 
 
void main( ) 
{ 
 TDia dia; 
 
 dia.diaMes = 1; 
 dia.diaSemana = lun; 
} 
 
 
Introducción al lenguaje C++ 11 
 
Arrays 
 
 Para cualquier tipo T se puede declarar el tipo T[num] que representa a los vec-
tores de num elementos de tipo T, con índices en el intervalo 0 .. num –1 
— num ha de ser una expresión constante 
 
 También es posible declarar una variable de tipo array sin necesidad de definir 
un nuevo tipo, con la siguiente sintaxis 
nombre_tipo identificador[num]; 
 
 Los arrays se pueden inicializar en la declaración proporcionando una lista de 
valores, separados por comas y encerrados entre llaves 
— Si la lista de inicialización contiene menos elementos que la dimensión del 
array el resto de posiciones se inicializan a 0 
 
 Los vectores multidimensionales se declaran como arrays de arrays 
 
 Ejemplo 
 
typedef int TMatriz[2][2]; 
 
void main( ) 
{ 
 int lista[5] = {0, 1, 2, 3, 4}; 
 TMatriz matriz = { {1,1}, {2,2} }; 
 
 matriz[0][0] = lista[0]; 
 cout << matriz[0][0] << matriz[0][1] << matriz[1][0] \ 
 << matriz[1][1] << "\n"; 
} 
 
¿qué se mostraría por pantalla? 
 
Introducción al lenguaje C++ 12 
 
0.6 Estructuras de control 
Selección condicional 
 
 Sintaxis 
 
 if ( condición ) sentencia 
 
 if ( condición ) sentencia else sentencia 
 
 Ejemplos 
 
int x, y; 
 
if (x == y) 
 x = y++; 
 
 
int x, y, z; 
 
if (x == y) 
{ x = y++; 
 z = ++x; } 
else 
 z = x + y; 
 
 
int x, y, z; 
 
if (x == y) 
 z = 2; 
else if (x > y) 
{ z = 1; 
 x++; } 
else 
{ z = 0; 
 y++; } 
 
 
Introducción al lenguaje C++ 13 
 
Selección múltiple 
 
 Sintaxis 
 
 switch ( expresión ) 
 case expresión-constante : sentencia 
 ... 
 default : sentencia 
 
— Se compara secuencialmente la expresión con las expresiones-constantes hasta en-
contrar una que coincida. A partir de ahí, se ejecutan todas las sentencias hasta 
llegar al final o hasta encontrar una sentencia break 
 
— Si ninguna expresión constante coincide con el valor de expresión, se ejecuta la 
sentencia de la cláusula default, si es que la hay 
 
 Ejemplo 
 
int x = 1, y; 
 
switch (x) 
{ case 1: 
 y++; 
 break; 
 case 2: 
 x++; 
 break; 
 default: 
 x++; y++; 
 break; 
} 
 
Introducción al lenguaje C++ 14 
 
Composición iterativa 
 
 Sentencia while 
while ( condición ) sentencia 
 
— Ejemplo: 
 
int x = 10, y = 1; 
 
while (x > y) 
{ x--; 
 y++; } 
 
 
 Sentencia do .. while 
do sentencia while ( condición ); 
 
— Ejemplo: 
 
int x = 10, y = 1; 
 
do 
{ x--; 
 y++; } 
while (x > y); 
 
Introducción al lenguaje C++ 15 
 
 Sentencia for 
for ( inicializaciónopcional; condiciónopcional; expresiónopcional ) 
 sentencia 
— Se ejecuta la inicialización y mientras la condición sea cierta, se ejecuta la sentencia 
y a continuación la expresión 
— Es posible incluir varias inicializaciones, condiciones y/o expresiones separándolas 
por comas 
 
 Ejemplos 
— Este fragmento de código 
 
int a[10], indice; 
 
for (indice = 0; indice < 10; indice++) 
 a[indice] = 2*indice; 
 
es equivalente a este otro 
 
int a[10], indice; 
 
indice = 0; 
while ( indice < 10 ) 
{ a[indice] = 2*indice; 
 indice++; } 
 
— Ejemplo de expresiones compuestas 
 
int a[10], indice, valor; 
 
for (indice = 0, valor = 9; indice < 10; indice++, valor--) 
 a[indice] = valor; 
 
 
 
 
 
 
Introducción al lenguaje C++ 16 
 
0.7 Procedimientos y funciones 
 
 Sintaxis 
 
tipo-resultado nombre ( lista-parámetros ) 
{ 
 lista-de-sentencias 
} 
 
 El resultado de una función se devuelve con la sentencia 
 
return expresión; 
 
que además termina la ejecución de la función 
 
 Un procedimiento es una función cuyo tipo de resultado es void 
 
 Ejemplo 
 
int suma ( int x, int y ) 
{ 
 return x + y; 
} 
 
void main( ) 
{ 
 int x = suma(2, 3); 
} 
 
 
Introducción al lenguaje C++ 17 
 
Paso de parámetros 
 
 Por defecto los parámetros se pasan por valor. 
Para indicar que un parámetro formal es por referencia, se escribe el carácter 
& detrás del nombre del tipo 
Los arrays, por razones de eficiencia, se pasan siempre por referencia 
 
 Es posible utilizar como parámetro arrays abiertos con la sintaxis 
nombre-tipo identificador [ ] 
Así se pueden escribir funciones que pueden recibir un array de cualquier lon-
gitud. Para que la función conozca la longitud del array que se pasa como pa-
rámetro real es habitual pasar un segundo argumento con la longitud. 
 
 También es posible definir valores por defecto para los argumentos 
Los argumentos con valor por defecto deben aparecer siempre al final de la lis-
ta de argumentos 
 
 Ejemplo 
void acumula ( int vector[], int longitud, int& suma ) 
{ 
 int indice; 
 
 for( indice = 0; indice < longitud; indice++ ) 
 suma += vector[indice]; 
} 
 
int suma ( int x, int y = 1 ) 
{ 
 return x + y; 
} 
 
void main( ) 
{ 
 int v[5] = {0, 1, 2, 3, 4}; 
 int s = 0; 
 
 acumula( v, suma(4), s); 
} 
 
Introducción al lenguaje C++ 18 
 
0.8 Entrada y salida 
 Una de las posibles formas de realizar entrada/salida en C++ es por medio de 
los flujos o canales –streams– 
— En el módulo iostream se incluyen las funciones e identificadores estándar pa-
ra el manejo de canales 
 
 En iostream se definen tres canales predefinidos 
— cin, para entrada de datos 
— cout, para salida de datos 
— cerror, para salida de errores 
 
 Los canales se manejan con los operadores de inserción y extracción 
— <<, para enviar datos a un canal 
— >>, para obtener datos de un canal 
 
 Los operadores importados de iostream saben cómo manejar datos de los tipos 
predefinidos 
 
 Ejemplo 
void pesado ( ) 
{ 
 const int magico = 123; 
 int intento; 
 char c; 
 
 cout << “Adivina el número mágico: “; 
 cin >>
intento; 
 while ( intento != magico ) { 
 cout << “\nTe equivocaste, “ << intento << “ no es correcto” \ 
 << “\nVuelve a intentarlo: “; 
 cin >> intento; 
 } 
 cout << "¡¡Acetate!!"; 
 cout << "\nPulsa una tecla para continuar"; 
 cin >> c; 
} 
Introducción al lenguaje C++ 19 
 
 
EDI todo/Presentaci?n.pdf
 
 
ESTRUCTURAS DE DATOS 
Y DE LA INFORMACIÓN 
CURSO 07/08 
 
 
Gonzalo Méndez (Primer Cuatrimestre) 
Despacho 420-bis 
gmendez@fdi.ucm.es 
Tutorías: L,X,J,V de 14’30 a 16’00 
 
 
Pedro González Calero (Segundo Cuatrimestre) 
Despacho 417 
pedro@sip.ucm.es 
Tutorías: X,J de 13 a 14 y de 15 a 16 
Presentación 1 
 
Programa 
0. Introducción a C++ 
1. Análisis de algoritmos iterativos 
2. Diseño de algoritmos recursivos 
3. Tipos abstractos de datos 
4. Tipos de datos con estructura lineal 
5. Árboles 
6. Tablas 
7. Grafos 
8. Bibliotecas de estructuras de datos 
 
Presentación 2 
 
Bibliografía 
Básica 
— G. L. Heileman. "Estructuras de datos, algoritmos y progra-
mación orientada a objetos". McGraw-Hill, 1996. 
— E. Horowitz, S. Sahni, D. Mehta. "Fundamentals of Data 
Structures in C++". W H Freeman & Co., 1995. 
Complementaria 
— B. r. Preiss. “Data Structures and Algorithms with Object-
Oriented Design Patterns in C++”. John Wiley & Sons, 
1999. 
— Carrano, Helman y Veroff. "Data Abstraction and Problem 
Solving with C++, Second Edition". Addison-Wesley, 1998. 
— M. Allen Weiss. "Estructuras de datos y algoritmos". Addi-
son-Wesley Iberoamericana, 1995. 
— A.V.Aho, J.E.Hopcroft, J.D.Ullman. "Data Structures and 
Algorithms". Addison Wesley, 1983. 
— G. Brassard, P. Bratley. "Fundamentos de Algoritmia". Pren-
tice Hall, 1997. 
— J. P. Cohoon, J. W. Davidson. "Programación y diseño en 
C++". MacGraw-Hill, 2000 
— X. Franch Gutiérrez. "Estructuras de datos. Especificación, 
diseño e implementación". Ediciones UPC, 1994. 
 
Presentación 3 
 
Método de evaluación 
 
Exámenes 
• Parcial de febrero 
 Primer cuatrimestre 
 
• Final de junio 
 Primer cuatrimestre 
 Segundo cuatrimestre 
 Toda la asignatura 
 
• Final de septiembre 
 Primer cuatrimestre 
 Segundo cuatrimestre 
 Toda la asignatura 
 
 
Los compensables sólo sirven de un examen al 
siguiente 
 
EDI todo/Tema 7-Grafos.pdf
 i 
TEMA 7 
GRAFOS 
 
— Modelo matemático y especificación 
— Técnicas de implementación 
— Matriz de adyacencia 
— Vector de listas/secuencias de adyacencia 
— Vector de multilistas de adyacencia 
— Implementación de las matrices de adyacencia 
— Implementación de las listas de adyacencia 
— Recorridos de grafos 
— Recorrido en profundidad 
— Recorrido en anchura 
— Recorrido de ordenación topológica 
— Caminos de coste mínimo 
— Caminos mínimos con origen fijo 
— Caminos mínimos entre todo par de vértices 
 
 
 
Grafos 1 
 
7.1 Modelo matemático y especificación 
 Un grafo está compuesto por un conjunto de vértices (o nodos) y un conjunto de 
aristas (arcos en los grafos dirigidos) que definen conexiones entre los vértices. 
A partir de esta idea básica, se definen distintos tipos de grafos:  
— Dirigidos o no dirigidos. 
— Valorados y no valorados. 
— Simples o multigrafos. 
 
 Un grafo se puede modelar como una pareja formada por un conjunto de vér-
tices y un conjunto de aristas:  
G = (V, A)      A ⊆ V × V   
Otro modelo para los grafos consiste en representarlos como un aplicación:  
g : V × V → Bool     para los grafos no valorados  
g : V × V → W        para los grafos valorados 
 
 Algunos conceptos ya conocidos sobre grafos  
— Adyacencia, incidencia. 
— Grado –de entrada y de salida– de un vértice. 
— Relaciones entre el número de vértices NV y el de aristas NA. 
— Grafo completo: existe una arista entre todo par de vértices 
— dirigido: na = nv ⋅ (nv – 1) = nv2 – nv 
— no dirigido: na = (nv2 – nv) / 2 
— Un camino es una sucesión de vértices v0 , v1 , … , vn tal que para 1 ≤ i ≤ n 
(vi–1 , vi ) es una arista. La longitud del camino es n. 
— Camino abierto: vn ≠ v0 
— Circuito, camino cerrado o circular: vn = v0 
— Camino simple: no repite vértices (excepto quizá v0 = vn) 
— Camino elemental: no repite arcos 
— Ciclo: camino circular simple 
— Grafo conexo: existe un camino entre cada 2 vértices 
Grafos 2 
 
Especificación algebraica 
 
 Los detalles de la especificación cambian según la clase de grafos a especificar. 
En el caso más general de los grafos valorados, necesitamos especificar los gra-
fos con dos parámetros: el tipo de los vértices y el tipo de los valores de los ar-
cos. 
 
 A los vértices les exigimos que pertenezcan a la clase de los tipos discretos, 
que sirve como generalización de los tipos que se pueden utilizar como índices 
de los vectores. 
 
clase DIS 
  hereda 
    ORD 
  operaciones 
    card: → Nat 
    ord: Elem → Nat 
    elem: Nat – → Elem 
    prim, ult: → Elem 
    suc, pred: Elem – → Elem 
  axiomas 
    ∀ x, y : Elem : ∀ i : Nat :  
    card ≥ 1 
    1 ≤ ord(x) ≤ card 
    def elem(i) si 1 ≤ i ≤ card 
    ord(elem(i))  =d i 
    elem(ord(x))  = x 
    prim = elem(1) 
    ult = elem(card) 
    def suc(x) si x /= ult 
    suc(x)   =d elem(ord(x) – 1) 
    x == y  = ord(x) == ord(y) 
    x ≤ y  = ord(x) ≤ ord(y) 
fclase 
 
 
La razón de exigir esta condición a los vértices de los grafos está en que algu-
nos algoritmos sobre grafos utilizan vectores indexados por vértices. 
 
 
Grafos 3 
 
 En cuanto a las etiquetas de los arcos, les exigimos que pertenezcan a un tipo 
ordenado, que exporte una operación de suma, y que incluya el valor NoDef, 
que renombramos a ∞, y el valor 0. La razón de estas restricciones está, otra 
vez, en los algoritmos que queremos implementar sobre los grafos 
 
clase VAL‐ORD 
  hereda 
    EQ–ND, ORD 
  renombra 
    Elem a Valor 
    Nodef a ∞ 
  operaciones 
    0: → Valor 
    ( + ): (Valor, Valor) → Valor 
  axiomas 
    ∀ x, y, z : Valor : 
    0 ≤ x 
    x ≤ ∞ 
    x + y   = y + x 
    (x + y) + z   = x + (y + z) 
    x + 0   = 0 
    x + ∞  = ∞ 
fclase 
 
Grafos 4 
 
 En la especificación de los grafos incluimos las siguientes operaciones: 
— Vacío construye un grafo vacío 
— PonArista añade una arista, indicando los vértices que une, y su coste 
— quitaArista quita la arista que une a dos vértices dados 
— costeArista obtiene el coste de la arista que une a dos vértices dados 
— hayArista? consulta si existe una arista que une a dos vértices dados 
— sucesores obtiene los vértices sucesores de uno dado 
— predecesores obtiene los vértices predecesores de uno dado  
No prohibimos que haya bucles, con origen y destino en el mismo vértice. 
Especificamos PonArista de forma que no pueda haber dos aristas entre los 
mismos vértices; en la especificación de los multigrafos habría que suprimir 
esta restricción. 
 
 
tad WDGRAFO[V :: DIS, W :: VAL‐ORD]  
  renombra 
    V.Elem a Vértice  
  usa 
    BOOL, SEC[PAREJA[V, W]]  
  usa privadamente 
    CJTO[PAREJA[V, W]]  
  tipo 
    DGrafo[Vértice, Valor] 
 
  operaciones 
    Vacío: → DGrafo[Vértice, Valor]    /* gen */  
    PonArista:  (DGrafo[Vértice, Valor], Vértice, Vértice, Valor) 
          → DGrafo[Vértice, Valor]    /* gen */  
    quitaArista: (DGrafo[Vértice, Valor], Vértice, Vértice) 
          → DGrafo[Vértice, Valor]    /* mod */  
    costeArista: (DGrafo[Vértice, Valor], Vértice, Vértice) → Valor  /* obs */  
    hayArista?:  (DGrafo[Vértice, Valor], Vértice, Vértice) → Bool  /* obs */  
    sucesores: (DGrafo[Vértice, Valor], Vértice)  
          → Sec[Pareja[Vértice, Valor]]    /* obs */  
    predecesores: (DGrafo[Vértice, Valor], Vértice)  
          → Sec[Pareja[Vértice, Valor]]    /* obs */ 
 
 
   
Grafos 5 
 
operaciones privadas  
    cjtoSuc: (DGrafo[Vértice, Valor], Vértice)  
          → Cjto[Pareja[Vértice, Valor]]    /* obs */  
    cjtoPred: (DGrafo[Vértice, Valor], Vértice)  
          → Cjto[Pareja[Vértice, Valor]]
/* obs */  
    enumera: Cjto[Pareja[Vértice, Valor]]  
          → Sec[Pareja[Vértice, Valor]]    /* obs */  
    insertaOrd: (Pareja[Vértice, Valor], Sec[Pareja[Vértice, Valor]] 
          → Sec[Pareja[Vértice, Valor]]    /* obs */ 
 
  ecuaciones 
    ∀ g : DGrafo[Vértice, Valor] : ∀ u, u1, u2, v, v1, v2 : Vértice :  
    ∀ c, c1, c2 : Valor : ∀ ps : Sec[Pareja[Vértice, Valor]] : 
    ∀ xs : Cjto[Pareja[Vértice, Valor]] : 
  
    PonArista(PonArista(g, u1, v1, c1), u2, v2, c2 )   = PonArista(g, u2, v2, c2 ) 
      si u1 == u2 AND v1 == v2   
    PonArista(PonArista(g, u1, v1, c1), u2, v2, c2 )  
      = PonArista(PonArista(g, u2, v2, c2), u1, v1, c1 ) 
      si NOT (u1 == u2 AND v1 == v2 ) 
 
    quitaArista(Vacío, u, v)    = Vacío   
    quitaArista(PonArista(g, u1, v1, c), u2, v2)  = quitaArista(g, u2, v2) 
        si u1 == u2 AND v1 == v2   
    quitaArista(PonArista(g, u1, v1, c), u2, v2)   
      = PonArista(quitaArista(g, u2, v2), u1, v1, c) 
        si NOT (u1 == u2 AND v1 == v2 ) 
 
    costeArista(Vacío, u, v)    = ∞   
    costeArista(PonArista(g, u1, v1, c), u2, v2)  = c 
        si u1 == u2 AND v1 == v2   
    costeArista(PonArista(g, u1, v1, c), u2, v2)  = costeArista(g, u2, v2) 
        si NOT (u1 == u2 AND v1 == v2 ) 
 
    hayArista?(g, u, v)    = costeArista(g,u,v) /= ∞ 
 
    sucesores(g, u)    = enumera(cjtoSuc(g, u)) 
 
    predecesores(g, v)    = enumera(cjtoPred(g, v)) 
 
 
   
Grafos 6 
 
% enumera devuelve una secuencia con parte izquierda vacía 
    enumera(CJTO.Vacío)    = SEC.Crea   
    enumera(Pon(Par(v, c)), xs)) 
      = insertaOrd(Par(v, c), enumera(quita(Par(v, c), xs))) 
 
  % insertaOrd devuelve una secuencia con parte izquierda vacía 
    insertaOrd(Par(v, c), ps) 
      = reinicia(inserta(Par(v, c), ps)) 
        si fin?(ps)   
    insertaOrd(Par(v, c), ps) 
      = reinicia(inserta(Par(v, c), ps)) 
        si NOT fin?(ps) AND actual(ps) = Par(v1, c1) AND v < v1   
    insertaOrd(Par(v, c), ps) 
      = insertaOrd(Par(v, c), avanza(ps)) 
        si NOT fin?(ps) AND actual(ps) = Par(v1, c1) AND v ≥ v1 
 
    cjtoSuc(Vacío, u)    = CJTO.Vacío   
    cjtoSuc(PonArista(g, u1, v, c), u) 
      = Pon(Par(v, c), cjtoSuc(quitaArista(g, u, v), u)) 
        si u == u1   
    cjtoSuc(PonArista(g, u1, v, c), u)  = cjtoSuc(g, u) 
        si u /= u1 
 
    cjtoPred(Vacío, v)    = CJTO.Vacío   
    cjtoPred(PonArista(g, u, v1, c), v) 
      = Pon(Par(u, c), cjtoPred(quitaArista(g, u, v), v)) 
        si v == v1   
    cjtoPred(PonArista(g, u, v1, c), v)  = cjtoPred(g, v) 
        si v /= v1 
 
ftad 
 
Grafos 7 
 
7.2 Técnicas de implementación 
Matriz de adyacencia 
 
 El grafo se representa como una matriz bidimensional indexada por vértices. 
En los grafos valorados, en la posición (i, j ) de la matriz se almacena el peso de 
la arista que va del vértice i al vértice j, ∞ si no existe tal arista.  
Por ejemplo: 
 
 
 A B C D 
A ∞ 1 5 2 
B ∞ ∞ ∞ 3 
C 2 ∞ ∞ 2 
D ∞ 4 ∞ ∞ 
 
Vector de listas/secuencias de adyacencia 
 
 Se representa como un vector indexado por vértices, donde cada posición del 
vector contiene la lista de aristas que parten de ese vértice –la lista de suceso-
res–, representadas como el vértice de destino y la etiqueta de la arista. En el 
grafo del ejemplo anterior: 
 
 
Grafos 8 
 
Vector de multilistas de adyacencia 
 
 Se representa el grafo como un vector indexado por vértices, donde cada posi-
ción del vector contiene dos listas: una con las aristas que inciden en ese vérti-
ce –lista de predecesores–, y otra con las aristas que parten de él –lista de 
sucesores–. La representación de cada arista se compone de el vértice de parti-
da, el de destino y el peso. En el grafo del ejemplo anterior: 
 
 
 
 
 
En este caso no se puede realizar una implementación modular que importe 
las listas/secuencias de otro módulo; hay que construir directamente un tipo 
representante usando registros y punteros. 
Grafos 9 
 
Variantes 
 
 Grafos no dirigidos. 
— La matriz de adyacencia es triangular y admite una representación optimi-
zada. 
— En las listas de adyacencia puede ahorrarse espacio suponiendo un orden 
entre vértices y poniendo v en la lista de u sólo si u < v –con lo que nos 
ahorramos representar dos veces la misma arista–. 
 
 Grafos no valorados. 
— La matriz de adyacencia puede ser de booleanos 
— Las listas de adyacencia se reducen a listas de vértices. 
 
 Multigrafos. 
— Las matrices de adyacencia no son una representación válida. 
— Las listas de adyacencia deben ser listas de ternas (identificador, vértice, valor), 
donde el identificador identifica unívocamente al arco. 
Grafos 10 
 
Eficiencia de las operaciones 
 
 Dados los parámetros de tamaño:  
— NV: número de vértices 
— NA: número de aristas 
— GE: máximo grado de entrada 
— GS: máximo grado de salida 
 
Operación Matriz Lista Multilista 
Vacío O(NV2) (1) O(NV) O(NV) 
PonArista O(1) O(GS) (2) O(GS+GE) (4) 
quitaArista O(1) O(GS) (2) O(GS+GE) (4) 
costeArista O(1) O(GS) (2) O(GS) (2) 
hayArista O(1) O(GS) (2) O(GS) (2) 
sucesores O(NV) (1) O(GS) (2) (C) O(GS) (2) (C) 
predecesores O(NV) (1) O(NV+NA) (3) O(GE) (5) (C) 
 
(1) Recorrido de todos los vértices 
(2) Recorrido de la lista de sucesores de un vértice. Nótese que GS ≤ NV–1 y que 
por lo tanto O(GS) ⊆ O(NV). 
(3) Recorrido de todos los vértices, y para cada uno, recorrido de su lista de su-
cesores. El tiempo es O(NV+NA) porque cada arista u → v se atraviesa una 
sola vez –en el recorrido de los sucesores de u–. 
(4) Para poner o quitar la arista u → v hay que recorrer los sucesores de u y los 
predecesores de v –para así determinar la modificación oportuna de los enla-
ces en la multilista de adyacencia–. Nótese que GE, GS ≤ NV–1 y que por lo 
tanto O(GE+GS) ⊆ O(NV). 
(5) Recorrido de los predecesores de un vértice. 
 
Los tiempos marcados como (C) se reducen a O(1) si no se hace una copia de 
la lista de sucesores/predecesores devuelta como resultado. 
 
Grafos 11 
 
Implementación con multilistas de adyacencia 
Tipo representante 
 
 Clase de los vértices 
 
template <class TVerticeElem, class TAristaElem> 
class TVertice { 
  private: 
    TVerticeElem _elem; 
    int _ord; 
    TVertice( const TVerticeElem&, int ); 
  public: 
    const TVerticeElem& elem() const; 
    int ord() const; 
    friend TGrafo<TVerticeElem, TAristaElem>; 
}; 
 
 Clase de las aristas 
 
template <class TVerticeElem, class TAristaElem> 
class TArista { 
  private: 
    TAristaElem _elem; 
    TVertice<TVerticeElem, TAristaElem> *_origen, *_destino; 
    TVertice<TVerticeElem, TAristaElem>* origen() const; 
    TVertice<TVerticeElem, TAristaElem>* destino() const; 
    TArista( const TAristaElem&, 
             TVertice<TVerticeElem, TAristaElem>*, 
             TVertice<TVerticeElem, TAristaElem>* ); 
  public: 
    const TAristaElem& elem() const; 
    friend TGrafo<TVerticeElem, TAristaElem>; 
}; 
 
Grafos 12 
 
 Nodos de las multilistas 
 
template <class TVerticeElem, class TAristaElem> 
class TNodoGrafo { 
  private: 
    TNodoGrafo *_suc, *_pred; 
    TArista<TVerticeElem, TAristaElem>* _arista; 
    TNodoGrafo* suc() const; 
    TNodoGrafo* pred() const; 
    TArista<TVerticeElem, TAristaElem>* arista() const; 
    TNodoGrafo( TNodoGrafo*, TNodoGrafo*, TArista<TVerticeElem,TAristaElem>* ); 
  public: 
    friend TGrafo<TVerticeElem, TAristaElem>; 
}; 
 
 Representación de los grafos 
 
     int _numVertices, _longitud; 
     TVertice<TVerticeElem,TAristaElem>* *_vertices; 
     TNodoGrafo<TVerticeElem,TAristaElem> *_aristas; 
 
 
Grafos 13 
 
Interfaz de la clase TGrafo 
 
// Excepciones que generan las operaciones de este TAD 
// EVerticeInexistente 
 
// El tipo TVerticeElem debe implementar 
//    operator== 
//    int TVerticeElem::num() const 
// El tipo TAristaElem debe implementar 
//    operator+ 
//    operator<= 
 
template <class TVerticeElem, class TAristaElem> 
class
TGrafo { 
  public: 
 
  // Constructoras, destructora y operador de asignación 
     TGrafo( int ); 
     // El parámetro de la constructora permite especificar el número 
     // máximo de vértices que se preve almacenar. 
     // Por defecto es MaxVertices 
 
     static const int MaxVertices = 10; 
 
     TGrafo( const TGrafo<TVerticeElem,TAristaElem>& ); 
     ~TGrafo( ); 
     TGrafo<TVerticeElem,TAristaElem>& operator=(  
       const TGrafo<TVerticeElem,TAristaElem>& ); 
 
  // Operaciones de los grafos 
     int insertaVertice( const TVerticeElem& ); 
     // Pre : true 
     // Post : inserta un nuevo vértice en el grafo, asignándole un número de 
     //        orden que devuelve como resultado 
 
     void insertaArista( int, int, const TAristaElem& )  
     throw ( EVerticeInexistente ); 
     // Pre : son ordinales válidos 0 <= i < numVertices y son distintos 
     // Post : inserta una nueva arista entre los vértices identificados 
     //        por los parámetros 
 
      
Grafos 14 
 
     void borraArista( int, int ) throw ( EVerticeInexistente ); 
     // Pre : son ordinales válidos 0 <= i < numVertices 
     // Post : elimina la arista que conecta los vértices identificados 
     //        por los parámetros 
 
     // observadoras 
     bool hayArista( int, int ) const; 
     // Pre : true 
     // Post : devuelve true | false según si el grafo contiene o no 
     //        una arista conectando los vértices indicados 
 
     bool esVacio( ) const; 
     // Pre: true 
     // Post: Devuelve true | false según si el grafo está o no vacía 
 
     int numVertices( ) const; 
     // Pre: true 
     // Post: Devuelve el número de vértices del grafo 
 
     int ord( const TVerticeElem& ) const  throw ( EVerticeInexistente ); 
     // Pre: el elemento es un vértice del grafo 
     // Post: Devuelve el ordinal del vértice 
 
     TSecuenciaDinamica<TVerticeElem> enumera( ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices del grafo 
 
     TSecuenciaDinamica<TVerticeElem> sucesores( int ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices sucesores de uno dado 
 
     TSecuenciaDinamica<TVerticeElem> predecesores( int ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices predecesores de uno dado 
 
     // Recorridos 
     TSecuenciaDinamica<TVerticeElem> enumeraProfundidad( ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices del grafo, obtenidos 
     //       con un recorrido en profundidad 
 
     TSecuenciaDinamica<TVerticeElem> enumeraAnchura( ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices del grafo, obtenidos 
     //       con un recorrido en anchura 
Grafos 15 
 
     TSecuenciaDinamica<TVerticeElem> enumeraTopologico( ) const; 
     // Pre: true 
     // Post: Devuelve una secuencia con los vértices del grafo, obtenidos 
     //       con un recorrido en orden topológico 
 
  // Búsqueda de caminos mínimos 
     TSecuenciaDinamica< TPareja< TAristaElem, 
                                  TSecuenciaDinamica<TVerticeElem> > >  
     Dijkstra( int ) const  throw ( EVerticeInexistente ); 
     // Pre: es un ordinal válido 0 <= i < numVertices 
     // Post: devuelve una secuencia con pares de vértice y la distancia 
     //       mínima desde el origen a todos los vértices que le son 
     //       accesibles, junto con el camino más corto que los conecta  
 
  // Escritura 
     void escribe( ostream& salida ) const; 
 
  private: 
  // Variables privadas 
     int _numVertices, _longitud; 
     TVertice<TVerticeElem,TAristaElem>* *_vertices; 
     TNodoGrafo<TVerticeElem,TAristaElem> *_aristas; 
 
  // Operaciones privadas 
     void libera(); 
     void copia( const TGrafo<TVerticeElem,TAristaElem>& ); 
     void buscaSucesor( int, 
                        TNodoGrafo<TVerticeElem,TAristaElem>* &, 
                        TNodoGrafo<TVerticeElem,TAristaElem>* & ) const; 
     void buscaPredecesor( int, 
                           TNodoGrafo<TVerticeElem,TAristaElem>* &, 
                           TNodoGrafo<TVerticeElem,TAristaElem>* & ) const; 
     void profundidad( int, 
                       TCjto<int>& , 
                       TSecuenciaDinamica<TVerticeElem>& ) const; 
     void anchura( int, 
                   TCjto<int>& , 
                   TSecuenciaDinamica<TVerticeElem>& ) const; 
}; 
 
 
Grafos 16 
 
 Implementación de los vértices 
 
    template <class TVerticeElem, class TAristaElem> 
    TVertice<TVerticeElem, TAristaElem>::TVertice(  
      const TVerticeElem& elem, int ord ) : 
      _elem(elem), _ord(ord) { }; 
 
    template <class TVerticeElem, class TAristaElem> 
    const TVerticeElem& TVertice<TVerticeElem, TAristaElem>::elem() const { 
      return _elem; 
    }; 
 
    template <class TVerticeElem, class TAristaElem> 
    int TVertice<TVerticeElem, TAristaElem>::ord() const { 
      return _ord; 
    }; 
 
 Clase de las aristas 
 
    template <class TVerticeElem, class TAristaElem> 
    TArista<TVerticeElem,TAristaElem>::TArista(  
      const TAristaElem& elem, 
      TVertice<TVerticeElem, TAristaElem>* origen, 
      TVertice<TVerticeElem, TAristaElem>* destino        ) : 
      _elem(elem), _origen(origen), _destino(destino) { }; 
 
    template <class TVerticeElem, class TAristaElem> 
    const TAristaElem& TArista<TVerticeElem,TAristaElem>::elem() const { 
      return _elem; 
    }; 
 
    template <class TVerticeElem, class TAristaElem> 
    TVertice<TVerticeElem,TAristaElem>* 
    TArista<TVerticeElem,TAristaElem>::origen() const { 
      return _origen; 
    }; 
 
    template <class TVerticeElem, class TAristaElem> 
    TVertice<TVerticeElem,TAristaElem>* 
    TArista<TVerticeElem,TAristaElem>::destino() const { 
      return _destino; 
    }; 
 
 
 
Grafos 17 
 
 Clase de los nodos de las multilistas de adyacencia 
 
    template <class TVerticeElem, class TAristaElem> 
    TNodoGrafo<TVerticeElem,TAristaElem>::TNodoGrafo(  
      TNodoGrafo* suc = 0, TNodoGrafo* pred = 0, 
      TArista<TVerticeElem,TAristaElem>* arista = 0  ) : 
      _suc(suc), _pred(pred), _arista(arista) { }; 
 
    template <class TVerticeElem, class TAristaElem> 
    TNodoGrafo<TVerticeElem,TAristaElem>* 
    TNodoGrafo<TVerticeElem,TAristaElem>::suc() const { 
      return _suc; 
    }; 
 
    template <class TVerticeElem, class TAristaElem> 
    TNodoGrafo<TVerticeElem,TAristaElem>* 
    TNodoGrafo<TVerticeElem,TAristaElem>::pred() const { 
      return _pred; 
    }; 
 
    template <class TVerticeElem, class TAristaElem> 
    TArista<TVerticeElem,TAristaElem>* 
    TNodoGrafo<TVerticeElem,TAristaElem>::arista() const { 
      return _arista; 
    }; 
 
 
 
 Constructora de los grafos 
 
     template <class TVerticeElem, class TAristaElem> 
     TGrafo<TVerticeElem,TAristaElem>::TGrafo(  
       int maxVertices = MaxVertices ) : 
       _numVertices(0), 
       _longitud(maxVertices), 
       _vertices(new TVertice<TVerticeElem,TAristaElem>*[_longitud]), 
       _aristas(new TNodoGrafo<TVerticeElem,TAristaElem>[_longitud]) { }; 
 
Grafos 18 
 
 Copia, asignación y destrucción 
 
     template <class TVerticeElem, class TAristaElem> 
     TGrafo<TVerticeElem,TAristaElem>::TGrafo(  
       const TGrafo<TVerticeElem,TAristaElem>& grafo ) { 
        copia(grafo); 
     }; 
 
     template <class TVerticeElem, class TAristaElem> 
     TGrafo<TVerticeElem,TAristaElem>::~TGrafo( ) { 
       libera(); 
     }; 
 
     template <class TVerticeElem, class TAristaElem> 
     TGrafo<TVerticeElem,TAristaElem>& 
     TGrafo<TVerticeElem,TAristaElem>::operator=(  
       const TGrafo<TVerticeElem,TAristaElem>& grafo ) { 
       if( this != &grafo ) { 
         libera(); 
         copia(grafo); 
       } 
       return *this; 
     }; 
 
 
 
Grafos 19 
 
 Inserción de un vértice 
 
     template <class TVerticeElem, class TAristaElem>
int TGrafo<TVerticeElem,TAristaElem>::insertaVertice(  
       const TVerticeElem& vertice ) { 
       _vertices[_numVertices] =  
         new TVertice<TVerticeElem,TAristaElem>( vertice, _numVertices ); 
       return _numVertices++; 
     }; 
 
 
 Inserción de una arista 
 
     template <class TVerticeElem, class TAristaElem> 
     void TGrafo<TVerticeElem,TAristaElem>::insertaArista(  
       int origen, 
       int destino, 
       const TAristaElem& arista ) 
     throw ( EVerticeInexistente ) { 
       if ( ( origen == destino ) || 
            ( origen < 0 ) || ( origen >= _numVertices ) || 
            ( destino < 0 ) || ( destino >= _numVertices )  ) 
         throw EVerticeInexistente(); 
 
       TArista<TVerticeElem,TAristaElem>* nuevaArista = 
         new TArista<TVerticeElem,TAristaElem>( arista,  
                                                _vertices[origen], 
                                                _vertices[destino] ); 
       TNodoGrafo<TVerticeElem,TAristaElem>* nuevoNodo = 
         new TNodoGrafo<TVerticeElem,TAristaElem>( _aristas[origen].suc(), 
                                                   _aristas[destino].pred(), 
                                                   nuevaArista              ); 
       _aristas[origen]._suc = nuevoNodo; 
       _aristas[destino]._pred = nuevoNodo; 
     }; 
 
Grafos 20 
 
 Borrado de una arista 
 
     template <class TVerticeElem, class TAristaElem> 
     void TGrafo<TVerticeElem,TAristaElem>::borraArista(  
       int origen, int destino ) 
     throw ( EVerticeInexistente ) { 
       if ( ( origen == destino ) || 
            ( origen < 0 ) || ( origen >= _numVertices ) || 
            ( destino < 0 ) || ( destino >= _numVertices )  ) 
         throw EVerticeInexistente(); 
 
       TNodoGrafo<TVerticeElem,TAristaElem> *act, *ant; 
       act = _aristas[origen].suc(); 
       buscaSucesor( destino, act, ant ); 
       if ( act != 0 ) { 
         if ( ant == 0 ) 
           _aristas[origen]._suc = act‐>suc(); 
         else 
           ant‐>_suc = act‐>suc(); 
         act = _aristas[destino].pred(); 
         buscaPredecesor( origen, act, ant ); 
         if ( ant == 0 ) 
           _aristas[destino]._pred = act‐>pred(); 
         else 
           ant‐>_pred = act‐>pred(); 
         delete act‐>arista(); 
         delete act; 
       } 
     }; 
 
 
 Consulta si existe una arista conectando dos vértices dados 
 
     template <class TVerticeElem, class TAristaElem> 
     bool TGrafo<TVerticeElem,TAristaElem>::hayArista(  
       int origen, int destino ) const { 
       TNodoGrafo<TVerticeElem,TAristaElem> *act, *ant; 
       act = _aristas[origen].suc(); 
       buscaSucesor( destino, act, ant ); 
       return act != 0; 
     }; 
 
 
 
 
Grafos 21 
 
 Consulta si el grafo está vacío 
 
     template <class TVerticeElem, class TAristaElem> 
     bool TGrafo<TVerticeElem,TAristaElem>::esVacio( ) const { 
       return _numVertices == 0; 
     }; 
 
 Consulta el número de vértices 
 
     template <class TVerticeElem, class TAristaElem> 
     int TGrafo<TVerticeElem,TAristaElem>::numVertices( ) const { 
       return _numVertices; 
     }; 
 
 Dado una etiqueta de un vértice obtiene el ordinal del vértice 
 
     template <class TVerticeElem, class TAristaElem> 
     int TGrafo<TVerticeElem,TAristaElem>::ord( const TVerticeElem& vertice ) 
     const throw ( EVerticeInexistente ) { 
       bool encontrado = false; 
       int i = 0; 
 
       while ( (! encontrado) && (i < _numVertices) ) 
         encontrado = _vertices[i++]‐>elem() == vertice; 
       if ( ! encontrado ) 
         throw EVerticeInexistente(); 
       return ‐‐i; 
     }; 
 
 Obtiene unas secuencia con los vértices del grafo 
 
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::enumera( ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado; 
       for ( int i = 0; i < _numVertices; i++ ) 
         resultado.inserta( _vertices[i]‐>elem() ); 
       return resultado; 
     }; 
 
Grafos 22 
 
 Obtiene la secuencia de sucesores de un vértice dado 
 
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::sucesores( int vertice ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado; 
       TNodoGrafo<TVerticeElem, TAristaElem>* p = _aristas[vertice].suc(); 
 
       while( p != 0 ) { 
         resultado.inserta( p‐>arista()‐>destino()‐>elem() ); 
         p = p‐>suc(); 
       } 
 
       return resultado; 
     }; 
 
 Obtiene la secuencia de predecesores de un vértice dado 
 
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::predecesores( int vertice ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado; 
       TNodoGrafo<TVerticeElem, TAristaElem>* p = _aristas[vertice].pred(); 
 
       while( p != 0 ) { 
         resultado.inserta( p‐>arista()‐>origen()‐>elem() ); 
         p = p‐>pred(); 
       } 
 
       return resultado; 
     }; 
 
 
Grafos 23 
 
 Operaciones auxiliares de búsqueda 
 
     template <class TVerticeElem, class TAristaElem> 
      void TGrafo<TVerticeElem,TAristaElem>::buscaSucesor( 
        int destino, 
        TNodoGrafo<TVerticeElem,TAristaElem>* & act, 
        TNodoGrafo<TVerticeElem,TAristaElem>* & ant ) const { 
        ant = 0; 
        while ( ( act != 0 ) &&  
                ( act‐>arista()‐>destino()‐>ord() != destino ) ) { 
          ant = act; 
          act = act‐>suc(); 
        } 
      } 
 
     template <class TVerticeElem, class TAristaElem> 
      void TGrafo<TVerticeElem,TAristaElem>::buscaPredecesor( 
        int origen, 
        TNodoGrafo<TVerticeElem,TAristaElem>* & act, 
        TNodoGrafo<TVerticeElem,TAristaElem>* & ant ) const { 
        ant = 0; 
        while ( ( act != 0 ) &&  
                ( act‐>arista()‐>origen()‐>ord() != origen ) ) { 
          ant = act; 
          act = act‐>pred(); 
        } 
      } 
 
Grafos 24 
 
7.3 Recorridos de grafos 
 
 En analogía con los árboles, los grafos admiten recorridos en profundidad y en 
anchura. En general, los recorridos no dependen de los valores de los arcos, 
por lo que en este apartado nos limitaremos a grafos no valorados. 
 
 El TAD grafo no impone un orden determinado a los sucesores (o predeceso-
res) de un vértice. Por lo tanto, no es posible especificar de manera unívoca 
una lista de vértices como resultado de un recorrido. 
 
 Los grafos dirigidos acíclicos admiten un tercer tipo de recorrido, denominado 
recorrido de ordenación topológica. 
 
 Realizaremos implementaciones modulares de las operaciones, sin acceder a la 
representación interna de los grafos. 
 
 
 
Grafos 25 
 
7.3.1 Recorrido en profundidad 
 
 Se puede considerar como una generalización del recorrido en preorden de un 
árbol. La idea es:  
— Visitar el vértice inicial 
— Si es posible, avanzar a un sucesor aún no visitado 
— En otro caso, retroceder al vértice visitado anteriormente, e intentar conti-
nuar el recorrido desde éste.  
Una vez visitados todos los descendientes del vértice inicial, si quedan vértices 
no visitados se inicia un recorrido de otra componente del grafo.  
Por ejemplo, el recorrido del grafo:   
 
 
Representado como un bosque:  
 
 
El algoritmo recursivo de recorrido en profundidad  
— Devuelve una secuencia en la cual aparece cada vértice del grafo una sola 
vez. 
— Usa un conjunto de vértices para llevar cuenta de los visitados. 
— Usa un procedimiento auxiliar privado encargado del recorrido de una sola 
componente. 
Grafos 26 
 
 La implementación 
 
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::enumeraProfundidad( ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado;
TCjto<int> visitados; 
 
       for ( int i = 0; i < _numVertices; i++ ) 
         if ( ! visitados.esta( i ) ) 
           profundidad( i, visitados, resultado ); 
 
       return resultado; 
     }; 
 
 
 
El procedimiento auxiliar que recorre una componente: 
 
     template <class TVerticeElem, class TAristaElem> 
     void 
     TGrafo<TVerticeElem,TAristaElem>::profundidad( 
       int vertice, 
       TCjto<int>& visitados, 
       TSecuenciaDinamica<TVerticeElem>& resultado  ) const { 
 
       TNodoGrafo<TVerticeElem, TAristaElem>* act = _aristas[vertice].suc(); 
 
       visitados.inserta( vertice ); 
       resultado.inserta( _vertices[vertice]‐>elem() ); 
       while ( act != 0 ) { 
         if ( ! visitados.esta( act‐>arista()‐>destino()‐>ord() ) ) 
           profundidad( act‐>arista()‐>destino()‐>ord(),  
                        visitados, resultado ); 
         act = act‐>suc(); 
       } 
     }; 
 
Grafos 27 
 
 La complejidad de la operación depende de la representación elegida para los 
grafos:  
— Si se usan listas o multilistas de adyacencia, el tiempo es O(NV+NA), ya que 
cada vértice se visita una sola vez, pero se exploran todas las aristas que sa-
len de él. 
— Si se usan matrices de adyacencia el tiempo es O(NV 2), ya que cada vértice 
se visita una sola vez, pero el cálculo de sus sucesores requiere tiempo 
O(NV). 
Para conseguir estos tiempos es necesario que las operaciones de CJTO sean 
O(1), lo que se puede conseguir con una implementación basada en tablas dis-
persas (un vector de booleanos). 
 
 
Grafos 28 
 
7.3.2 Recorrido en anchura 
 
 El recorrido en anchura, o por niveles, generaliza el recorrido de árboles con 
igual denominación. La idea es:  
— Visitar el vértice inicial. 
— Si el último vértice visitado tiene sucesores aún no visitados, realizar 
sucesivamente un recorrido desde cada uno de estos. 
— En otro caso, continuar con un recorrido iniciado en cualquier vértice no 
visitado aún. 
 
Por ejemplo, para el mismo grafo del ejemplo anterior: 
   
 
 
representado como un bosque, resultado del recorrido por niveles 
 
 
 
 La implementación es similar a la del recorrido en profundidad, con ayuda de 
un procedimiento auxiliar que recorre una componente del grafo. La diferencia 
está en que en lugar de utilizar un procedimiento recursivo, lo hacemos iterati-
vo con ayuda de una cola. 
 
 
Grafos 29 
 
 La implementación  
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::enumeraAnchura( ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado; 
       TCjto<int> visitados; 
 
       for ( int i = 0; i < _numVertices; i++ ) 
         if ( ! visitados.esta( i ) ) 
           anchura( i, visitados, resultado ); 
 
       return resultado; 
     };    
El procedimiento auxiliar que recorre por niveles una componente:  
     template <class TVerticeElem, class TAristaElem> 
     void 
     TGrafo<TVerticeElem,TAristaElem>::anchura( 
       int vertice, 
       TCjto<int>& visitados, 
       TSecuenciaDinamica<TVerticeElem>& resultado  ) const { 
 
       TNodoGrafo<TVerticeElem, TAristaElem>* act; 
       TColaDinamica<int> pendientes; 
       int actual;  
       visitados.inserta( vertice ); 
       pendientes.ponDetras( vertice ); 
       while ( ! pendientes.esVacio() ) { 
         actual = pendientes.primero(); 
         pendientes.quitaPrim(); 
         resultado.inserta( _vertices[actual]‐>elem() ); 
         act = _aristas[actual].suc(); 
         while ( act != 0 ) { 
           if ( ! visitados.esta( act‐>arista()‐>destino()‐>ord() ) ) { 
             visitados.inserta( act‐>arista()‐>destino()‐>ord() ); 
             pendientes.ponDetras( act‐>arista()‐>destino()‐>ord() ); 
           } 
           act = act‐>suc(); 
         } 
       } 
     }; 
 
El análisis de la complejidad es el mismo que hemos hecho para el recorrido 
en profundidad. 
Grafos 30 
 
7.3.3 Recorrido de ordenación topológica 
 
 Este tipo de recorridos sólo es aplicable a grafos dirigidos acíclicos. 
Dado un grafo dirigido acíclico G, la relación entre vértices definida como  
u G v ⇔def existe un camino de u a v en G  
es un orden parcial. Se llama recorrido de ordenación topológica de G a cualquier re-
corrido de G que visite cada vértice v solamente después de haber visitado to-
dos los vértices de u tales que u G v. En general, son posibles varios 
recorridos de ordenación topológica para un mismo grafo G. 
   
Por ejemplo, algunos recorridos en ordenación topológica del grafo:  
 
 
 
xs : [ A, B, C, D, E, F, G, H, I, J, K, L] 
xs : [ A, B, D, F, H, J, C, E, G, I, L, K] … 
 
 La idea básica del algoritmo es reiterar la elección de un vértice aún no visitado 
y tal que todos sus predecesores hayan sido ya visitados. El problema es que el 
algoritmo resultante de seguir directamente esta idea es poco eficiente. 
Una forma de lograr un algoritmo más eficiente es:  
— Mantener un vector P indexado por vértices, tal que P[v ] es el número de 
predecesores de v aún no visitados. 
— Mantener los vértices v tal que P[v ] = 0 en un conjunto M. 
— Organizar un bucle que en cada vuelta: 
— añade un vértice de M al recorrido 
— actualiza P y M 
Grafos 31 
 
 La implementación del algoritmo  
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica<TVerticeElem> 
     TGrafo<TVerticeElem,TAristaElem>::enumeraTopologico( ) const { 
       TSecuenciaDinamica<TVerticeElem> resultado; 
       int* numPred = new int[_numVertices]; 
       TSecuenciaDinamica<int> visitables; 
       TNodoGrafo<TVerticeElem, TAristaElem> *act; 
       int actual, destino;  
       for ( int i = 0; i < _numVertices; i++ ) { 
         numPred[i] = 0; 
         act = _aristas[i].pred(); 
         while ( act != 0 ) { 
           numPred[i]++; 
           act = act‐>pred(); 
         } 
         if ( numPred[i] == 0 ) 
           visitables.inserta(i); 
       }  
       while ( ! visitables.esVacio() ) { 
         visitables.reinicia(); 
         actual = visitables.actual(); 
         visitables.borra(); 
         resultado.inserta( _vertices[actual]‐>elem() ); 
         act = _aristas[actual].suc(); 
         while ( act != 0 ) { 
           destino = act‐>arista()‐>destino()‐>ord(); 
           numPred[ destino ]‐‐; 
           if ( numPred[ destino ] == 0 ) 
             visitables.inserta( destino ); 
           act = act‐>suc(); 
         } 
       } 
       return resultado; 
     };     
 En cuanto a la complejidad, el algoritmo opera en tiempo: 
— O(NV 2) si el grafo está representado como matriz de adyacencia. 
— O(NV+NA) si el grafo está representado conlistas de adyacencia.     
 Este algoritmo se puede modificar para detectar si el grafo es acíclico o no, en 
lugar de exigir aciclicidad en la precondición. Si el conjunto M es queda vacío 
antes de haber visitado NV vértices, el grafo no es acíclico. 
Grafos 32 
 
7.4 Caminos de coste mínimo 
 En este apartado vamos a estudiar algoritmos que calculan caminos de coste 
mínimo en grafos dirigidos valorados. El coste de un camino se calcula como 
la suma de los valores de sus arcos. Se presupone por tanto que existe una 
operación de suma entre valores. En las aplicaciones prácticas, los valores sue-
len ser números no negativos. Sin embargo, la corrección de los algoritmos 
que vamos a estudiar sólo exige que el tipo de los valores satisfaga los requisi-
tos expresados en la especificación de la clase de tipos VAL-ORD que presen-
tamos al principio del tema. 
 
 
7.4.1 Caminos mínimos con origen fijo 
 Dado un vértice u de un grafo dirigido valorado g, nos interesa calcular todos 
los caminos mínimos con origen u y sus correspondientes costes. 
 
 Para resolver este problema vamos a utilizar el algoritmo de Dijkstra (1959). El 
algoritmo mantiene un conjunto M de vértices v para los cuales ya
se ha encon-
trado el camino mínimo desde u. La idea del algoritmo:  
— Se inicializa M := {u}. Para cada vértice v diferente de u, se inicializa un 
coste estimado C(v) := costeArista(G, u, v).  
— Se entra en un bucle. En cada vuelta se elige un vértice w que no esté en M 
y cuyo coste estimado sea mínimo. Se añade w a M, y se actualizan los cos-
tes estimados de los restantes vértices v que no estén en M, haciendo 
C(v) := min(C(v), C(w) + costeArista(G, w, v)).   
Al terminar, se han encontrado caminos de coste mínimo C(v) desde u hasta 
los restantes vértices v. Esta idea se puede refinar:  
— Si en algún momento llega a cumplirse que para todos los vértices v que no 
están en M C(v) = ∞, el algoritmo puede terminar. 
— Además de calcular C, calculamos otro vector p, indexado por los ordinales 
de los vértices, que representa los caminos mínimos desde u a los restantes 
vértices, según el siguiente criterio:  
— p(ord(v)) = 0 si v no es accesible desde u. 
— p(ord(v)) = ord(w) si v es accesible desde u y w es el predecesor inmediato 
de v en el camino mínimo de u a v. 
Grafos 33 
 
 Como ejemplo del funcionamiento del algoritmo, vamos a ejecutarlo para el 
grafo dirigido de la figura siguiente, tomando como vértice inicial u = 1.  
 
 
 
 
 
 
It. M w c/p(1) c/p(2) c/p(3) c/p(4) c/p(5) c/p(6) 
0 {1} – 0/1 30/1 ∞/0 50/1 40/1 100/1 
1 {1,2} 2 0/1 30/1 70/2 50/1 40/1 100/1 
2 {1,2,5} 5 0/1 30/1 70/2 50/1 40/1 100/1 
3 {1,2,5,4} 4 0/1 30/1 60/4 50/1 40/1 100/1 
4 {1,2,5,4,3} 3 0/1 30/1 60/4 50/1 40/1 90/3 
5 {1,2,5,4,3,6} 6 0/1 30/1 60/4 50/1 40/1 90/3 
  
De aquí se deduce, por ejemplo, que un camino mínimo de 1 a 6, con coste 
c(v) = 90, es [1, 4, 3, 6].  
 
Grafos 34 
 
Implementación del algoritmo 
 
 Necesitamos una clase auxiliar para representar las distancias entre vértices 
 
template <class TAristaElem> 
class TDistancia { 
  public: 
 
    enum TTipo { Cero, Valor, Infinito }; 
 
    TDistancia ( TTipo tipo = Infinito ) : 
      _tipo(tipo), _valor(0) { 
    }; 
 
    TDistancia ( const TAristaElem& valor ) : 
      _tipo( Valor ) { 
      _valor = new TAristaElem( valor ); 
    }; 
 
    TDistancia ( const TDistancia& distancia ) { copia( distancia ); } 
 
    ~TDistancia ( ) { libera( ); } 
 
    TDistancia& operator=( const TDistancia& distancia ) { 
      if ( &distancia != this ) { 
        libera(); 
        copia( distancia ); 
      } 
      return *this; 
    } 
 
    bool operator== ( const TDistancia& distancia ) const { 
      return ( esCero() && distancia.esCero() ) || 
             ( esInfinito() && distancia.esInfinito() ) || 
             ( ( esValor() ) && ( distancia.esValor() ) && 
               ( *_valor == distancia.valor() )             ); 
    } 
 
    bool operator<= ( const TDistancia& distancia ) const { 
      return esCero() || distancia.esInfinito() || 
             ( ( esValor() ) && ( distancia.esValor() ) && 
               ( *_valor <= distancia.valor() )            ); 
    } 
 
Grafos 35 
 
    const TDistancia& operator+= ( const TDistancia& distancia ) const { 
      if ( esCero() || ( esValor() && distancia.esInfinito() ) ) { 
        libera(); 
        copia( distancia ); 
      } 
      else if ( esValor() && distancia.esValor() ) 
        *_valor += distancia.valor(); 
      return *this; 
    } 
 
    TDistancia& operator+ ( const TDistancia& distancia ) const { 
      TDistancia<TAristaElem> *resultado =  
        new TDistancia<TAristaElem>( *this ); 
      *resultado += distancia; 
      return *resultado; 
    } 
 
    const TAristaElem& valor( ) const { return *_valor; } 
 
    bool esCero () const { return _tipo == Cero; } 
    bool esInfinito () const { return _tipo == Infinito; } 
    bool esValor () const { return _tipo == Valor; } 
 
  private: 
    TAristaElem * _valor; 
    TTipo _tipo; 
 
    void libera() { if ( esValor() ) delete _valor; } 
 
    void copia( const TDistancia& distancia ) { 
      _tipo = distancia._tipo; 
      if ( distancia.esValor() ) 
        _valor = new TAristaElem( distancia.valor() ); 
    } 
}; 
 
 
 
 
Grafos 36 
 
 Finalmente el algoritmo de Dijkstra 
 
     template <class TVerticeElem, class TAristaElem> 
     TSecuenciaDinamica< TPareja< TAristaElem, 
                                  TSecuenciaDinamica<TVerticeElem> > > 
     TGrafo<TVerticeElem,TAristaElem>::Dijkstra( int vertice ) const 
     throw ( EVerticeInexistente ) 
     { 
       if ( ( vertice < 0 ) || ( vertice >= _numVertices ) ) 
         throw EVerticeInexistente();  
       TSecuenciaDinamica< TPareja< TAristaElem, 
                                    TSecuenciaDinamica<TVerticeElem> > > 
         resultado; 
       TCjto<int> calculados; 
       int *caminos = new int[_numVertices]; 
       // el array de distancias se inicializa con todas las posiciones a 
       // Infinito 
       TDistancia<TAristaElem> *distancias =  
         new TDistancia<TAristaElem>[_numVertices]; 
       TNodoGrafo<TVerticeElem,TAristaElem> *act, *ant; 
 
       calculados.inserta(vertice); 
       for ( int i = 0; i < _numVertices; i++ ) { 
         if ( i == vertice ) { 
           caminos[i] = i; 
           distancias[i] = 
             TDistancia<TAristaElem>(TDistancia<TAristaElem>::Cero); 
         } 
         else { 
           act = _aristas[vertice].suc(); 
           buscaSucesor( i, act, ant ); 
           if ( act != 0 ) { 
             caminos[i] = vertice; 
             distancias[i] = TDistancia<TAristaElem>( act‐>arista()‐>elem() ); 
           } 
           else 
             caminos[i] = ‐1; 
         } 
       } 
 
Grafos 37 
 
       bool fin = false; 
       int n = 1, masCercano; 
       TDistancia<TAristaElem> minDist, nuevaDist;  
       while ( ( n < _numVertices ) && ! fin ) { 
         minDist = TDistancia<TAristaElem>(TDistancia<TAristaElem>::Infinito); 
         for ( int i = 0; i < _numVertices; i++ ) { 
           if ( ! calculados.esta(i) ) 
             if ( distancias[i] <= minDist ) { 
               minDist = distancias[i]; 
               masCercano = i; 
             } 
         } 
         if ( minDist.esInfinito() ) 
           fin = true; 
         else { 
           calculados.inserta( masCercano ); 
           n++; 
           for ( int i = 0; i < _numVertices; i++ ) 
             if ( ! calculados.esta(i) ) { 
               act = _aristas[masCercano].suc(); 
               buscaSucesor( i, act, ant ); 
               if ( act != 0 ) { 
                 nuevaDist = TDistancia<TAristaElem> ( act‐>arista()‐>elem() ) 
                            + minDist; 
                 if ( ! ( distancias[i] <= nuevaDist ) ) { 
                   caminos[i] = masCercano; 
                   distancias[i] = nuevaDist; 
                 } 
               } 
             } 
         } 
       } 
Grafos 38 
 
       TSecuenciaDinamica<TVerticeElem> camino; 
       int actual; 
       for ( int i = 0; i < _numVertices; i ++ ) { 
         if ( ( caminos[i] != ‐1 ) && ( i != vertice ) ) { 
           camino = TSecuenciaDinamica<TVerticeElem>(); 
           camino.inserta( _vertices[i]‐>elem() ); 
           actual = i; 
           do { 
             actual = caminos[actual]; 
             camino.reinicia(); 
             camino.inserta( _vertices[actual]‐>elem() ); 
           } while ( actual != vertice ); 
           resultado.inserta( 
             TPareja< TAristaElem, TSecuenciaDinamica< TVerticeElem > > ( 
               distancias[i].valor(), camino                              ) ); 
         } 
       } 
       return resultado; 
     }; 
Grafos 39 
 
Complejidad del algoritmo de Dijkstra 
 La realización anterior está pensada para una representación de los grafos con 
matrices de adyacencia. El coste de las diferentes etapas:  
— Incialización de c y p: O(NV). NV iteraciones donde cada iteración sólo invo-
lucra operaciones con coste constante. Esto es cierto si los grafos se im-
plementan con matrices
de adyacencia, donde efectivamente costeArista es 
O(1). 
— El bucle principal se compone de O(NV) iteraciones, donde: 
— La selección de w se realiza con un bucle de coste O(NV), suponiendo 
que utilizamos una implementación de los conjuntos donde pertenece es 
O(1). 
— La actualización de c y p se realiza con un bucle que ejecuta O(NV) itera-
ciones. Cada iteración tiene coste constante siempre y cuando costeArista 
tenga coste O(1). 
Por lo tanto, el coste total es O(NV 2). 
 
 Si se utilizan grafos representados con listas de adyacencia, es necesario reali-
zar algunos cambios en el algoritmo para obtener esa complejidad, debido a 
que costeArista pasa a ser O(NV):  
— Inicializaciones, para obtener tiempo O(NV) 
— c se inicializa con ∞ y p se inicializa con 0, excepto c(u) := 0 y p(ord(u)) := 
ord(u). O(NV) 
— Para cada pareja (c, v) perteneciente a sucesores(g, u) se hace: 
c(v) := c; p(v) := u. O(GS) ⊆ O(NV); O(GS) ⊆ O(NA). 
— Bucle principal, para obtener tiempo O(NV 2) 
— Se cambia el bucle interno que actualiza c y p, escribiéndolo como bucle 
que recorre la secuencia de sucesores de w. O(GS) ⊆ O(NV)  
 
 En cuanto al espacio, se requiere espacio adicional O(NV) para el conjunto de 
vértices m, además del espacio ocupado por el propio grafo y los resultados. 
Grafos 40 
 
7.4.2 Caminos mínimos entre todo par de vértices 
 
 El problema es, dado un grafo dirigido valorado g, se quieren calcular todos los 
caminos mínimos entre todas las parejas de vértices de g, junto con sus costes. 
Aplicando reiteradamente el algoritmo de Dijkstra, se obtiene una solución de 
complejidad O(NV 3). 
 
 
 La otra posibilidad es utilizar el algoritmo de Floyd (1962). Este algoritmo, aun-
que con el mismo coste, tiene la ventaja de ser más elegante y compacto. 
Además sólo necesita espacio adicional O(1), mientras que el algoritmo de 
Dijkstra necesita espacio auxiliar O(NV). 
 
 
 La idea básica del algoritmo consiste en mejorar la estimación c(u, v) del coste 
de un camino mínimo de u a v mediante la asignación:   
c(v, w) := min( c(v, w), c(v, u) + c(u, w) ) 
 
Diremos que el vértice u actúa como pivote en esta actualización de 
c(v, w). El algoritmo comienza con una inicialización natural de c, y a continua-
ción reitera la actualización de c(v, w) para todas las parejas de vértices (v, w) y 
con todos los pivotes u. 
 
Al igual que en el algoritmo de Dijkstra, construimos un resultado adicional 
que representa los caminos mínimos entre cada par de vértices. Este resultado 
viene representado por un vector bidimensional s indexado por parejas de or-
dinales de los vértices, según el siguiente criterio:  
— s(ord(v), ord(w)) = 0 si v no hay caminos de v a w. 
— s(ord(v), ord(w)) = ord(u) si w es accesible desde v y u es el sucesor inmediato 
de v en el camino mínimo de v a w calculado por el algoritmo. 
Grafos 41 
 
 Apliquemos el algoritmo al siguiente grafo:    
   
0: Estado inicial 
 
Matriz de costes c Matriz de sucesores s 
 A B C D E 
A 0 3 9 ∞ ∞ 
B ∞ 0 4 ∞ ∞ 
C ∞ ∞ 0 ∞ ∞ 
D ∞ ∞ ∞ 0 7 
E ∞ ∞ ∞ ∞ 0 
 1 2 3 4 5 
1 1 2 3 0 0 
2 0 2 3 0 0 
3 0 0 3 0 0 
4 0 0 0 4 5 
5 0 0 0 0 5 
 
1: Después de actualizar c y s usando A como vértice pivote. 
Ningún arco entra en A por lo tanto usar A como pivote no mejora nada. c 
y s quedan inalterados.  
2: Después de actualizar c y s usando B como vértice pivote. 
Esto permite mejorar el coste del camino entre A y C. c(A, C) y 
s(1, 3) se modifican, las demás posiciones no se modifican. 
 
Matriz de costes c Matriz de sucesores s 
 A B C D E 
A 0 3 7 ∞ ∞ 
B ∞ 0 4 ∞ ∞ 
C ∞ ∞ 0 ∞ ∞ 
D ∞ ∞ ∞ 0 7 
E ∞ ∞ ∞ ∞ 0 
 1 2 3 4 5 
1 1 2 2 0 0 
2 0 2 3 0 0 
3 0 0 3 0 0 
4 0 0 0 4 5 
5 0 0 0 0 5 
 
3, 4, 5: Actualizaciones de c y s usando como vértices pivote C, D y E. 
No mejoran nada, c y s quedan como en la figura anterior. 
EDI todo/Tema1-An.AlgoritmIterativos.pdf
 
TEMA 1 
ANÁLISIS DE LA EFICIENCIA 
 
1. Complejidad de los algoritmos 
2. Medidas asintóticas de la complejidad 
3. Ordenes de complejidad 
4. Métodos de análisis de la complejidad temporal de los algoritmos 
iterativos 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Bibliografía: Fundamentals of Data Structures in C++ 
E. Horowitz, S. Sahni, D. Mehta 
Computer Science Press, 1995 
 
Análisis de la eficiencia 1 
 
1.1 Complejidad de los algoritmos 
 
Correcto programa
ideal
Legible y
bien documentado
Eficiente
Fácil de mantener
y reutilizar 
 
 Un algoritmo será tanto más eficiente cuantos menos recursos consuma: tiem-
po y espacio de memoria necesario para ejecutarlo. 
 
 La eficiencia de los algoritmos se cuantifica con las medidas de complejidad: 
— Complejidad temporal: tiempo de cómputo de un programa 
— Complejidad espacial: memoria que utiliza un programa en su ejecución 
 
 En general, es más importante conseguir buenas complejidades temporales 
 
¡¡¡ Seamos generosos con las variables !!! 
 
Análisis de la eficiencia 2 
 
 La complejidad de un programa depende de 
— la máquina y el compilador. 
— el tamaño de los datos de entrada, 
— el valor de los datos de entrada, y 
 
 En los estudios teóricos de la complejidad no nos ocupamos de la máquina ni 
del compilador. 
 
 El “tamaño de los datos” depende del tipo de datos y del algoritmo: 
— para un vector, su longitud, 
— para un número, su valor o su número de dígitos, … 
 
 Para un tamaño de datos dado, hablamos del valor de los datos como pertene-
cientes al caso mejor, al caso peor, o, si tenemos en cuenta todos los valores 
posibles y sus probabilidades, al caso promedio. Por ejemplo, 
 
 const int N = 10; 
 int i, j, x; 
 int v[N]; 
 
 for ( i = 1; i < N; i++ ) 
 { 
 x = v[i]; 
 j = i-1; 
 while ( (j >= 0) && (v[j] > x) ) 
 { 
 v[j+1] = v[j]; 
 j = j-1; 
 } 
 v[j+1] = x; 
 } 
 // ¿qué hace este algoritmo? 
 
con tamaño 4: uno = [1,2,5,4], dos = [1,2,3,4] y tres = [4,3,2,1] 
— Caso mejor: dos (no se entra nunca en el while) 
— Caso peor: tres (se ejecuta (i-1) veces el bucle por cada i) 
Análisis de la eficiencia 3 
 
Medida del tiempo de ejecución de un algoritmo 
 
 Vamos a medir el tiempo de ejecución del algoritmo de ordenación anterior, 
aplicado sobre datos de distinto tamaño 
— Convertimos el algoritmo de ordenación en un procedimiento 
void ordena ( int v[], int num ) { 
 int i, j, x; 
 
 for ( i = 1; i < num; i++ ) { 
 x = v[i]; 
 j = i-1; 
 while ( (j >= 0 ) && (v[j] > x) ) { 
 v[j+1] = v[j]; 
 j = j-1; 
 } 
 v[j+1] = x; 
 } 
} 
 
— Para medir el tiempo utilizamos la función clock( ) que devuelve un valor de 
tipo clock_t con el número de ciclos de reloj en ese instante. 
 const int N = 50; 
 clock_t t1, t2; 
 int v[N]; 
 double tiempo; 
 
 for ( i = 0; i < N; i++ ) 
 v[i] = rand(); 
 t1 = clock(); 
 ordena(v, N); 
 t2 = clock(); 
 tiempo = double(t2-t1)/CLOCKS_PER_SEC; 
el problema de este método es que la precisión del reloj del sistema suele estar 
entorno a los milisegundos, y el tiempo a medir es inferior o de ese orden. 
La solución es repetir la ejecución del procedimiento que pretendemos medir 
un cierto número de veces hasta que el tiempo medido sea significativo. Como 
pretendemos tomar medidas para distintos tamaños de datos, repetiremos 
también el proceso para cada valor de N, y utilizaremos un número de repeti-
ciones distinto dependiendo de dicho valor. 
Análisis de la eficiencia 4 
 
 
 El programa de toma de tiempos queda entonces 
 
int main( int argc, char* argv[] ) 
{ 
 const int N = 1000; 
 const int NumPuntos = 10; 
 int i, j, k; 
 int tamanyos[NumPuntos] = { 100, 200, 300, 400, 500, 600, 
 700, 800, 900, 1000 }; 
 int repeticiones[NumPuntos] = { 5000, 4000, 4000, 3000, 3000, 3000, 
 2000, 1000, 500, 100 }; 
 double tiempos[NumPuntos];
int u[N], v[N]; 
 clock_t t1, t2; 
 
 for ( i = 0; i < N; i++ ) 
 u[i] = rand(); 
 
 for ( k = 0; k < NumPuntos; k++ ) { 
 t1 = clock(); 
 for( i = 0; i < repeticiones[k]; i++) { 
 for ( j = 0; j < tamanyos[k]; j++ ) 
 v[j] = u[j]; 
 ordena(v, tamanyos[k]); 
 } 
 t2 = clock(); 
 tiempos[k] = (double(t2-t1)/CLOCKS_PER_SEC) / repeticiones[k]; 
 } 
 
 for ( k = 0; k < NumPuntos; k++ ) 
 cout << "N = " << tamanyos[k] << "; t = " << tiempos[k] << endl; 
 
 char c; 
 cin >> c; 
 return 0; 
} 
Análisis de la eficiencia 5 
 
 Los resultados de la ejecución anterior (en un Pentium III a 650MHz, compi-
lado en C++ Builder 5) 
 
N t 
100 0,000067
200 0,000220
300 0,000544
400 0,000947
500 0,001439
600 0,002063
700 0,002842
800 0,003775
900 0,004662
1000 0,005840
 
Ordenación
0,000
0,001
0,002
0,003
0,004
0,005
0,006
0,007
0 200 400 600 800 1000
N
t
 
 
Análisis de la eficiencia 6 
 
Definición de complejidad temporal 
 
 El tiempo que tarda un algoritmo A en procesar una entrada concreta xr lo no-
taremos como 
tA(x
r ) 
 Definimos la complejidad de un algoritmo A en el caso peor, que notaremos 
TA(n) 
como la función 
TA(n) =def max{ tA(x
r ) | xr de tamaño n } 
 Definimos la complejidad de un algoritmo A en el caso promedio, que nota-
remos 
TMA(n) 
como la función 
TMA(n)=def ∑
n tamaño de xr
p(xr ) ⋅ tA(x
r ) 
siendo p(xr ) la función de probabilidad −p(xr ) ∈ [0,1]− de que la entrada sea el 
dato xr . 
 
 La complejidad en el caso promedio supone el conocimiento de la distribución 
de probabilidades de los datos. 
 La complejidad en el caso peor proporciona una medida pesimista, pero fiable. 
 
 Dado que realizamos un estudio teórico, ignorando los detalles de la máquina 
y el compilador, y teniendo en cuenta que las diferencias en eficiencia se hacen 
significativas para tamaños grandes de los datos, no pretendemos obtener la 
forma exacta de la función de complejidad sino que nos limitaremos a estudiar 
su comportamiento asintótico. 
Análisis de la eficiencia 7 
 
1.2 Medidas asintóticas de la complejidad 
 
 Una medida asintótica es un conjunto de funciones que muestran un comporta-
miento similar cuando los argumentos toman valores muy grandes. 
Las medidas asintóticas se definen en términos de una función de referencia f. 
 Tanto las funciones de complejidad, T, como las funciones de referencia de las 
medidas asintóticas, f, presentan el perfil 
T, f : ℵ → ℜ+ 
donde ℵ es el dominio del tamaño de los datos y ℜ+ el valor del coste del al-
goritmo. 
 Definimos la medida asintótica de cota superior f, que notaremos 
O( f ) 
como el conjunto de funciones 
{ T | existen c ∈ ℜ+, n0 ∈ ℵ tales que, 
para todo n ≥ n0, T(n) ≤ c ⋅ f(n) } 
Si T ∈ O( f ) decimos que “T(n) es del orden de f(n)” y que 
“f es asintóticamente una cota superior del crecimiento de T”. 
 Definimos la medida asintótica de cota inferior f, que notaremos 
Ω( f ) 
como el conjunto de funciones 
{ T | existen c ∈ℜ+, n0 ∈ ℵ tales que, 
para todo n ≥ n0, T(n) ≥ c ⋅ f(n) } 
Si T ∈ Ω( f ) podemos decir que “f es asintóticamente una cota inferior del 
crecimiento de T”. 
 Definimos la medida asintótica exacta f, que notaremos 
Θ( f ) 
como el conjunto de funciones 
{ T | existen c1, c2 ∈ ℜ+, n0 ∈ ℵ, tales que, 
para todo n ≥ n0, c1 ⋅ f(n) ≤ T(n) ≤ c2 ⋅ f(n) } 
Si T ∈ Θ( f ) decimos que “T(n) es del orden exacto de f(n)”. 
 
Análisis de la eficiencia 8 
 
 Ejemplos 
— f ∈ O( f ) 
— (n+1)2 ∈ O(n2), con c=4, n0=1 
— 3n3+2n2 ∈ O(n3), con c=5, n0=0 
— P(n) ∈ O(nk), para todo polinomio P de grado k 
— 3n ∉ O(2n). Si lo fuese existirían c ∈ ℜ+, n0 ∈ ℵ tales que 
 ∀ n : n ≥ n0 : 3n ≤ c ⋅ 2n 
⇒ ∀ n ≥ n0 : (3/2)n ≤ c 
⇔ falso 
pues limn→∞(3/2)n = ∞ 
— f ∈ Ω( f ) 
— n ∈ Ω(2n) con c = ½, n0=0 
— (n+1)2 ∈ Ω(n2) con c = 1 y n0=1 
— P(n) ∈ Ω(nk) para todo polinomio P de grado k 
— f ∈ Θ( f ) 
— 2n ∈ Θ(n) con c1=1, c2=2 y n0=0 
— (n+1)2 ∈ Θ(n2) con c1=1, c2 = 4, n0=1 
— P(n) ∈ Θ(nk) para todo polinomio P de grado k 
 
Estas afirmaciones se pueden demostrar por inducción sobre n, una vez fijadas 
las constantes c −c1 , c2− y n0. 
 
 Se puede demostrar que 
Θ( f ) = O( f ) ∩ Ω( f ) 
o lo que es igual 
T ∈ Θ( f ) ⇔ T ∈ O( f ) ∧ T ∈ Ω( f ) 
 
Análisis de la eficiencia 9 
 
1.3 Ordenes de complejidad 
 
 Nos referiremos a las medidas asintóticas aplicadas a funciones concretas co-
mo órdenes. 
Algunos órdenes tienen nombres particulares: 
 
O(1) → orden constante O(n2) → orden cuadrático 
O(log n) → orden logarítmico O(n3) → orden cúbico 
O(n) → orden lineal O(nk) → orden polinómico 
O(n log n) → orden cuasi-lineal O(2n) → orden exponencial 
 
 Es posible demostrar que se cumplen las siguientes relaciones de inclusión en-
tre los órdenes de complejidad, que permiten definir una jerarquía de órdenes de 
complejidad (a saberse): 
O(1) ⊂ O(log n) ⊂ O( n ) ⊂ O(n) ⊂ O(n log n) ⊂ 
O(n2) ⊂ O(n3) ⊂ ... ⊂ O(nk) ⊂ ... 
O(2n) ⊂ O(n!) 
 
1,0E+00
1,0E+02
1,0E+04
1,0E+06
1,0E+08
1,0E+10
0 20 40 60 80 100
 
 
 
2n
n3 
n2 
n log n
n 
log n
Análisis de la eficiencia 10 
 
 Debemos recordar que los órdenes de complejidad son medidas asintóticas. 
Para datos pequeños, y si las constantes multiplicativas son distintas, nos po-
demos encontrar situaciones como ésta 
0
100.000
200.000
300.000
400.000
0 200 400 600
 
 
Para determinar con exactitud las constantes multiplicativas, es necesario reali-
zar medidas empíricas. 
¿Cómo determinarías la función exacta a partir de medidas empíricas? 
 
10.000 log n 
n2
Análisis de la eficiencia 11 
 
¿La eficiencia importa? 
 ¿Para qué emplear tiempo en diseñar algoritmos eficientes si los ordenadores 
van cada vez más rápido? 
— En una computadora capaz de procesar un dato cada 10–4 seg., ¿cuánto se 
tarda en ejecutar un algoritmo A1 de coste exponencial 2n ? 
 
n Tiempo 
10 ≈ 1 décima de seg. 
20 ≈ 2 min. 
30 > 1 día 
40 > 3 años 
 
— ¿y con una máquina que va 100 veces más rápido?, es decir, que procesa un 
dato en 10–6 seg 
Se tardarían sólo 35 años en procesar 50 datos 
 
— ¿Y un algoritmo A2 de coste cúbico n3 en la máquina antigua (10–4 seg. por 
dato)? 
 
n tiempo 
10 ≈ 1 décima de seg. 
1000 ≈ 1 día 
7000 ≈ 1 año 
 
— Por último, para un algoritmo de complejidad lineal n la máquina antigua es 
capaz de procesar 10.000 datos es un segundo. 
 
 
 
 
 
Análisis de la eficiencia 12 
 
 Dados dos algoritmos A y B que resuelven el mismo problema pero con com-
plejidades diferentes: 
TA ∈ O(log n) TB ∈ O(2n) 
— A puede consumir más memoria que B 
— Para A, aumentar “mucho” el tamaño de los datos aumenta “poco” el tiem-
po necesario; y aumentar “poco” el tiempo disponible −la velocidad de la 
máquina− aumenta “mucho” el máximo tamaño tratable: 
log(2n) = 1 + log n 
al doblar el tamaño de los datos, sólo se suma 1 al tiempo 
2 ⋅ log n = log n2 
al doblar la velocidad, los datos aumentan al cuadrado 
— Para B, aumentar “poco” el tamaño de los datos aumenta “mucho” el tiem-
po necesario; y aumentar “mucho” el tiempo disponible aumenta “poco” el 
máximo tamaño tratable 
2 2n = (2n )2 
duplicar el tamaño de los datos, eleva al cuadrado el tiempo necesario 
2 ⋅ 2n = 2n+1 
duplicar el tiempo, sólo aumenta en 1 el máximo tratable 
 Conclusión: Sólo un algoritmo eficiente, con un orden de complejidad bajo, 
puede tratar grandes volúmenes de datos. 
Se suele considerar: que un algoritmo es 
— muy eficiente si su complejidad es de orden log n 
— eficiente si su complejidad es de orden nk 
— ineficiente si su complejidad es de orden 2n. 
Decimos que un problema es tratable si existe un algoritmo que lo resuelve con 
complejidad de orden menor que 2n, y que es intratable en caso contrario. 
 
 Aunque, en ocasiones, es más importante el tiempo
del programador que el de 
la CPU 
— La eficiencia no es tan importante si, por ejemplo, el programa va a ejecutar-
se pocas veces o con datos de pequeño tamaño. 
— En cualquier caso, es conveniente partir de un diseño claro, aunque sea in-
eficiente, y optimizar posteriormente. 
Análisis de la eficiencia 13 
 
1.4 Métodos de análisis de la complejidad temporal de 
los algoritmos iterativos 
 
 
 Si T1(n) ∈ O(f1(n)) y T2(n) ∈ O(f2(n)), se cumplen las siguientes propiedades 
 
— Regla de la suma 
T1(n) + T2(n) ∈ O( max( f1(n), f2(n) ) ) 
 
— Regla del producto 
T1(n) * T2(n) ∈ O( f1(n) ⋅ f2(n) ) 
 
Para demostrarlo, usamos la definición de medida asintótica de cota superior: 
existen c1, c2 ∈ ℜ+; n1, n2 ∈ ℵ tales que 
∀ n ≥ n1 T1(n) ≤ c1 ⋅ f1(n) ∀ n ≥ n2 T2(n) ≤ c2 ⋅ f2(n) 
tomando n0 = max(n1, n2) 
 
— suma 
T1(n) + T2(n) ≤ c1 ⋅ f1(n) + c2 f2(n) 
 ≤ (c1+c2) ⋅ max( f1(n), f2(n) ) 
por lo que para n ≥ n0 se cumple la propiedad 
 
— producto 
T1(n) ⋅ T2(n) ≤ c1 ⋅ f1(n) ⋅ c2 f2(n) 
 ≤ (c1 ⋅ c2) ⋅ ( f1(n) ⋅ f2(n) ) 
por lo que para n ≥ n0 se cumple la propiedad 
 
Análisis de la eficiencia 14 
 
 Veamos cuál es la complejidad para cada una de las instrucciones básicas de un 
lenguaje imperativo como C++. 
Esto es una simplificación que puede no funcionar en todos los casos. 
 
 Si tenemos un algoritmo A de la forma 
 
— x = e 
entonces TA(n) ∈ O(1), excepto si la evaluación de e es compleja, en cuyo 
caso TA(n) será del orden de evaluar dicha expresión. 
 
— A1 ; A2 y TAi(n) ∈ O(fi (n)) 
entonces 
TA(n) = TA1(n)+TA2(n) 
y por la regla de la suma TA(n) ∈ O( max( f1(n), f2(n) ) ) 
 
— if ( B1 ) A1 else if ... else if ( Br ) then Ar y 
TAi(n) ∈ O(fi (n)) (1 ≤ i ≤ n) 
entonces TA(n) ∈ O( max( f1(n), ... , fr(n) ) ) 
Siempre que podamos suponer que la evaluación de las condiciones Bi con-
sume tiempo constante. Si esta suposición no es razonable y sabemos que 
TBi(n) ∈ O(gi(n)) (1 ≤ i ≤ n) 
entonces TA(n) ∈ O( max( g1(n), ... , gr(n), f1(n), ... , fr(n) ) ) 
Análisis de la eficiencia 15 
 
— while ( B ) A1 y 
TA1(n) ∈ O(f(n)) TB (n) ∈ O(g(n)) 
tomamos el máximo de ambas 
h(n) =def max( g(n), f(n)) 
de forma que el tiempo de ejecución de una vuelta del bucle será O(h(n)) 
Si podemos estimar que el número de vueltas en el caso peor es O(t(n)), en-
tonces 
TA(n) ∈ O(h(n) ⋅ t(n)) 
 
Generalmente es posible realizar una estimación más fina de TA(n), esti-
mando por separado el tiempo de cada vuelta y calculando un sumatorio. 
Si el tiempo de ejecución de la vuelta número i es O(h(n, i)) entonces: 
TA(n) ∈ O( Σ i : 1 ≤ i ≤ t(n) : h(n, i ) ) 
 
Análisis de la eficiencia 16 
 
 Ejemplo 
 
const int N = 10; 
int v[N][N]; 
bool b; 
int i, j; 
 
 b = true; 
 for ( i = 0; i < N-1; i++ ) 
 for ( j = i+1; j < N; j++ ) 
 b = b && (v[i][j] == v[j][i]) 
 
Como tamaño de los datos tomamos N. 
Suponemos que todas las asignaciones, evaluación de condiciones y acceso a 
vectores tienen complejidad constante O(1) 
— cuerpo del bucle interno: 2 
— coste del bucle interno: ((N–i–1) ⋅ (2+1)) + 1 = 3 ⋅ (N−i) – 2 
— cuerpo del bucle externo: (3 ⋅ (N−i) – 2) + 1 = 3 ⋅ (N−i) – 1 
— coste del bucle externo 
 [ ∑
−
=
2
0
N
i
(3 ⋅ (N−i) – 1 + 1)] + 1 
 = 3 ⋅ ∑
−
=
2
0
N
i
 (N−i) + 1 
 = 3 ⋅ ∑
−
=
2
0
N
i
N − 3 ⋅ ∑
−
=
2
0
N
i
i + 1 
 = 3 ⋅ (N –1) ⋅ N – 3 ⋅ 
2
)1()2( −⋅− NN + 1 
 = 3 N2 – 3 N – 3/2 N2 + 9/2 N – 3 + 1 
 = 3/2 N2 + 3/2 N – 2 
— y el coste del algoritmo, considerando la asignación inicial 
TA(N) = 3/2 N2 + 3/2 N – 2 + 1 = 3/2 N2 + 3/2 N – 1 
 
con lo que TA(N) ∈ O(N2) 
 
∑
=
+−⋅
+
=
max
min
)1min(max
2
maxmin
i
i
Análisis de la eficiencia 17 
 
 En lugar de las reglas generales, en muchos casos es suficiente estimar el orden 
de complejidad a partir de la acción característica. Las acciones características de 
un algoritmo son aquellas que se ejecutan con mayor frecuencia. 
El orden de magnitud de TA(n) se podrá estimar calculando el número de eje-
cuciones de las acciones características. 
— Ejemplo 
const int N = 10; 
 
bool esSim( int a[N][N] ) { 
 int i, j; 
 bool b; 
 
 b = true; 
 i = 0; 
 while ( (i < N-1) && b ) { 
 j = i+1; 
 while ( (j < N) && b ) { 
 b = b && ( a[i][j] == a[j][i] ); 
 j++; 
 } 
 i++; 
 } 
 return b; 
} 
Consideramos como acciones características las asignaciones que componen el 
cuerpo del bucle más interno. El bucle interno se ejecuta N–i–1 veces para ca-
da valor de i 
T(N) ≈ ∑
−
=
2
0
N
i
(N – i – 1) 
= ∑
−
=
2
0
N
i
N – ∑
−
=
2
0
N
i
i – ∑
−
=
2
0
N
i
1 
= (N–1) ⋅ N – 
2
)1()2( −⋅− NN – (N–1) 
= N2 – N – 1/2 N2 + 3/2 N – 1 – N + 1 
= 1/2 N2 – 1/2 N 
Por lo tanto T(N) ∈ O(N2) 
¿Cuál de las dos versiones del algoritmo es más eficiente? 
Análisis de la eficiencia 18 
 
 Ejemplo 
 
int x, y, p; 
 
// Pre.: x == X, y == Y, y >= 0 
p = 0; 
while ( y != 0 ) { 
 if ( (y % 2) == 0 ) { 
 x = x + x; 
 y = y / 2; 
 } 
 else { 
 p = p + x; 
 y = y – 1; 
 } 
} 
// Post.: p = X*Y 
 
Como tamaño de los datos tomamos n = Y 
La acción característica es el cuerpo del bucle, que tiene complejidad constante 
O(1). Por tanto, la complejidad del bucle será el número de vueltas 
TA(n) ∈ O(t(n)) 
 
Si en cada vuelta el tamaño de los datos se divide por 2 hasta llegar a 1, ¿cuán-
tas vueltas da el bucle? 
El tamaño de los datos en las sucesivas iteraciones será: 
n, n/2, n/4, ... , 1 
o lo que es igual 
n/20, n/21, n/22, ... , n/2k 
siendo k el número de veces que se ha ejecutado el bucle, de forma que 
 1 = n/2k 
⇔ 2k = n 
⇔ k = log n 
Con lo que, si t(n) ∈ O(log n) entonces TA(n) ∈ O(log n) 
 
¿ Este razonamiento es válido para cualquier valor de n ? 
Análisis de la eficiencia 19 
 
 
 Podemos demostrar este resultado. 
Estudiando algunos valores concretos de n 
n = 0 : y = 0 
n = 1 : y = 1 y = 0 
n = 2 : y = 2 y = 1 y = 0 
n = 3 : y = 3 y = 2 y = 1 y = 0 
n = 4 : y = 4 y = 2 y = 1 y = 0 
n = 5 : y = 5 y = 4 y = 2 y = 1 y = 0 
n = 6 : y = 6 y = 3 y = 2 y = 1 y = 0 
n = 7 : y = 7 y = 6 y = 3 y = 2 y = 1 y = 0 
n = 8 : y = 8 y = 4 y = 2 y = 1 y = 0 
podemos formular la siguiente conjetura 
t(n) ≤ 2 log n para n ≥ 2 
que demostramos por inducción sobre n 
 
— casos base1 
n = 2 
t(n) = 2 = 2 log 2 
n = 3 
t(n) = 3 ≤ 2 log 3 
que se puede demostrar de la siguiente forma 
 3 ≤ 2 log 3 
⇔ 23 ≤ 22 log 3 
⇔ 23 ≤ (2log 3)2 
⇔ 23 ≤ 32 
⇔ 8 ≤ 9 
⇔ cierto 
 
 
1 No podemos usar el 1 como caso base porque log 1 = 0; y necesitamos incluir el caso base del 3 porque no entra en los ca-
sos inductivos (n = 2n’ +1, n’ ≥ 2). 
Análisis de la eficiencia 20 
 
— pasos inductivos 
— n > 3, par n = 2n’, n’ ≥ 2 
HI: t(n’) ≤ 2 log n’ 
Para llegar al valor n’ desde el valor n hay que dar una sola pasada por el 
bucle, por lo tanto: 
t(n) = 1 + t(n’ ) ≤HI 1 + 2 log n’ 
 < 2 ⋅ ( 1 + log n’ ) 
 = 2 ⋅ log(2 ⋅ n’ ) 
 = 2 ⋅ log n 
— n > 3, impar n = 2n’ +1, n’ ≥ 2 
HI: t(n’) ≤ 2 log n’ 
Para llegar al valor n’ desde el valor n hay que dar dos pasadas por el bu-
cle, una para llegar a 2n’ y otra para llegar a n’, por lo tanto: 
t(n) = 2 + t(n’ ) ≤HI 2 + 2 log n’ 
 = 2 ⋅ ( 1 + log n’ ) 
 = 2 ⋅ log(2 n’ ) 
 < 2 ⋅ log( 2n’ + 1 ) 
 = 2 ⋅ log n 
 
 
 
 
 
EDI todo/Tema2.pdf
 
TEMA 2 
DISEÑO DE ALGORITMOS RECURSIVOS 
 
1. Introducción a la recursión 
2. Diseño de algoritmos recursivos 
3. Análisis de algoritmos recursivos 
4. Transformación de la recursión final a forma iterativa 
5. Técnicas de generalización 
 
 
 
 
 
 
 
 
 
 
 
 
 
Bibliografía: Fundamentals of Data Structures in C++ 
E. Horowitz, S. Sahni, D. Mehta 
Computer Science Press, 1995 
Data Abstraction and Problem Solving with C++, Second
Edition 
Carrano, Helman y Veroff 
 
Diseño de algoritmos recursivos 1 
 
2.1 Introducción a la recursión 
 Optamos por una solución recursiva cuando sabemos cómo resolver de mane-
ra directa un problema para un cierto conjunto de datos, y para el resto de los 
datos somos capaces de resolverlo utilizando la solución al mismo problema 
con unos datos “más simples”. 
 
 Cualquier solución recursiva se basa en un análisis (clasificación) de los datos, 
xr , para distinguir los casos de solución directa y los casos de solución recursi-
va: 
— caso(s) directo(s): xr es tal que el resultado yr puede calcularse directamente 
de forma sencilla. 
— caso(s) recursivo(s): sabemos cómo calcular a partir de xr otros datos más 
pequeños xr ’, y sabemos además cómo calcular el resultado yr para xr supo-
niendo conocido el resultado yr ’ para xr ’. 
 
 Para implementar soluciones recursivas en un lenguaje de programación tene-
mos que utilizar una acción que se invoque a sí misma −con datos cada vez 
“más simples”−: funciones o procedimientos. 
 
 Para entender la recursividad, a veces resulta útil considerar cómo se ejecutan 
las acciones recursivas en una computadora, como si se crearan múltiples co-
pias del mismo código, operando sobre datos diferentes (en realidad sólo se 
copian las variables locales y los parámetros por valor). 
El ejemplo clásico del factorial: 
 
int fact ( int n ) { 
// P : n >= 0 
 
 int r; 
 
 if ( n == 0 ) r = 1; 
 else if ( n > 0 ) r = n * fact(n–1); 
 return r; 
 
// Q : devuelve n! 
} 
 
¿Cómo se ejecuta la llamada fact(2)? 
Diseño de algoritmos recursivos 2 
 
Programa
principal
.
.
.
x := fact(2)
Programa
principal
.
.
.
x := fact(2)
fact(2)
.
.
.
r := n * fact(n-1)
Programa
principal
.
.
.
x := fact(2)
fact(2)
.
.
.
r := n * fact(n-1)
fact(1)
.
.
.
r := n * fact(n-1)
Programa
principal
.
.
.
x := fact(2)
fact(2)
.
.
.
r := n * fact(n-1)
fact(1)
.
.
.
r := n * fact(n-1)
fact(0)
.
.
.
r := 1
 
 
Diseño de algoritmos recursivos 3 
 
Programa
principal
.
.
.
x := fact(2)
fact(2)
.
.
.
r := n * fact(n-1)
fact(1)
.
.
.
r := n * fact(n-1)
Programa
principal
.
.
.
x := fact(2)
fact(2)
.
.
.
r := n * fact(n-1)
Programa
principal
.
.
.
x := fact(2)
 
 
 
 ¿La recursión es importante? 
— Un método muy potente de diseño y razonamiento formal. 
— Tiene una relación natural con la inducción y, por ello, facilita conceptual-
mente la resolución de problemas y el diseño de algoritmos. 
— Algunos lenguajes no incluyen instrucciones iterativas. 
 
Diseño de algoritmos recursivos 4 
 
2.1.1 Recursión simple 
 
 Decimos que una acción recursiva tiene recursión simple si cada caso recursivo 
realiza exactamente una llamada recursiva. Abusando de la notación en las 
asignaciones, podemos describirlo mediante el esquema general: 
 
void nombreProc ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
// declaración de constantes 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 δ1 y1’ ; ... ; δm ym’ ; // yr ’ 
 
 if ( d(xr ) ) 
 yr = g(xr ); 
 else if ( ¬d(xr ) ) { 
 xr ‘ = s(xr ); 
 nombreProc(xr ‘, yr ‘); 
 yr = c(xr , yr ‘); 
 } 
// Postcondición 
} 
 
donde: 
— xr representa a los parámetros de entrada x1, ... , xn, x
r ’ a los parámetros de 
la llamada recursiva x1’, ... , xn’, y
r ’ a los resultados de la llamada recursiva y1’, 
... , ym’, e y
r a los parámetros de salida y1, ... , ym. 
— d(xr ) es la condición que determina el caso directo 
— ¬d(xr ) es la condición que determina el caso recursivo 
— g calcula el resultado en el caso directo 
— s, la función sucesor, calcula los argumentos para la siguiente llamada recursiva 
— c, la función de combinación, obtiene la combinación de los resultados de la lla-
mada recursiva yr ’ junto con los datos de entrada xr , proporcionando así el 
resultado yr . 
 
 A la recursión simple también se la conoce como recursión lineal porque el nú-
mero de llamadas recursivas depende linealmente del tamaño de los datos. 
 
 Veamos cómo la función factorial se ajusta a este esquema de declaración: 
Diseño de algoritmos recursivos 5 
 
int fact ( int n ) { 
// P : n >= 0 
 
 int r; 
 
 if ( n == 0 ) r = 1; 
 else if ( n > 0 ) r = n * fact(n–1); 
 return r; 
 
// Q : devuelve n! 
} 
d(n) ⇔ n == 0 
g(n) = 1 
¬d(n) ⇔ n > 0 
s(n) = n−1 
c(n, fact(s(n))) = n * fact(s(n)) 
 
 d(xr ) y ¬d(xr ) pueden desdoblarse en una alternativa con varios casos. 
Ejemplo: multiplicación de dos naturales por el método del campesino egipcio 
 
int prod ( int a, int b ) { 
// P : ( a >= 0 ) && ( b >= 0 ) 
 int r; 
 if ( b == 0 ) r = 0; 
 else if ( b == 1 ) r = a; 
 else if ( b > 1 && (b % 2 == 0) ) r = prod(2*a, b/2); 
 else if ( b > 1 && (b % 2 == 1) ) r = prod(2*a, b/2) + a; 
 return r; 
// Q : r = a * b 
} 
 
d1(a, b) ⇔ b == 0 d2(a, b) ⇔ b == 1 
g1(a, b) = 0 g2(a, b) = 1 
¬d1(a, b) ⇔ b > 1 && par(b) ¬d2(a, b) ⇔ b > 1 && impar(b) 
s1(a, b) = (2*a, b/2) s2(a, b) = (2*a, b/2) 
c1(a, b, prod(s1(a, b))) = prod(s1(a, b)) c2(a, b, prod(s2(a, b))) = prod(s2(a, b))+a 
 
Recursión final 
 La recursión final (tail recursion) es un caso particular de recursión simple donde la 
función de combinación se limita a transmitir el resultado de la llamada recur-
Diseño de algoritmos recursivos 6 
 
siva. Se llama final porque lo último que se hace en cada pasada es la llamada 
recursiva. 
El resultado será siempre el obtenido en uno de los casos base. 
Los algoritmos recursivos finales tienen la interesante propiedad de que pue-
den ser traducidos de manera directa a soluciones iterativas, más eficientes. 
 
 Como ejemplo de función recursiva final veamos el algoritmo de cálculo del 
máximo común divisor por el método de Euclides. 
 
int mcd( int a, int b ){ 
// P : ( a > 0 ) && ( b > 0 ) 
 
 int m; 
 
 if ( a == b ) m = a; 
 else if ( a > b ) m = mcd(a–b, b); 
 else if ( a < b ) m = mcd(a, b–a); 
 return m; 
// Q : devuelve mcd(a, b) 
} 
 
que se ajusta al esquema de recursión simple: 
d(a, b) ⇔ a == b 
g(a, b) = a 
¬d1(a, b) ⇔ a > b ¬d2(a, b) ⇔ a < b 
s1(a, b) = (a−b, a) s2(a, b) = (a, b−a) 
 
y donde las funciones de combinación se limitan a devolver el resultado de la 
llamada recursiva 
c1(a, b, mcd(s1(a, b))) = mcd(s1(a, b)) 
c2(a, b, mcd(s2(a, b))) = mcd(s2(a, b)) 
 
 
Diseño de algoritmos recursivos 7 
 
2.1.2 Recursión múltiple 
 
 Este tipo de recursión se caracteriza por que, al menos en un caso recursivo, se 
realizan varias llamadas recursivas. El esquema correspondiente: 
void nombreProc ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
// declaración de constantes 
 τ1 x11 ; ... ; τn x1n ; ... τ1 xk1 ; ... ; τn xkn ; 
 δ1 y11 ; ... ; δm y1m ; ... δ1 yk1 ; ... ; δm ykm ; 
 
 if ( d(xr ) ) 
 yr = g(xr ); 
 else if ( ¬d(xr ) ) { 
 xr 1 = s1(xr ); 
 nombreProc(xr 1, yr 1); 
 ... 
 xr k = sk(xr ); 
 nombreProc(xr k, yr k); 
 yr = c(xr , yr 1, ... , yr k); 
 } 
// postcondición 
} 
 
donde 
— k > 1, indica el número de llamadas recursivas 
— xr representa a los parámetros de entrada x1,..., xn, x
r
i a los parámetros de la 
i-ésima llamada recursiva xi1,..., xin, y
r
i a los resultados de la i-ésima llamada re-
cursiva yi1,..., yim, para i = 1,..., k, e y
r a los parámetros de salida y1,..., ym 
— d(xr ) es la condición que determina el caso directo 
— ¬d(xr ) es la condición que determina el caso recursivo 
— g calcula el resultado en el caso directo 
— si , las funciones sucesor, calculan la descomposición de los datos de entrada pa-
ra realizar la i-ésima llamada recursiva, para i = 1, ... , k 
— c, la función de combinación, obtiene la combinación de los resultados yr i de las
llamadas recursivas, para i = 1, ... , k, junto con los datos de entrada xr , pro-
porcionando así el resultado yr . 
 
 Otro ejemplo clásico, los números de Fibonacci: 
Diseño de algoritmos recursivos 8 
 
int fibo( int n ) { 
// Pre: n >= 0 
 
 int r; 
 
 if ( n == 0 ) r = 0; 
 else if ( n == 1 ) r = 1; 
 else if ( n > 1 ) r = fibo(n-1) + fibo(n-2); 
 return r; 
// Post: devuelve fib(n) 
} 
que se ajusta al esquema de recursión múltiple (k = 2) 
d1(n) ⇔ n == 0 d2(n) ⇔ n == 1 
g(0) == 0 g(1) == 1 
¬d(n) ⇔ n > 1 
s1(n) = n−1 s2(n) = n−2 
c(n, fibo(s1(n)), fibo(s2(n))) = fibo(s1(n)) + fibo(s2(n)) 
 En la recursión múltiple, el número de llamadas no depende linealmente del 
tamaño de los datos. Por ejemplo, el cómputo de fib(4) 
fib(4)
fib(2) fib(3)
fib(1)fib(0) fib(1) fib(2)
fib(0) fib(1)
0 1
0 1
0+1=1
0+1=1
1
1+1=2
1+2=3
 
Nótese que algunos valores se computan más de una vez. 
 Para terminar con la introducción, recapitulamos los distintos tipos de funcio-
nes recursivas que hemos presentado: 
— Simple. Una llamada recursiva en cada caso recursivo 
— No final. Requiere combinación de resultados 
— Final. No requiere combinación de resultados 
— Múltiple. Más de una llamada recursiva en algún caso recursivo. 
Diseño de algoritmos recursivos 9 
 
2.2 Diseño de algoritmos recursivos 
 
 Dada la especificación { P } A { Q }, hemos de obtener una acción A que la 
satisfaga. 
Nos planteamos implementar A como una función o un procedimiento recur-
sivo cuando podemos obtener –fácilmente– una definición recursiva de la 
postcondición. 
 
 Proponemos un método que descompone la obtención del algoritmo recursivo 
en varios pasos: 
 
(R.1) Planteamiento recursivo. Se ha de encontrar una estrategia recursiva para 
alcanzar la postcondición, es decir, la solución. A veces, la forma de la post-
condición, o de las operaciones que en ella aparecen, nos sugerirá directa-
mente una estrategia recursiva. 
 
(R.2) Análisis de casos. Se trata de obtener las condiciones que permiten discri-
minar los casos directos de los recursivos. Deben tratarse de forma exhaus-
tiva y mutuamente excluyente todos los casos contemplados en la 
precondición: 
P0 ⇒ d(xr ) ∨ ¬ d(xr ) 
 
(R.3) Caso directo. Hemos de encontrar la acción que resuelve el caso directo 
{ P0 ∧ d(xr ) } A1 { Q0 } 
Si hubiese más de un caso directo, repetiríamos este paso para cada uno de 
ellos. 
 
(R.4) Descomposición recursiva. Se trata de obtener la función sucesor s(xr ) 
que nos proporciona los datos que empleamos para realizar la llamada recur-
siva. 
Si hay más de un caso recursivo, obtenemos la función sucesor para cada uno 
de ellos. 
Diseño de algoritmos recursivos 10 
 
(R.5) Función de acotación y terminación. Determinamos si la función sucesor 
escogida garantiza la terminación de las llamadas, obteniendo una función 
que estime el número de llamadas restantes hasta alcanzar un caso base –la 
función de acotación– y justificando que se decrementa en cada llamada. 
Si hay más de un caso recursivo, se ha de garantizar la terminación para cada 
uno de ellos. 
 
(R.6) Llamada recursiva. Pasamos a ocuparnos entonces del caso recursivo. Ca-
da una de las descomposiciones recursivas ha de permitir realizar la(s) lla-
mada(s) recursiva(s). 
P0 ∧ ¬d(xr ) ⇒ P0[xr /s(xr )] 
(R.7) Función de combinación. Lo único que nos resta por obtener del caso re-
cursivo es la función de combinación, que, en el caso de la recursión simple, 
será de la forma yr = c(xr , yr '). 
 
Si hubiese más de un caso recursivo, habría que encontrar una función de 
combinación para cada uno de ellos. 
(R.8) Escritura del caso recursivo. Lo último que nos queda por decidir es si 
necesitamos utilizar en el caso recursivo todas las variables auxiliares que 
han ido apareciendo. Partiendo del esquema más general posible 
{ P0 ∧ ¬d(xr ) } 
 xr ’ = s(xr ); 
 nombreProc(xr ’, yr ’); 
 yr = c(xr , yr ’) 
{ Q0 } 
a aquel donde el caso recursivo se reduce a una única sentencia 
{ P0 ∧ ¬d(xr ) } 
 yr = c(xr , nombreFunc(s(xr ))) 
{ Q0 } 
Repetimos este proceso para cada caso recursivo, si es que tenemos más de 
uno, y lo generalizamos de la forma obvia cuando tenemos recursión múl-
tiple. 
 
 Ejemplo: una función que dado un natural n calcule la suma de los dobles de 
los naturales hasta n: 
Diseño de algoritmos recursivos 11 
 
Especificación 
int sumaDoble ( int n ) { 
// Pre : n >= 0 
 int s; 
 
// cuerpo de la función 
 
 return s; 
// Post : devuelve Σ i : 0 ≤ i ≤ n : 2 * i 
} 
 
(R.1) (R.2) Planteamiento recursivo y análisis de casos 
El problema se resuelve trivialmente cuando n == 0, donde el resultado es 0. 
La distinción de casos queda entonces: 
 d(n) : n == 0 
¬d(n) : n > 0 
que cubren exhaustivamente todos los casos posibles 
n ≥ 0 ⇒ n == 0 ∨ n > 0 
La estrategia recursiva se puede obtener manipulando la expresión que forma-
liza el resultado a obtener 
Σ i : 0 ≤ i ≤ n : 2 * i = (Σ i : 0 ≤ i ≤ n-1 : 2 * i) + 2 
* n 
que nos lleva directamente a la estrategia recursiva: 
sumaDoble(n) = sumaDoble(n–1) + 2*n 
 
(R.3) Solución en el caso directo. 
{ n == 0 } 
 A1 
{ s == Σ i : 0 ≤ i ≤ n : 2 * i } 
Se resuelve trivialmente con la asignación 
A1 ≡ s = 0 
 
(R.4) Descomposición recursiva. 
La descomposición recursiva ya aparecía en el planteamiento recursivo 
s(n) = n–1 
 
(R.5) Función de acotación y terminación. 
El tamaño del problema viene dado por el valor de n, que ha de disminuir 
en las sucesivas llamadas recursivas hasta llegar a 0: 
t(n) = n 
Diseño de algoritmos recursivos 12 
 
Efectivamente, se decrementa en cada llamada recursiva: 
t(s(n)) < t(n) ⇔ n–1 < n 
 
(R.6) Es posible hacer la llamada recursiva. En el caso recursivo se cumple 
P0(n) ∧ ¬d(n) ⇒ P0(s(n)) 
n ≥ 0 ∧ n > 0 ⇒ n > 0 ⇒ n – 1 ≥ 0 
 
(R.7) Función de combinación. 
Esta función aparecía ya en el planteamiento recursivo. La postcondición 
se puede alcanzar con una simple asignación: 
s = s’ + 2*n 
(R.8) Escritura del caso recursivo 
Siguiendo el esquema teórico, tenemos que el caso recursivo se escribe: 
{ P0 ∧ n > 0 } 
 n’ = n-1; 
 s’ = sumaDoble(n’); 
 s = s’ + 2*n; 
{ Q0 } 
Podemos evitar el uso de las dos variables auxiliares, simplemente inclu-
yendo las expresiones correspondientes en la función de combinación: 
 s = sumaDoble(n–1) + 2*n; 
Con todo esto, la función queda: 
int sumaDoble ( int n ) { 
// Pre: n >= 0 
 int s; 
 if ( n == 0 ) s = 0; 
 else if ( n > 0 ) s = sumaDoble( n-1 ) + 2*n; 
 return s; 
// Post: devuelve Σ i : 0 ≤ i ≤ n : 2*i 
} 
Diseño de algoritmos recursivos 13 
 
 Ejemplo: suma de las componentes de un vector de enteros. 
Especificación 
int sumaVec ( int v[], int num ) { 
// Pre : v es un array de al menos num elementos 
 int s; 
 
// cuerpo de la función 
 
 return s; 
// devuelve Σ i : 0 ≤ i < num : v[i] 
} 
 
(R.1) (R.2) Planteamiento recursivo y análisis de casos 
El problema se resuelve trivialmente cuando num == 0, porque la suma de las 
0 primeras componentes de un vector es 0. Por lo tanto, distinguimos los ca-
sos 
 d(v, num) ≡ num == 0 
¬d(v, num) ≡ num > 0 
¿qué ocurre si num < 0? 
Podemos optar por: 
— exigir en la precondición que sólo son válidas invocaciones donde num 
sea ≥ 0, o 
— tratar también el caso en el que num < 0 devolviendo también 0 
Optando por la segunda solución: d(v, num) ≡ num ≤ 0 
P0 ⇒ num < 0 ∨ num == 0 ∨ num > 0 ⇔ cierto 
El planteamiento recursivo puede ser: para obtener la suma de las num com-
ponentes de un vector, obtenemos la suma de las num–1 primeras y le suma-
mos la última. 
sumaVec(v, num) = sumaVec(v, num-1) + v[num-1] 
 
(R.3) Solución en el caso directo. 
{ P0 ∧ num ≤ 0 } 
 A1 
{ s = Σ i : 0 ≤ i < num : v[i] } 
A1 ≡ s = 0; 
 
 
(R.4) Descomposición recursiva. 
s(v, num) = (v,
num-1) 
Diseño de algoritmos recursivos 14 
 
 
(R.5) Función de acotación y terminación. 
Al avanzar la recursión, num se va acercando a 0 
t(v, num) = num 
Se cumple 
num - 1 < num 
 
(R.6) Llamada recursiva. 
 v es un array de al menos num elementos ∧ num > 0 
 ⇒ v es un array de al menos num-1 elementos 
 
(R.7) Función de combinación. 
Se resuelve con la asignación 
s = s’ + v[num-1]; 
 
(R.8) Escritura del caso recursivo. 
Bastará con la asignación 
s = sumaVec(v, num-1) + v[num-1]; 
 
De forma que la función resultante queda: 
int sumaVec ( int v[], int num ) { 
// Pre : v es un array de al menos num elementos 
 int s; 
 
 if ( num <= 0 ) s = 0; 
 else if ( num > 0 ) s = sumaVec(v, num-1) + v[num-1]; 
 return s; 
// devuelve Σ i : 0 ≤ i < num : v[i] 
} 
Diseño de algoritmos recursivos 15 
 
Implementación recursiva de la búsqueda binaria 
 
 Partimos de un vector ordenado, donde puede haber elementos repetidos, y 
un valor x que pretendemos encontrar en el vector. Buscamos la aparición más 
a la derecha del valor x, o, si no se encuentra en el vector, buscamos la posi-
ción anterior a dónde se debería encontrar −por si queremos insertarlo−. Es 
decir, estamos buscando el punto del vector donde las componentes pasan de 
ser ≤ x a ser > x. 
La idea es que en cada pasada por el bucle se reduce a la mitad el tamaño del 
subvector donde puede estar el elemento buscado. 
 
P
≤ x > x
… …
QM = (P+Q) div 2
 
si v[m] ≤ x entonces debemos buscar a la derecha de m 
 
P
≤ x > x
… …
QM
 
y si v[m] > x entonces debemos buscar a la izquierda de m 
 
P
≤ x > x
… …
Q
M
 
 
 Como el tamaño de los datos se reduce a la mitad en cada pasada tenemos cla-
ramente una complejidad logarítmica en el caso peor (en realidad en todos los 
casos, pues aunque encontremos x en el vector hemos de seguir buscando ya 
que puede que no sea la aparición más a la derecha). 
 
 
 Hay que ser cuidadoso con los índices, sobre todo si: 
Diseño de algoritmos recursivos 16 
 
— x no está en el vector, o si, en particular, 
— x es mayor o menor que todos los elementos del vector; 
además, es necesario pensar con cuidado cuál es el caso base. 
 
 La especificación del algoritmo 
typedef int TElem; 
int buscaBin( TElem v[], int num, TElem x ) { 
// Pre: v está ordenado entre 0 .. num-1 
 int pos; 
 
// cuerpo de la función 
 
 return pos; 
// Post: devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
} 
 
Comentarios: 
— Utilizamos el tipo TElem para resaltar la idea de que la búsqueda binaria es 
aplicable sobre cualquier tipo que tenga definido un orden, es decir, los ope-
radores == y <=. 
— Si x no está en v devolvemos la posición anterior al lugar donde debería es-
tar. En particular, si x es menor que todos los elementos de v el lugar a in-
sertarlo será la posición 0 y, por lo tanto, devolvemos –1. 
 
 El planteamiento recursivo parece claro: para buscar x en un vector de n ele-
mentos tenemos que comparar x con el elemento central y 
— si x es mayor o igual que el elemento central, seguimos buscando recursiva-
mente en la mitad derecha, 
— si x es menor que el elemento central, seguimos buscando recursivamente 
en la mitad izquierda. 
El problema es ¿cómo indicamos en la llamada recursiva que se debe seguir 
buscando “en la mitad izquierda” o “en la mitad derecha”? 
Evidentemente la llamada 
buscaBin( v, num / 2, x ); 
no funciona. 
 Al diseñar algoritmos recursivos, en muchas ocasiones es necesario utilizar una 
función –o un procedimiento– auxiliar que nos permita implementar el plan-
Diseño de algoritmos recursivos 17 
 
teamiento recursivo. Estas funciones auxiliares son una generalización de la fun-
ción –o el procedimiento– a desarrollar porque 
— tienen más parámetros y/o más resultados, y 
— la función original se puede calcular como un caso particular de la función 
auxiliar. 
 
 En el caso de la búsqueda binaria, una posible generalización consiste en des-
arrollar una función que en lugar de recibir el número de elementos del vector, 
reciba dos índices, a y b, que señalen dónde empieza y dónde acaba el fragmen-
to de vector a considerar. 
int buscaBin( TElem v[], TElem x, int a, int b ) 
de esta forma, la función que realmente nos interesa se obtiene como 
buscaBin( v, num, x ) = buscaBin( v, x, 0, num-1) 
Nótese que no es necesario inventarse otro nombre para la función auxiliar 
porque C++ permite la sobrecarga de funciones: definir varias funciones con 
el mismo nombre que se distinguen por los parámetros. 
 
 La función recursiva es por tanto la función auxiliar, mientras que en la im-
plementación de la función original nos limitaremos a realizar la llamada inicial 
a la función auxiliar. 
 
int buscaBin( TElem v[], TElem x, int a, int b ) { 
 
// cuerpo de la función 
 
} 
 
int buscaBin( TElem v[], int num, TElem x ) { 
 
 return buscaBin( v, x, 0, num-1); 
} 
 
Nótese que es necesario escribir primero la función auxiliar para que sea visi-
ble desde la otra función. 
Diseño de algoritmos recursivos 18 
 
 Diseño del algoritmo 
(R.1) (R.2) Planteamiento recursivo y análisis de casos. 
Aunque el planteamiento recursivo está claro: dados a y b, obtenemos el pun-
to medio m y 
— si v[m] ≤ x seguimos buscando en m+1 .. b 
— si v[m] > x seguimos buscando en a .. m–1, 
es necesario ser cuidadoso con los índices. La idea consiste en garantizar que 
en todo momento se cumple que: 
— todos los elementos a la izquierda de a –sin incluir v[a]– son menores o 
iguales que x, y 
— todos los elementos a la derecha de b –sin incluir v[b]– son estrictamente 
mayores que x. 
Una primera idea puede ser considerar como caso base a == b. Si lo hiciése-
mos así, la solución en el caso base quedaría: 
if ( a == b ) 
 if ( v[a] == x ) p = a; 
 else if ( v[a] < x ) p = a; // x no está en v 
 else if ( v[a] > x ) p = a-1; // x no está en v 
Sin embargo, también es necesario considerar el caso base a == b+1 pues 
puede ocurrir que en ninguna llamada recursiva se cumpla a == b. Por ejem-
plo, en un situación como esta 
x == 8 a == 0 b == 1 v[0] == 10 v[1] == 15 
el punto medio m=(a+b)/2 es 0, para el cual se cumple v[m] > x y por lo tanto 
la siguiente llamada recursiva se hace con 
a == 0 b == -1 
que es un caso base donde debemos devolver –1 y donde para alcanzarlo no 
hemos pasado por a == b. 
Como veremos a continuación, el caso a==b se puede incluir dentro del caso 
recursivo si consideramos como caso base el que cumple a == b+1, que 
además tiene una solución más sencilla y que siempre se alcanza. 
Diseño de algoritmos recursivos 19 
 
Con todo lo anterior, la especificación de la función recursiva auxiliar queda: 
 
int buscaBin( TElem v[], TElem x, int a, int b ) { 
// Pre: v está ordenado entre 0 .. num-1 
// ( 0 <= a <= num ) && ( -1 <= b <= num-1 ) && ( a <= 
b+1 ) 
// todos los elementos a la izquierda de ‘a’ son <= x 
// todos los elementos a la derecha de ‘b’ son > x 
 
 int p; 
// cuerpo de la función 
 return p; 
// Post: devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
} 
 
Donde se tiene la siguiente distinción de casos 
 d(v,x,a,b) : a == b+1 
¬d(v,x,a,b) : a <= b 
para la que efectivamente se cumple 
P0 ⇒ a ≤ b+1 ⇒ a == b+1 ∨ a ≤ b 
 
(R.3) Solución en el caso directo. 
{ P0 ∧ a == b+1 } 
 A1 
{ Q0 } 
 
Si 
— todos los elementos a la izquierda de a son ≤ x, 
— todos los elementos a la derecha de b son > x, y 
— a == b+1, es decir, a y b se han cruzado, 
entonces el último elemento que cumple que es ≤ x es v[a–1], y por lo tanto, 
A1 ≡ p = a–1; 
 
Diseño de algoritmos recursivos 20 
 
(R.4) Descomposición recursiva.
Los parámetros de la llamada recursiva dependerán del resultado de comparar 
x con la componente central del fragmento de vector que va desde a hasta b. 
Por lo tanto, obtenemos el punto medio 
m = (a+b) / 2; 
de forma que 
— si x < v[m] la descomposición es 
s1(v, x, a, b) = (v, x, a, m–1) 
— si x ≥ v[m] la descomposición es 
s2(v, x, a, b) = (v, x, m+1, b) 
 
(R.5) Función de acotación y terminación. 
Lo que va a ir disminuyendo, según avanza la recursión, es la longitud del 
subvector a considerar, por lo tanto tomamos como función de acotación: 
t(v, x, a, b) = b–a+1 
y comprobamos 
a ≤ b ∧ a ≤ m ≤ b ⇒ t(s1( x
r
)) < t(xr ) ∧ t(s2( x
r
)) < t(xr ) 
que efectivamente se cumple, ya que 
a ≤ b ⇒ a ≤ (a+b)/2 ≤ b 
(m–1)–a+1 < b–a+1 ⇐ m–1 < b ⇐ m ≤ b 
b–(m+1)+1 < b–a+1 ⇔ b–m–1 < b–a ⇐ b–m ≤ b–a ⇔ m ≥ a 
 
 
Diseño de algoritmos recursivos 21 
 
(R.6) Llamada recursiva. 
La solución que hemos obtenido, sólo funciona si en cada llamada se cumple 
la precondición, por lo tanto, debemos demostrar que de una llamada a la si-
guiente efectivamente se sigue cumpliendo la precondición. 
Tratamos por separado cada caso recursivo 
— x < v[m] 
P0 ∧ a ≤ b ∧ a ≤ m ≤ b ∧ x < v[m] ⇒ P0[b/m-1)] 
Que es cierto porque: 
 v está ordenado entre 0 .. num-1 
⇒ v está ordenado entre 0 .. num-1 
 
 0 ≤ a ≤ num 
⇒ 0 ≤ a ≤ num 
 
 -1 ≤ b ≤ num-1 ∧ a ≤ m ≤ b ∧ 0 ≤ a ≤ num 
⇒ -1 ≤ m-1 ≤ num-1 
 
 a ≤ m 
⇒ a ≤ m-1+1 
 
 todos los elementos a la izquierda de ‘a’ son <= x 
⇒ todos los elementos a la izquierda de ‘a’ son <= x 
 
 v está ordenado entre 0 .. num-1 ∧ 
 todos los elementos a la derecha de ‘b’ son > x ∧ 
 m ≤ b ∧ x < v[m] 
⇒ todos los elementos a la derecha de ‘m-1’ son > x 
 
— x ≥ v[m] 
 
 v está ordenado entre 0 .. num-1 
 ( 0 <= a <= num ) && ( -1 <= b <= num-1 ) && ( a <= b+1 
) 
 todos los elementos a la izquierda de ‘a’ son <= x 
 todos los elementos a la derecha de ‘b’ son > x 
⇒ v está ordenado entre 0 .. num-1 
 ( 0 <= m+1 <= num ) && ( -1 <= b <= num-1 ) && ( m+1 <= 
b+1 ) 
 todos los elementos a la izquierda de ‘m+1’ son <= x 
 todos los elementos a la derecha de ‘b’ son > x 
 
Se razona de forma similar al anterior. 
Diseño de algoritmos recursivos 22 
 
Debemos razonar también que la llamada inicial a la función auxiliar cumple 
la precondición 
 v está ordenado entre 0 .. num-1 ∧ 
 a == 0 ∧ b == num-1 
⇒ v está ordenado entre 0 .. num-1 
 ( 0 <= a <= num ) && ( -1 <= b <= num-1 ) && ( a <= b+1 
) 
 todos los elementos a la izquierda de ‘a’ son <= x 
 todos los elementos a la derecha de ‘b’ son > x 
 
Que no es cierto si num < 0 ya que en ese caso no se cumple a ≤ b+1. De 
hecho si num < 0 no está claro qué resultado se debe devolver, por lo tanto lo 
mejor es añadir esta restricción a la precondición de la función original. 
Y con esta nueva condición (num ≥ 0 ) sí es sencillo demostrar la corrección 
de la llamada inicial: 
 
 a == 0 ∧ b == num-1 ∧ num ≥ 0 
⇒ 0 ≤ a ≤ num ∧ -1 ≤ b ≤ num-1 ∧ a ≤ b+1 
 
 a == 0 
⇒ todos los elementos a la izquierda de ‘a’ son <= x 
 
 a == num-1 
⇒ todos los elementos a la derecha de ‘b’ son > x 
 
(R.7) Función de combinación. 
En los dos casos recursivos nos limitamos a propagar el resultado de la lla-
mada recursiva: 
p = p' 
 
(R.9) Escritura de la llamada recursiva. 
Cada una de las dos llamadas recursivas se puede escribir como una sola asig-
nación: 
p = buscaBin( v, x, m+1, b ) 
y 
p = buscaBin( v, x, a, m–1 ) 
 
Diseño de algoritmos recursivos 23 
 
 Con lo que finalmente la función queda: 
int buscaBin( TElem v[], TElem x, int a, int b ) { 
// Pre: v está ordenado entre 0 .. num-1 
// ( 0 <= a <= num ) && ( -1 <= b <= num-1 ) && ( a <= 
b+1 ) 
// todos los elementos a la izquierda de ‘a’ son <= x 
// todos los elementos a la derecha de ‘b’ son > x 
 
 int p, m; 
 
 if ( a == b+1 ) 
 p = a - 1; 
 else if ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 p = buscaBin( v, x, m+1, b ); 
 else 
 p = buscaBin( v, x, a, m-1 ); 
 } 
 return p; 
// Post: devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
} 
 
int buscaBin( TElem v[], int num, TElem x ) { 
// Pre: los num primeros elementos de v están ordenados y 
// num >= 0 
 
 return buscaBin(v, x, 0, num-1); 
 
// Post : devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, 
devuelve -1 
} 
 
Nótese que la generalización está pensada como una función auxiliar y no para 
ser utilizada por sí sola, en cuyo caso deberíamos repensar la precondición y la 
postcondición. 
 
Diseño de algoritmos recursivos 24 
 
2.2.1 Algoritmos avanzados de ordenación 
 
 La ordenación rápida (quicksort) y la ordenación por mezcla (mergesort) son dos 
algoritmos de ordenación de complejidad cuasi-lineal, O(n log n). 
Las idea recursiva es similar en los dos algoritmos: para un ordenar un vector 
se procesan por separado la mitad izquierda y la mitad derecha. 
— En la ordenación rápida, se colocan los elementos pequeños a la izquierda y 
los grandes a la derecha, y luego se sigue con cada mitad por separado. 
— En la ordenación por mezcla, se ordena la mitad de la izquierda y la mitad de 
la derecha por separado y luego se mezclan los resultados. 
 
Ordenación rápida 
 
 Especificación: 
void quickSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 
 quickSort(v, 0, num-1); 
 
// Post: se han ordenado las num primeras posiciones de v 
} 
 
void quickSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
// Post: v está ordenado entre a y b 
} 
 
Como en el caso de la búsqueda binaria, la necesidad de encontrar un plan-
teamiento recursivo nos lleva a definir un procedimiento auxiliar con los pa-
rámetros a y b, para así poder indicar el subvector del que nos ocupamos en 
cada llamada recursiva. 
Diseño de algoritmos recursivos 25 
 
 El planteamiento recursivo consiste en: 
 
1. Elegir un pivote: un elemento cualquiera del subvector v[a..b]. Normalmente 
se elige v[a] como pivote. 
 
2. Particionar el subvector v[a..b], colocando a la izquierda los elementos me-
nores que el pivote y a la derecha los mayores. Los elementos iguales al pi-
vote pueden quedar indistintamente a la izquierda o a la derecha. Al final 
del proceso de partición, el pivote debe quedar en el centro, separando los 
menores de los mayores. 
 
3. Ordenar recursivamente los dos fragmentos que han quedado a la izquierda 
y a la derecha del pivote. 
 
3,5 6,2 2,8 5,0 1,1 4,5
2,8 1,1 3,5 5,0 6,2 4,5
a b
a bp
<= 3,5 >= 3,5
Partición
2,81,1 3,5 5,0 6,24,5
a b
Ordenación recursiva
 
 
Diseño de algoritmos recursivos 26 
 
 Análisis de casos 
— Caso directo: a == b+1 
El subvector está vacío y, por lo tanto, ordenado. 
— Caso recursivo: a ≤ b 
Se trata de un segmento no vacío y aplicamos el planteamiento recursivo: 
— considerar x == v[a] como elemento pivote 
— reordenar parcialmente el subvector v[a..b] para conseguir que x quede 
en la posición p que ocupará cuando v[a..b] esté ordenado. 
— ordenar recursivamente v[a .. (p–1)] y v[(p+1) .. b]. 
 
 Función de acotación: 
t(v, a, b) = b – a + 1 
 
 Suponiendo que tenemos una implementación correcta de partición, el algorit-
mo nos queda: 
 
void quickSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
 int p; 
 
 if ( a <= b ) { 
 particion(v, a, b, p); 
 quickSort(v, a, p–1); 
 quickSort(v, p+1, b); 
 } 
 
// Post: v está ordenado entre a y b 
} 
 
Diseño de algoritmos recursivos 27 
 
 Diseño de partición. 
Partimos de la especificación: 
{ P: 0 ≤ a ≤ b ≤ num-1 } 
 particion 
{
Q : 0 ≤ a ≤ p ≤ b ≤ num-1 ∧ 
 todos los elementos desde ‘a’ hasta ‘p-1’ son ≤ v[p] ∧ 
 todos los elementos desde ‘p+1’ hasta ‘b’ son ≥ v[p] 
} 
 
La idea es obtener un bucle que mantenga invariante la siguiente situación 
 
x
a i j b
<= x >= x 
 
de forma que i y j se vayan acercando hasta cruzarse, y finalmente intercam-
biemos v[a] con v[j] 
— El invariante se obtiene generalizando la postcondición con la introducción 
de dos variables nuevas, i, j, que indican el avance por los dos extremos del 
subvector 
a+1 ≤ i ≤ j+1 ≤ b+1 ∧ 
todos los elementos desde ‘a+1’ hasta ‘i-1’ son ≤ v[a] ∧ 
todos los elementos desde ‘j+1’ hasta ‘b’ son ≥ v[a] 
 
— Condición de repetición 
El bucle termina cuando se cruzan los índices i y j, es decir cuando se cum-
ple i == j+1, y, por lo tanto, la condición de repetición es 
i <= j 
A la salida del bucle, el vector estará particionado salvo por el pivote v[a]. Pa-
ra terminar el proceso basta con intercambiar los elementos de las posiciones 
a y j, quedando la partición en la posición j. 
p = j; 
aux = v[a]; 
v[a] = v[p]; 
v[p] = aux; 
 
 
— Expresión de acotación 
Diseño de algoritmos recursivos 28 
 
C : j – i + 1 
 
— Acción de inicialización 
i = a+1; 
j = b; 
Esta acción hace trivialmente cierto el invariante porque v[(a+1)..(i–1)] y 
v[(j+1) .. b] se convierten en subvectores vacíos. 
— Acción de avance 
El objetivo del bucle es conseguir que i y j se vayan acercando, y además se 
mantenga el invariante en cada iteración. Para ello, se hace un análisis de 
casos comparando las componentes v[i] y v[j] con v[a] 
— v[i] ≤ v[a] 
→ incrementamos i 
— v[j] ≥ v[a] 
→ decrementamos j 
— v[i] > v[a] && v[j] < v[a] 
→ intercambiamos v[i] con v[j], incrementamos i y decrementamos j 
De esta forma el avance del bucle queda 
 if ( v[i] <= v[a] ) i = i + 1; 
 else if ( v[j] >= v[a] ) j = j - 1; 
 else if ( (v[i] > v[a]) && (v[j] < v[a]) ) { 
 aux = v[i]; 
 v[i] = v[j]; 
 v[j] = aux; 
 i = i + 1; 
 j = j - 1; 
 } 
 
Nótese que las dos primeras condiciones no son excluyentes entre sí pero sí 
con la tercera, y, por lo tanto, la distinción de casos se puede optimizar te-
niendo en cuenta esta circunstancia. 
Diseño de algoritmos recursivos 29 
 
Con todo esto el algoritmo queda: 
void particion ( TElem v[], int a, int b, int & p ) { 
// Pre: 0 <= a <= b <= num-1 
 
 int i, j; 
 TElem aux; 
 
 i = a+1; 
 j = b; 
 while ( i <= j ) { 
 if ( (v[i] > v[a]) && (v[j] < v[a]) ) { 
 aux = v[i]; v[i] = v[j]; v[j] = aux; 
 i = i + 1; j = j - 1; 
 } 
 else { 
 if ( v[i] <= v[a] ) i = i + 1; 
 if ( v[j] >= v[a] ) j = j – 1; 
 } 
 } 
 p = j; 
 aux = v[a]; v[a] = v[p]; v[p] = aux; 
 
// Post: 0 <= a <= p <= b <= num-1 y 
// todos los elementos desde ‘a’ hasta ‘p-1’ son ≤ 
v[p] y 
// todos los elementos desde ‘p+1’ hasta ‘b’ son ≥ 
v[p] 
} 
void quickSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 int p; 
 if ( a <= b ) { 
 particion(v, a, b, p); 
 quickSort(v, a, p–1); 
 quickSort(v, p+1, b); 
 } 
// Post: v está ordenado entre a y b 
} 
 
void quickSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 quickSort(v, 0, num-1); 
// Post: se han ordenado las num primeras posiciones de v 
} 
Ordenación por mezcla 
 
 Partimos de una especificación similar a la del quickSort 
Diseño de algoritmos recursivos 30 
 
void mergeSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 
 mergeSort(v, 0, num-1); 
 
// Post: se han ordenado las num primeras posiciones de v 
} 
 
void mergeSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
// Post: v está ordenado entre a y b 
} 
 
 Planteamiento recursivo. 
Para ordenar el subvector v[a..b] 
— Obtenemos el punto medio m entre a y b, y ordenamos recursivamente los 
subvectores v[a..m] y v[(m+1)..b]. 
— Mezclamos ordenadamente los subvectores v[a..m] y v[(m+1)..b] ya ordena-
dos. 
 
3,5 6,2 2,8 5,0 1,1 4,5
2,8 3,5 6,2 1,1 4,5 5,0
a b
a b
m m+1
m m+1
Ordenación
recursiva de las
dos mitades
1,1 2,8 3,5 4,5 5,0 6,2
a b
Mezcla
 
 Análisis de casos 
— Caso directo: a ≥ b 
El subvector está vacío o tiene longitud 1 y, por lo tanto, está ordenado. 
P0 ∧ a ≥ b ⇒ a = b ∨ a = b+1 
Q0 ∧ (a = b ∨ a = b+1) ⇒ v = V 
Diseño de algoritmos recursivos 31 
 
 
— Caso recursivo: a < b 
Tenemos un subvector de longitud mayor o igual que 2, y aplicamos el 
planteamiento recursivo: 
— Dividir v[a..b] en dos mitades. Al ser la longitud ≥ 2 es posible hacer la 
división de forma que cada una de las mitades tendrá una longitud es-
trictamente menor que el segmento original (por eso hemos considera-
do como caso directo el subvector de longitud 1). 
— Tomando m = (a+b)/2 ordenamos recursivamente v[a..m] y v[(m+1)..b]. 
— Usamos un procedimiento auxiliar para mezclar las dos mitades, que-
dando ordenado todo v[a..b]. 
 
 Función de acotación. 
t(v, a, b) = b – a + 1 
 
 Suponiendo que tenemos una implementación correcta para mezcla, el proce-
dimiento de ordenación queda: 
void mergeSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
 int m; 
 
 if ( a < b ) { 
 m = (a+b) / 2; 
 mergeSort( v, a, m ); 
 mergeSort( v, m+1, b ); 
 mezcla( v, a, m, b ); 
 } 
 
// Post: v está ordenado entre a y b 
} 
 
 
 
 Diseño de mezcla. 
El problema es que para conseguir una solución eficiente, O(n), necesitamos 
utilizar un vector auxiliar donde iremos realizando la mezcla, para luego copiar 
el resultado al vector original. 
El problema es que el tamaño del array auxiliar dependerá del valor de a y b y 
en C++ –y en general en cualquier lenguaje de programación– no es posible 
ubicar un array utilizando variables para indicar el tamaño. Es decir, no es co-
rrecta la declaración: 
Diseño de algoritmos recursivos 32 
 
 
void mezcla( TElem v[], int a, int m, int b ) { 
 TElem u[b-a+1]; 
... 
} 
En C++ podemos resolver este problema aprovechándonos de la “dualidad 
puntero-array”: el identificador de un array es un puntero al primer elemento 
del array1, lo cual nos permite crear el array auxiliar con la declaración 
 TElem *u = new TElem[b-a+1]; 
De esta forma, u se puede tratar como un array de b–a+1 elementos, aunque al 
final del procedimiento, deberemos liberar el espacio que ocupa 
 delete[] u; 
La idea del algoritmo es colocarse al principio de cada subvector e ir tomando, 
de uno u otro, el menor elemento, y así ir avanzando. Uno de los subvectores 
se acabará primero y habrá entonces que copiar el resto del otro subvector. En 
el array auxiliar tendremos los índices desplazados pues mientras el subvector a 
mezclar es v[a..b], en el array auxiliar tendremos los elementos almacenados en 
v[0..b–a], y habrá que ser cuidadoso con los índices que recorren ambos arrays. 
 
 
Con todo esto, el procedimiento de mezcla queda: 
 
void mezcla( TElem v[], int a, int m, int b ) { 
// Pre: a <= m < b y 
// v está ordenado entre a y m y v está ordenado entre 
m+1 y b 
 
 TElem *u = new TElem[b-a+1]; 
 int i, j, k; 
 
 for ( k = a; k <= b; k++ ) 
 u[k-a] = v[k]; 
 
1 Más sobre esto en el tema 3 
Diseño de algoritmos recursivos 33 
 
 i = 0; 
 j = m-a+1; 
 k = a; 
 while ( (i <= m-a) && (j <= b-a) ) { 
 if ( u[i] <= u[j] ){ 
 v[k] = u[i]; 
 i = i + 1; 
 } else { 
 v[k] = u[j]; 
 j = j + 1; 
 } 
 k = k + 1; 
 } 
 while ( i <= m-a ) { 
 v[k] = u[i]; 
 i = i+1; 
 k = k+1; 
 } 
 while ( j <= b-a ) { 
 v[k] = u[j]; 
 j = j+1; 
 k = k+1; 
 } 
 delete[] u; 
// Post: v está ordenado entre a y b 
} 
Diseño de
algoritmos recursivos 34 
 
Cota inferior de la complejidad para los algoritmos de ordenación basados en 
intercambios 
 
 Dado un algoritmo A de ordenación basado en intercambios, se tienen los si-
guientes resultados sobre la cota inferior de su complejidad en el caso peor, 
TA(n), tomando como tamaño de los datos la longitud del vector: 
(a) TA(n) ∈ Ω(n ⋅ log n) 
(b) TA(n) ∈ Ω(n2), en el caso de que A sólo efectúe intercambios de compo-
nentes vecinas. 
 
 Para demostrar (a) razonamos sobre el número de comparaciones que es nece-
sario realizar en el caso peor. Según el resultado de cada comparación, realiza-
remos o no un intercambio. 
Con una comparación podemos generar 2 permutaciones distintas, según si 
realizamos o no el intercambio; con dos comparaciones podemos generar 22 
permutaciones, y así sucesivamente, de forma que con t comparaciones pode-
mos obtener 2t permutaciones distintas. 
En el caso peor, deberemos considerar un número de comparaciones que nos 
permita alcanzar cualquiera de las n! permutaciones posibles: 
2t ≥ n! ⇒ t ≥ log( n! ) ⇒ t ≥ c ⋅ n ⋅ log n 
 
realizando t comparaciones, la complejidad del algoritmo debe ser 
TA(n) ≥ t ≥ c ⋅ n ⋅ log n ⇒ TA(n) ∈ Ω(n ⋅ log n) 
 
 Para demostrar (b) definimos una medida del grado de desorden de un vector, 
como el número de inversiones que contiene 
inv(v, i, j) ⇔def i < j ∧ v[ i ] > v[ j ] 
un vector estará ordenado si contiene 0 inversiones. 
El caso peor se tendrá cuando el vector esté ordenado en orden inverso, don-
de el número de inversiones será 
 (Σ i : 1 ≤ i < n : n−i) ≥ c ⋅ n2 
Si usamos un algoritmo que sólo realiza intercambios entre componentes veci-
nas, a lo sumo podrá deshacer una inversión con cada intercambio, y, por lo 
tanto, en el caso peor realizará un número de intercambios del orden de n2 
TA(n) ∈ Ω(n2) 
Diseño de algoritmos recursivos 35 
 
2.3 Análisis de algoritmos recursivos 
2.3.1 Ecuaciones de recurrencias 
 La recursión no introduce nuevas instrucciones en el lenguaje, sin embargo, 
cuando intentamos analizar la complejidad de una función o un procedimiento 
recursivo nos encontramos con que debemos conocer la complejidad de las 
llamadas recursivas ... 
La definición natural de la función de complejidad de un algoritmo recursivo 
también es recursiva, y viene dada por una o más ecuaciones de recurrencia. 
 
 Cálculo del factorial. 
Tamaño de los datos: n 
Caso directo, n = 1 : T(n) = 2 
Caso recursivo: 
— 1 de evaluar la condición + 
— 1 de evaluar la descomposición n–1 + 
— 1 de la asignación de n * fact(n–1) + 
— T(n–1) de la llamada recursiva. 
 2 si n = 1 
Ecuaciones de recurrencia: T(n) = 
 3 + T(n–1) si n > 1 
 
 Multiplicación por el método del campesino egipcio. 
Tamaño de los datos: n = b 
Caso directo, n = 0, 1: T(n) = 3 
En ambos casos recursivos: 
— 4 de evaluar todas las condiciones en el caso peor + 
— 1 de la asignación + 
— 2 de evaluar la descomposición 2*a y b div 2 + 
— T(n/2) de la llamada recursiva. 
 3 si n = 0, 1 
Ecuaciones de recurrencia: T(n) = 
 7 + T(n/2) si n > 1 
Diseño de algoritmos recursivos 36 
 
 Para calcular el orden de complejidad no nos interesa el valor exacto de las 
constantes, ni nos preocupa que sean distintas (en los casos directos, o cuando 
se suma algo constante en los casos recursivos): ¡estudio asintótico! 
 
 Números de fibonacci. 
Tamaño de los datos: n 
 c0 si n = 0, 1 
Ecuaciones de recurrencia: T(n) = 
 T(n−1) + T(n−2) + c si n > 1 
 
 Ordenación rápida (quicksort) 
Tamaño de los datos: n = num 
En el caso directo tenemos complejidad constante c0. 
En el caso recursivo: 
— El coste de la partición: c ⋅ n + 
— El coste de las dos llamadas recursivas. El problema es que la disminu-
ción en el tamaño de los datos depende de los datos y de la elección del 
pivote. 
— El caso peor se da cuando el pivote no separa nada (es el máximo o el 
mínimo del subvector): c0 + T(n−1) 
— El caso mejor se da cuando el pivote divide por la mitad: 2 ⋅ T(n/2) 
 
Ecuaciones de recurrencia c0 si n = 0 
en el caso peor: T(n) = 
 T(n−1) + c ⋅ n + c0 si n ≥ 1 
 
 
Ecuaciones de recurrencia c0 si n = 0 
en el caso mejor: T(n) = 
 2 ⋅ T(n/2) + c ⋅ n si n ≥ 1 
 
Se puede demostrar que en promedio se comporta como en el caso mejor. 
Cambiando la política de elección del pivote se puede evitar que el caso peor 
sea un vector ordenado. 
Diseño de algoritmos recursivos 37 
 
2.3.2 Despliegue de recurrencias 
 
 Hasta ahora, lo único que hemos logrado es expresar la función de compleji-
dad mediante ecuaciones recursivas. Pero es necesario encontrar una fórmula 
explícita que nos permita obtener el orden de complejidad buscado. 
 El objetivo de este método es conseguir una fórmula explícita de la función de 
complejidad, a partir de las ecuaciones de recurrencias. El proceso se compone 
de tres pasos: 
1. Despliegue. Sustituimos las apariciones de T en la recurrencia tantas veces 
como sea necesario hasta encontrar una fórmula que dependa del número 
de llamadas recursivas k. 
2. Postulado. A partir de la fórmula paramétrica resultado del paso anterior 
obtenemos una fórmula explícita. Para ello, se obtiene el valor de k que nos 
permite alcanzar un caso directo y, en la fórmula paramétrica, se sustituye k 
por ese valor y la referencia recursiva T por la complejidad del caso directo. 
3. Demostración. La fórmula explícita así obtenida sólo es correcta si la recu-
rrencia para el caso recursivo también es válida para el caso directo. Pode-
mos comprobarlo demostrando por inducción que la fórmula obtenida 
cumple las ecuaciones de recurrencia. 
Ejemplos 
 Factorial. 
 2 si n = 0 
T(n) = 
 3 + T(n–1) si n > 0 
— Despliegue 
T(n) = 3 + T(n–1) = 3 + 3 + T(n–2) = 3 + 3 + 3 + T(n–3) 
 ... 
 = 3 ⋅ k + T(n–k) 
— Postulado 
El caso directo se tiene para n = 0 
n − k = 0 ⇔ k = n 
T(n) = 3 n + T(n–n) = 3 n + T(0) = 3 n + 2 = 3 ⋅ n – 1 
por lo tanto T(n) ∈ O(n) 
Diseño de algoritmos recursivos 38 
 
 Multiplicación por el método del campesino egipcio. 
 3 si n = 0, 1 
T(n) = 
 7 + T(n/2) si n > 1 
 
— Despliegue 
T(n) = 7 + T(n/2) = 7 + (7 + T(n/2/2)) = 7 + 7 + 7 + 
T(n/2/2/2) 
 ... 
 = 7 ⋅ k + T(n/2k) 
 
— Postulado 
Las llamadas recursivas terminan cuando se alcanza 1 
n/2k = 1 ⇔ k = log n 
T(n) = 7 ⋅ log n + T(n/2log n) = 7 ⋅ log n + T(1) 
 = 7 ⋅ log n + 3 
por lo tanto T(n) ∈ O(log n) 
 
Si k representa el número de llamadas recursivas ¿qué ocurre cuando k = log n 
no tiene solución entera? 
La complejidad T(n) del algoritmo es una función monótona no decreciente, y, 
por lo tanto, nos basta con estudiar su comportamiento sólo en algunos pun-
tos: los valores de n que son una potencia de 2. Esta simplificación no causa 
problemas en el cálculo asintótico. 
 
 Números de Fibonacci. 
 
 c0 si n = 0, 1 
T(n) = 
 T(n−1) + T(n−2) + c1 si n > 1 
 
Podemos simplificar la resolución de la recurrencia, considerando que lo que 
nos interesa es una cota superior: 
T(n) ≤ 2 ⋅ T(n–1) + c1 si n > 1 
 
 
Diseño de algoritmos recursivos 39 
 
— Despliegue: 
T(n) ≤ c1 + 2 * T(n–1) 
 ≤ c1 + 2 * (c1 + 2 * T(n–2)) 
 ≤ c1 + 2 * (c1 + 2 * (c1 + 2 * T(n–3))) 
 ≤ c1 + 2 * c1 + 22 * c1 + 23 * T(n–3) 
 ... 
 ≤ c1 ⋅ ∑
−
=
1
0
k
i
2i + 2k T(n–k) 
 
— Postulado 
Las llamadas recursivas terminan cuando se alcanzan 0 y 1. Debido a la 
simplificación anterior, consideramos 1. 
n−k = 1 ⇔ k = n-1 
T(n) ≤ c1 ⋅ ∑
−
=
1
0
n
i
2i + 2k T(n–k) 
 
 = c1 ⋅ ∑
−
=
1
0
n
i
2i + 2n T(n–n+1) 
 
 = c1 ⋅ ∑
−
=
1
0
n
i
2i + 2n T(1) 
 
 (*) = c1 ⋅ (2n – 1) + c0 ⋅ 2n 
 = (c0 + c1) ⋅ 2n – c1 
 
donde en (*) hemos utilizado la fórmula para la suma de progresiones 
geométricas: 
∑
−
=
1
0
n
i
ri = 
1
1
−
−
r
r n r ≠ 1 
 
Por lo tanto T(n) ∈ O(2n) 
Las funciones recursivas múltiples
donde el tamaño del problema se disminuye 
por sustracción tienen costes prohibitivos, como en este caso donde el coste es 
exponencial. 
Diseño de algoritmos recursivos 40 
 
2.3.3 Resolución general de recurrencias 
 Utilizando la técnica de despliegue de recurrencias y algunos resultados sobre 
convergencia de series, se pueden obtener unos resultados teóricos para la ob-
tención de fórmulas explícitas, aplicables a un gran número de ecuaciones de 
recurrencias. 
Disminución del tamaño del problema por sustracción 
 
 Cuando: (1) la descomposición recursiva se obtiene restando una cierta canti-
dad constante, (2) el caso directo tiene coste constante, y (3) la preparación de 
las llamadas y de combinación de los resultados tiene coste polinómico, enton-
ces la ecuación de recurrencias será de la forma: 
 c0 si 0 ≤ n < n0 
T(n) = 
 a ⋅ T(n–b) + c ⋅ nk si n ≥ n0 
donde: 
— c0 es el coste en el caso directo, 
— a ≥ 1 es el número de llamadas recursivas, 
— b ≥ 1 es la disminución del tamaño de los datos, y 
— c ⋅ nk es el coste de preparación de las llamadas y de combinación de los re-
sultados. 
Se puede demostrar: 
 O(nk+1) si a = 1 
T(n) ∈ 
 O(an div b) si a > 1 
 
Vemos que, cuando el tamaño del problema disminuye por sustracción, 
— En recursión simple (a=1) el coste es polinómico y viene dado por el pro-
ducto del coste de cada llamada (c⋅nk) y el coste lineal de la recursión (n). 
— En recursión múltiple (a>1), por muy grande que sea b, el coste siempre es 
exponencial. 
Diseño de algoritmos recursivos 41 
 
Disminución del tamaño del problema por división 
 
 Cuando: (1) la descomposición recursiva se obtiene dividiendo por una cierta 
cantidad constante, (2) el caso directo tiene coste constante, y (3) la prepara-
ción de las llamadas y de combinación de los resultados tiene coste polinómi-
co, entonces la ecuación de recurrencias será de la forma: 
 c1 si 0 ≤ n < n0 
T(n) = 
 a ⋅ T(n/b) + c ⋅ nk si n ≥ n0 
donde: 
— c1 es el coste en el caso directo, 
— a ≥ 1 es el número de llamadas recursivas, 
— b ≥ 2 es el factor de disminución del tamaño de los datos, y 
— c ⋅ nk es el coste de preparación de las llamadas y de combinación de los re-
sultados. 
Se puede demostrar: 
 O(nk) si a < bk 
T(n) ∈ O(nk ⋅ log n) si a = bk 
 O( abn log ) si a > bk 
 
 
Si a ≤ bk la complejidad depende de nk que es el término que proviene de c ⋅ nk 
en la ecuación de recurrencias, y, por lo tanto, la complejidad de un algoritmo 
de este tipo se puede mejorar disminuyendo la complejidad de la preparación 
de las llamadas y la combinación de los resultados. 
Si a > bk las mejoras en la eficiencia se pueden conseguir 
— disminuyendo el número de llamadas recursivas a o aumentando el factor de 
disminución del tamaño de los datos b, o bien 
— optimizando la preparación de las llamadas y combinación de los resultados, 
pues, si esto hace disminuir k suficientemente, podemos pasar a uno de los 
otros casos: a = bk o incluso a < bk. 
Diseño de algoritmos recursivos 42 
 
Ejemplos 
 
 Suma recursiva de un vector de enteros. 
Tamaño de los datos: n = num 
Recurrencias: 
 
 c1 si n = 0 
T(n) = 
 T(n–1) + c si n > 0 
 
Se ajusta al esquema teórico de disminución del tamaño del problema por sus-
tracción, con los parámetros: 
a = 1, b = 1, k = 0 
 
Estamos en el caso a = 1, por lo que la complejidad resulta ser: 
O(nk+1) = O(n) 
 
 Búsqueda binaria. 
Tamaño de los datos: n = num 
Recurrencias: 
 
 c1 si n = 0 
T(n) = 
 T(n/2) + c si n > 0 
 
Se ajusta al esquema teórico de disminución del tamaño del problema por divi-
sión, con los parámetros: 
a = 1, b = 2, k = 0 
Estamos en el caso a = bk y la complejidad resulta ser: 
O(nk ⋅ log n) = O(n0 ⋅ log n) = O(log n) 
 
 
 Ordenación por mezcla (mergesort). 
Diseño de algoritmos recursivos 43 
 
Tamaño de los datos: n = num 
 c1 si n ≤ 1 
T(n) = 
 2 ⋅ T(n/2) + c ⋅ n si n ≥ 2 
donde c ⋅ n es el coste del procedimiento mezcla. 
Se ajusta al esquema teórico de disminución del tamaño del problema por divi-
sión, con los parámetros: 
a = 2, b = 2, k = 1 
Estamos en el caso a = bk y la complejidad resulta ser: 
O(nk ⋅ log n) = O(n ⋅ log n) 
 
Diseño de algoritmos recursivos 44 
 
2.4 Transformación de recursivo a iterativo 
 En general un algoritmo iterativo es más eficiente que uno recursivo porque la 
invocación a procedimientos o funciones tiene un cierto coste. 
 
 El inconveniente de transformar los algoritmos recursivos en iterativos radica 
en que puede ocurrir que el algoritmo iterativo sea menos claro, con lo cual se 
mejora la eficiencia a costa de perjudicar a la facilidad de mantenimiento. 
 
Correcto programa
ideal
Legible y
bien documentado
Eficiente
Fácil de mantener
y reutilizar
Compromiso
 
 
 En los casos más sencillos (recursión final), ciertos compiladores nos liberan 
de este compromiso porque son capaces de transformar automáticamente las 
versiones recursivas que nosotros programamos en versiones iterativas equiva-
lentes (que son las que en realidad se ejecutan). 
Diseño de algoritmos recursivos 45 
 
2.4.1 Transformación de la recursión final 
 
 El esquema general de un procedimiento recursivo final es: 
void nombreProc ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
// declaración de constantes 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 
 if ( d(xr ) ) 
 yr = g(xr ); 
 else if ( ¬d(xr ) ) { 
 xr ‘ = s(xr ); 
 nombreProc(xr ‘, yr ); 
 } 
// Postcondición 
} 
 
 
 Podemos imaginar la ejecución de una llamada recursiva nombreProc(xr , yr ) co-
mo un “bucle descendente” 
xr → nombreProc(xr , yr ) 
 ↓ 
 nombreProc(s(xr ), yr ) 
 ↓ 
 nombreProc(s2(xr ), yr ) 
 ↓ 
 ... 
 ↓ 
 nombreFunc(sn(xr ), yr ) → yr = g(sn(xr )) 
 
En realidad, hay otro “bucle ascendente” que va devolviendo el valor de yr ; 
sin embargo, cuando la recursión es final no se modifica yr y podemos 
ignorarlo. 
 Existe una traducción directa a la versión iterativa del esquema anterior: 
 
Diseño de algoritmos recursivos 46 
 
void nombreProcItr ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) 
{ 
// Precondición 
// declaración de constantes 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 
 xr ' = xr ; 
 while ( ¬d(xr ’) ) 
 xr ‘ = s(xr ’); 
 yr = g(xr ’); 
 } 
// Postcondición 
} 
 
— Este paso se puede realizar de forma mecánica. 
 
— Se puede demostrar que la corrección del algoritmo recursivo garantiza la 
del algoritmo iterativo obtenido de esta manera. 
 
— Si en la versión recursiva se han tenido que añadir parámetros adicionales 
para permitir la obtención de un planteamiento recursivo, en la versión itera-
tiva se pueden sustituir dichos parámetros por variables locales, inicializadas 
correctamente. 
 
Diseño de algoritmos recursivos 47 
 
Ejemplos 
 Versión recursiva final del factorial. 
int acuFact( int a, int n ) { 
// Pre: a >= 0 && n >= 0 
 int r; 
 if ( n == 0 ) r = a; 
 else if ( n > 0 ) r = acuFact( a*n, n-1 ); 
 return r; 
// Post: devuelve a * n! 
} 
 
int fact( int n ) { 
// Pre: n >= 0 
 
 return acuFact( 1, n ); 
 
// Post: devuelve n! 
} 
 
Para aplicar mecánicamente el esquema, tenemos que ajustarla al esquema ge-
neral de recursión final, lo que nos obliga a declarar las variables locales a’ y n’. 
La variable extra a se convierte en una variable local inicializada a 1. 
int fact( int n ) { 
// Pre: n >= 0 
 
 int a, r, a’, n’; 
 
 a = 1; 
 a’ = a; 
 n’ = n; 
 while ( n’ > 0 ) { 
 a’ = a' * n'; 
 n’ = n’ - 1; 
 } 
 r = a’; 
 return r; 
// Post: devuelve n! 
} 
Si eliminamos las variables innecesarias y sustituimos el bucle while por un bu-
cle for , obtendremos la versión iterativa habitual de la función factorial. 
Diseño de algoritmos recursivos 48 
 
 Búsqueda binaria. 
 
int buscaBin( TElem
v[], TElem x, int a, int b ) { 
 
 int p, m; 
 
 if ( a == b+1 ) 
 p = a - 1; 
 else if ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 p = buscaBin( v, x, m+1, b ); 
 else 
 p = buscaBin( v, x, a, m-1 ); 
 } 
 return p; 
} 
 
int buscaBin( TElem v[], int num, TElem x ) { 
 
 return buscaBin(v, x, 0, num-1); 
} 
 
Aplicando el esquema, incluyendo las variables extra a y b como variables loca-
les y haciendo algunas simplificaciones obvias obtenemos: 
 
int buscaBin( TElem v[], int num, TElem x ) { 
 
 int a, b, p, m; 
 
 a = 0; 
 b = num-1; 
 while ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 a = m+1; 
 else 
 b = m-1; 
 } 
 p = a - 1; 
 return p; 
} 
 
2.4.2 Transformación de la recursión lineal 
 
 En su formulación más general necesita del uso de una pila, un tipo abstracto 
de datos que estudiaremos en un tema posterior. En ese punto estudiaremos 
este tipo de transformación como una aplicación de las pilas. 
 
Diseño de algoritmos recursivos 49 
 
2.4.3 Trasformación de la recursión múltiple 
 
 En su formulación más general necesita usar árboles generales. Por ello, no 
trataremos este tipo de transformaciones. Sin embargo, sí nos ocuparemos de 
un caso especialmente interesante, la implementación iterativa de la ordena-
ción por mezcla, ya que la versión iterativa puede hacerla aún más eficiente. 
 
 Versión recursiva. 
void mergeSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
 int m; 
 
 if ( a < b ) { 
 m = (a+b) / 2; 
 mergeSort( v, a, m ); 
 mergeSort( v, m+1, b ); 
 mezcla( v, a, m, b ); 
 } 
 
// Post: v está ordenado entre a y b 
} 
 
void mergeSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 
 mergeSort(v, 0, num-1); 
 
// Post: se han ordenado las num primeras posiciones de v 
} 
 
¿Cómo funcionaría una versión iterativa? 
 
 Veamos gráficamente cómo funciona la versión recursiva. 
 
Avance de las llamadas recursivas. 
 
Diseño de algoritmos recursivos 50 
 
26
26
26
26
26
5
5
5
5
5
77
77
77
77
1
1
1
1
61
61
61
61
11
11
11
11
11
59
59
59
59
59
15
15
15
15
48
48
48
48
19
19
19
19
 
 
Retroceso de las llamadas recursivas y combinación de los resultados. 
 
26
5
5
1
1
5
26
26
5
5
77
77
26
11
1
1
61
15
61
61
77
19
11
11
11
11
26
59
59
15
15
48
15
59
19
59
48
19
48
61
19
48
59
77 
 
 
¿Cómo funcionaría una versión iterativa? 
Diseño de algoritmos recursivos 51 
 
 La idea de la transformación consiste en obviar la fase de avance de la recur-
sión, y comenzar directamente por los casos base, realizando la combinación 
de los subvectores mediante la operación de mezcla. 
5
1
1
26
5
5
1
26
11
77
77
15
11
11
26
61
15
59
59
59
61
15
61
77
19
19
19
48
48
48
26 5 77 1 61 11 59 15 48 19
1 5 11 15 19 26 48 59 61 77 
 
 
 Versión iterativa. 
void mergeSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 
 int a, b, l; 
 
 l = 1; 
 while ( l < num ) { // no se ha ordenado el subvector entero 
 a = 1; 
 while ( a+l-1 < num-1 ) { // queda más de un subvector por mez
 b = a + 2*l - 1; 
 if ( b > num-1 ) 
 b = num-1; 
 mezcla( v, a, a+l-1, b ); 
 a = a + 2*l; 
 } 
 l = 2 * l; 
 } 
 
// Post: se han ordenado las num primeras posiciones de v 
} 
 
 
 
Diseño de algoritmos recursivos 52 
 
2.5 Técnicas de generalización 
 
 En este tema, ya hemos utilizado varias veces este tipo de técnicas (también 
conocidas como técnicas de inmersión), con el objetivo de conseguir planteamien-
tos recursivos. 
La ordenación rápida 
void quickSort( TElem v[], int a, int b ) { 
// Pre: 0 <= a <= num && -1 <= b <= num-1 && a <= b+1 
 
// Post: v está ordenado entre a y b 
} 
 
void quickSort ( TElem v[], int num ) { 
// Pre: v tiene al menos num elementos y 
// num >= 0 
 
// Post: se han ordenado las num primeras posiciones de v 
} 
 
 Además de para conseguir realizar un planteamiento recursivo, las generaliza-
ciones también se utilizan para 
— transformar algoritmos recursivos ya implementados en algoritmos recursi-
vos finales, que se pueden transformar fácilmente en algoritmos iterativos. 
— mejorar la eficiencia de los algoritmos recursivos añadiendo parámetros y/o 
resultados acumuladores. 
La versión recursiva final del factorial 
int acuFact( int a, int n ) { 
// Pre: a >= 0 && n >= 0 
 
// Post: devuelve a * n! 
} 
 
int fact( int n ) { 
// Pre: n >= 0 
 
// Post: devuelve n! 
} 
 
 
 
 Decimos que una acción parametrizada (procedimiento o función) F es una 
generalización de otra acción f cuando: 
— F tiene más parámetros de entrada y/o devuelve más resultados que f. 
Diseño de algoritmos recursivos 53 
 
— Particularizando los parámetros de entrada adicionales de F a valores ade-
cuados y/o ignorando los resultados adicionales de F se obtiene el compor-
tamiento de f. 
 
En el ejemplo de la ordenación rápida: 
— f : void quickSort ( TElem v[ ], int num ) 
— F : void quickSort( TElem v[ ], int a, int b ) 
— En F se sustituye el parámetro num por los parámetros a y b. Mientras f 
siempre se aplica al intervalo 0 .. num-1, F se puede aplicar a cualquier sub-
intervalo del array determinado por los índices a .. b. 
— Particularizando los parámetros adicionales de F como a = 0, b = num-1, se 
obtiene el comportamiento de f. Razonando sobre las postcondiciones: 
 v está ordenado entre a y b ∧ a = 0 ∧ b = num-1 
⇒ se han ordenado las num primeras posiciones de v 
 
En el ejemplo del factorial: 
— f : int fact( int n ) 
— F : int acuFact( int a, int n ) 
— En F se ha añadido el nuevo parámetro a donde se va acumulando el resul-
tado a medida que se construye. 
— Particularizando el parámetro adicional de F como a = 1, se obtiene el com-
portamiento de f. Razonando sobre las postcondiciones: 
 devuelve a * n! ∧ a = 1 
⇒ devuelve n! 
 
 
Diseño de algoritmos recursivos 54 
 
Planteamientos recursivos finales 
 Dada una especificación Ef pretendemos encontrar una especificación EF más 
general que admita una solución recursiva final. 
El resultado se ha de obtener en un caso directo y, para conseguirlo, lo que 
podemos hacer es añadir nuevos parámetros que vayan acumulando el resulta-
do obtenido hasta el momento, de forma que al llegar al caso directo de F, el 
valor de los parámetros acumuladores sea el resultado de f. 
 
 Para obtener la especificación EF a partir de Ef 
— Fortalecemos la precondición de Ef para exigir que alguno de los parámetros 
de entrada ya traiga calculado una parte del resultado. 
— Mantenemos la misma postcondición. 
 
 Ejemplo: el producto escalar de dos vectores 
int prodEsc( int u[], int v[], int num ) { 
// Pre: ‘u’ y ‘v’ contienen al menos ‘num’ elementos 
 
 int r; 
 
 if ( num == 0 ) r = 0; 
 else if ( num > 0 ) r = v[num-1]*u[num-1] + prodEsc( u, 
v, num-1 ); 
 return r; 
 
// Post: devuelve el sumatorio para i desde 0 hasta ‘num-1’ 
// de u[i] * v[i], es decir, el producto escalar de u 
y v 
} 
Para obtener la precondición de la generalización recursiva final añadimos un 
nuevo parámetro que acumula la parte del producto escalar calculado hasta el 
momento por el algoritmo recursivo. 
 
int prodEscGen( int u[], int v[], int num, int a ) { 
// Pre: ‘u’ y ‘v’ contienen al menos ‘num’ elementos y 
// a es igual al sumatorio para i desde el valor 
inicial 
// de ‘num-1’ hasta ‘num’ de v[i]*u[i] 
 
 
// Post: devuelve el sumatorio para i desde 0 hasta ‘num-1’ 
// de u[i] * v[i], es decir, el producto escalar de u 
y v 
} 
 
Diseño de algoritmos recursivos 55
Para esta especificación resulta sencillo encontrar un planteamiento recursivo 
final: 
 
int prodEscGen( int u[], int v[], int num, int a ) { 
// Pre: ‘u’ y ‘v’ contienen al menos ‘num’ elementos y 
// a es igual al sumatorio para i desde el valor 
inicial 
// de ‘num-1’ hasta ‘num’ de v[i]*u[i] 
 
 int r; 
 
 if ( num == 0 ) 
 r = a; 
 else if ( num > 0 ) 
 r = prodEscGen( u, v, num-1, a + v[num-1]*u[num-1] ); 
 return r; 
 
// Post: devuelve Σ i : 1 ≤ i ≤ N : u[i] * v[i] 
} 
 
int prodEsc( int u[], int v[], int num ) { 
// Pre: ‘u’ y ‘v’ contienen al menos ‘num’ elementos 
 
 return prodEscGen(u, v, num, 0); 
 
// Post: devuelve el sumatorio para i desde 0 hasta ‘num-1’ 
// de u[i] * v[i], es decir, el producto escalar de u 
y v 
} 
 
Donde hemos fijado a 0 el valor inicial del parámetro acumulador. 
 
 El uso de acumuladores es una técnica que tiene especial relevancia en lengua-
jes en los que sólo se dispone de recursión. Esta es la forma de conseguir fun-
ciones recursivas eficientes. 
En programación imperativa tiene menos sentido pues, en muchos casos, re-
sulta más natural obtener directamente la solución iterativa. 
 
 
Diseño de algoritmos recursivos 56 
 
2.5.1 Generalización por razones de eficiencia 
 Suponemos ahora que partimos de un algoritmo recursivo ya implementado y 
que nos proponemos mejorar su eficiencia introduciendo parámetros y/o re-
sultados adicionales. 
Se trata de simplificar algún cálculo auxiliar, sacando provecho del resultado 
obtenido para ese cálculo en otra llamada recursiva. Introducimos parámetros 
adicionales, o resultados adicionales, según si queremos aprovechar los cálcu-
los realizados en llamadas anteriores, o posteriores, respectivamente. En oca-
siones, puede interesar añadir tanto parámetros como resultados adicionales. 
Generalización con parámetros acumuladores 
 Se aplica esta técnica cuando en un algoritmo recursivo f se detecta una expre-
sión e(xr ), que sólo depende de los parámetros de entrada, cuyo cálculo se 
puede simplificar utilizando el valor de esa expresión en anteriores llamadas re-
cursivas. 
 
 Se construye una generalización F que posee parámetros de entrada adiciona-
les ar , cuya función es transmitir el valor de e(xr ). La precondición de F se 
plantea como un fortalecimiento de la precondición de f 
P'(ar , xr ) ⇔ P(xr ) ∧ ar=e(xr ) 
mientras que la postcondición se mantiene constante 
Q'( ar , xr , yr ) ⇔ Q(xr , yr ) 
 
 El algoritmo F se obtiene a partir de f del siguiente modo: 
— Se reutiliza el texto de f, reemplazando e(xr ) por ar 
— Se diseña una nueva función sucesor s'(ar , xr ), a partir de la original s(xr ), de 
modo que se cumpla: 
{ ar = e(xr ) ∧ ¬d(xr ) } 
(ar ’, xr ’) = s’(ar , xr ) 
{ xr ’ = s(xr ) ∧ ar ’ = e(xr ’) } 
La técnica resultará rentable cuando en el cálculo de ar ’ nos podamos aprove-
char de los valores de ar y xr para realizar un cálculo más eficiente. 
 
Diseño de algoritmos recursivos 57 
 
 Ejemplo: función que calcula cuántas componentes de un vector son iguales a 
la suma de las componentes situadas a su derecha: 
int numCortesDer( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
// Post: devuelve el número de posiciones entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
 
Para obtener un planteamiento recursivo, es necesario generalizar esta especi-
ficación, añadiendo un parámetro que nos indique en qué posición del array 
nos encontramos. 
Hasta ahora no había sido necesario realizar este tipo de generalizaciones en 
algoritmos recursivos sobre vectores porque utilizábamos el parámetro num 
para indicar el avance por el array. En este caso, sin embargo, debemos man-
tener num pues en cada llamada recursiva se hace necesario obtener la suma 
desde la posición actual hasta num–1. 
 
int numCortesDerGen( int v[], int num, int i ) { 
// Pre: v contiene al menos num elementos 
// 0 <= i <= num-1 
 
// Post: devuelve el número de posiciones entre 0 e i 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
 
Diseño de algoritmos recursivos 58 
 
La implementación de esta generalización 
 
int numCortesDerGen( int v[], int num, int i ) { 
// Pre: v contiene al menos num elementos 
// 0 <= i <= num-1 
 
 int r, s, j; 
 
 if ( i < 0 ) 
 r = 0; 
 else if ( i >= 0 ) { 
 s = 0; 
 for ( j = i+1; j < num; j++ ) 
 s = s + v[j] 
 if ( s == v[i] ) 
 r = numCortesDerGen( v, num, i-1 ) + 1; 
 else 
 r = numCortesDerGen( v, num, i-1 ); 
 } 
 return r; 
// Post: devuelve el número de posiciones entre 0 e i 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
 
int numCortesDer( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
 return numCortesDerGen( v, num, num-1); 
 
// Post: devuelve el número de posiciones entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
 
 
Observamos que este algoritmo de complejidad O(n2) (n = num) se podría op-
timizar si en el cálculo de la suma de las componentes s utilizásemos el resulta-
do de la llamada anterior. Es decir, en este caso queremos optimizar el cálculo 
de la expresión s(i, v) = Σ j : i < j < num : v[j]. 
 
Para obtener la nueva generalización: 
Diseño de algoritmos recursivos 59 
 
— Eliminamos el parámetro i porque ahora podemos utilizar num para indicar 
la posición actual dentro del array. 
— Añadimos un nuevo parámetro s donde se recibirá la suma de las compo-
nentes hasta num, y fortalecemos la precondición para incluir las condiciones 
sobre el nuevo parámetro 
P’(v, num, s) : s es igual a la suma desde num hasta el 
valor inicial 
 de num-1 
 
La implementación de esta nueva generalización: 
int numCortesDerGen( int v[], int num, int s ) { 
// Pre: v contiene al menos num elementos 
// s es igual a la suma desde num hasta el valor 
inicial 
// de num-1 
 int r; 
 if ( num == 0 ) 
 r = 0; 
 else if ( num > 0 ) { 
 if ( s == v[num-1] ) 
 r = numCortesDerGen( v, num-1, s+v[num-1] ) + 1; 
 else 
 r = numCortesDerGen( v, num-1, s+v[num-1] ); 
 } 
 return r; 
// Post: devuelve el número de posiciones entre 0 y num 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha, hasta el valor inicial de 
num-1. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
 
int numCortesDer( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 return numCortesDerGen( v, num, 0); 
// Post: devuelve el número de posiciones entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// situadas a su derecha. 
// Se considera que la suma a la derecha de v[num-1] 
es 0 
} 
Una vez que hemos conseguido mejorar la complejidad –ahora es de O(n)–, es 
inmediato obtener otra generalización, añadiendo un parámetro más, que con-
vierta esta función en recursiva final. 
Diseño de algoritmos recursivos 60 
 
Generalización con resultados acumuladores 
 
 Se aplica esta técnica cuando en un algoritmo recursivo f se detecta una expre-
sión e(xr ', yr '), que puede depender de los parámetros de entrada y los resulta-
dos de la llamada recursiva, cuyo cálculo se puede simplificar utilizando el 
valor de esa expresión en posteriores llamadas recursivas. Obviamente, si la ex-
presión depende de los resultados de la llamada recursiva, debe aparecer
des-
pués de dicha llamada. 
Se construye una generalización F que posee resultados adicionales b
r
, cuya 
función es transmitir el valor de e(xr , yr ). La precondición de F se mantiene 
constante 
P’(xr ) ⇔ P(xr ) 
mientras que la postcondición de F se plantea como un fortalecimiento de la 
postcondición de f 
Q’(xr , b
r
, yr ) ⇔ Q(xr , yr ) ∧ b
r
 = e(xr , yr ) 
 
 El algoritmo F se obtiene a partir de f del siguiente modo: 
— Se reutiliza el texto de f, reemplazando e(xr ', yr ') por b
r
' 
— Se añade el cálculo de b
r
, de manera que la parte b
r
=e( yr ,xr ) de la 
postcondición Q'(xr ,b
r
, yr ) quede garantizada, tanto en los casos directos 
como en los recursivos. 
La técnica resultará rentable siempre que F sea más eficiente que f. 
 
 Ejemplo: cuántas componentes de un vector son iguales a la suma de las com-
ponentes que la preceden. 
 
int numCortesIzq( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
// Post: devuelve el número de posiciones i entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden. 
// Se considera que la suma que precede a v[0] es 0 
} 
 
 
 
Cuya implementación: 
 
Diseño de algoritmos recursivos 61 
 
int numCortesIzq( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
 int r, s, j; 
 
 if ( num == 0 ) 
 r = 0; 
 else if ( num > 0 ) { 
 s = 0; 
 for ( j = 0; j < num-1; j++ ) 
 s = s + v[j]; 
 if ( s == v[num-1] ) 
 r = numCortesIzq( v, num-1 ) + 1; 
 else 
 r = numCortesIzq( v, num-1 ); 
 } 
 return r; 
 
// Post: devuelve el número de posiciones i entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden. 
// Se considera que la suma que precede a v[0] es 0 
} 
 
 
Observamos que este algoritmo de complejidad O(n2) (n = num) se podría op-
timizar si en el cálculo de la suma de las componentes s utilizásemos el resulta-
do obtenido en la llamada recursiva. Es decir, en este caso queremos optimizar 
el cálculo de la expresión s(e, v) = Σ j : 0 ≤ i ≤ N : v[i]. Para ello: 
— Añadimos un nuevo resultado s donde se devolverá la suma de las compo-
nentes situadas a izquierda de num. Como se devuelven dos resultados, de-
bemos convertir la función en un procedimiento. 
— Fortalecemos la postcondición para incluir las condiciones sobre el nuevo 
resultado 
 
Q’(v,num, r, s): r es igual el número de posiciones i entre 
0 y num-1 
 que cumplen que su valor es igual a la suma de las 
componentes 
 que le preceden y 
 s es igual a la suma de las componentes de v desde 0 
hasta num-1 
 
Y la implementación de la generalización: 
 
void numCortesIzqGen( int v[], int num, int& r, int& s ) { 
// Pre: v contiene al menos num elementos 
Diseño de algoritmos recursivos 62 
 
 
 if ( num == 0 ) { 
 r = 0; 
 s = 0; 
 } 
 else if ( num > 0 ) { 
 numCortesIzqGen( v, num-1, r, s ); 
 if ( s == v[num-1] ) 
 r = r + 1; 
 s = s + v[num-1]; 
 } 
 
// Post: r es igual el número de posiciones i entre 0 y 
num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden y 
// s es igual a la suma de las componentes de v desde 0 
hasta num-1 
// Se considera que la suma que precede a v[0] es 0 
} 
 
int numCortesIzq( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
 int r, s; 
 
 numCortesIzqGen( v, num, r, s ); 
 
 return r; 
 
// Post: devuelve el número de posiciones i entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden. 
// Se considera que la suma que precede a v[0] es 0 
} 
 
Hemos conseguido pasar de complejidad cuadrática a complejidad lineal. 
 
¿Se te ocurre cómo se podría conseguir una versión recursiva final del algorit-
mo? 
 
 Para conseguir que sea recursivo final es necesario: 
— añadir un parámetro acumulador donde se almacene el número de posicio-
nes que cumplen la condición, y 
Diseño de algoritmos recursivos 63 
 
— recorrer el array de izquierda a derecha en lugar de hacerlo de derecha a iz-
quierda, y, para ello, necesitamos añadir un nuevo parámetro que controle la 
posición del array por la que vamos. 
De esta forma: 
 
int numCortesIzqGen( int v[], int num, int a, int s, int i 
) { 
// Pre: v contiene al menos num elementos 
// 0 <= i <= num y s es igual a la suma desde 0 hasta 
i-1 y 
// a acumula el número de componentes desde 0 hasta i-
1 que 
// cumplen la condición 
 
 int r; 
 
 if ( i == num ) 
 r = a; 
 else if ( i < num ) { 
 if ( s == v[i] ) 
 r = numCortesIzqGen( v, num, a+1, s+v[i], i+1 ); 
 else 
 r = numCortesIzqGen( v, num, a, s+v[i], i+1 ); 
 } 
 return r; 
 
// Post: r es igual el número de posiciones i entre 0 y 
num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden 
} 
 
int numCortesIzq( int v[], int num ) { 
// Pre: v contiene al menos num elementos 
 
 return numCortesIzqGen( v, num, 0, 0, 0 ); 
 
// Post: devuelve el número de posiciones i entre 0 y num-1 
// que cumplen que su valor es igual a la suma de las 
componentes 
// que le preceden. 
// Se considera que la suma que precede a v[0] es 0 
} 
 
EDI todo/Tema3-TAD?s.pdf
 
TEMA 3 
TIPOS ABSTRACTOS DE DATOS 
 
1. Introducción a la programación con TADs 
2. Especificación de TADs 
3. Implementación de TADs 
4. Módulos. Diseño modular 
5. Estructuras de datos dinámicas 
 
 
 
 
 
 
 
 
 
 
 
 
 
Bibliografía: Fundamentals of Data Structures in C++ 
E. Horowitz, S. Sahni, D. Mehta 
Computer Science Press, 1995 
Data Abstraction and Problem Solving with C++, Second 
Edition 
Carrano, Helman y Veroff 
 
Tipos abstractos de datos 1 
 
3.1 Introducción a la programación con tipos abstractos 
de datos 
 La abstracción es un método de resolución de problemas 
Los problemas reales tienden a ser complejos porque involucran demasiados 
detalles, y resulta imposible considerar todas las posibles interacciones a la vez. 
Hay estudios en Psicología que afirman que en la memoria a corto plazo sólo 
es posible almacenar 7 elementos distintos. 
 
 Una abstracción es un modelo simplificado de un problema, donde se con-
templan los aspectos de un determinado nivel, ignorándose los restantes. 
Por ejemplo. Para indicarle a un robot cómo cambiar la rueda de un coche po-
demos utilizar una descripción con distintos niveles de abstracción: 
1. Bajar del coche 
2. Sacar la rueda de repuesto y el gato 
3. Quitar la rueda pinchada 
4. Poner la rueda de repuesto 
5. Guardar la rueda pinchada 
Para quitar la rueda pinchada 
3.1. Se coloca el gato debajo del coche 
3.2. Se levanta el coche haciendo girar la manivela del gato 
3.2. ... 
Este es un ejemplo de resolución de problemas por refinamientos sucesivos o 
diseño descendente. 
 
 El razonar en términos de abstracciones conlleva una serie de ventajas: 
— La resolución de los problemas se simplifica 
— Las soluciones son más claras y resulta más sencillo razonar sobre su co-
rrección 
— Pueden obtenerse soluciones (o fragmentos de ellas) de utilidad más gene-
ral, que puedan adaptarse para resolver otros problemas 
— Permite la división de tareas 
Tipos abstractos de datos 2 
 
3.1.1 La abstracción como metodología de programación 
 La programación es un proceso de resolución de problemas y como tal tam-
bién se beneficia del uso de abstracciones. 
 
 La evolución de la Programación muestra una tendencia a incluir mecanismos 
de abstracción cada vez de más alto nivel 
— Ensamblador 
— Lenguajes de alto nivel 
— Estructuras de control 
— Procedimientos y funciones 
— Tipos de datos 
— Módulos 
— Tipos abstractos de datos 
— Clases en programación orientada a objetos 
— Generadores de aplicaciones (hojas de
cálculo, gestores de bases de datos, 
diseño visual de interfaces, ...) 
 
 En todos estos mecanismos, aunque a diferente nivel, tenemos una situación 
como esta 
Especificación
Abstracción
Implementación
 
Un mecanismo de abstracción es una barrera que, para una cierta entidad, deja a 
un lado la especificación, con la que los clientes de la abstracción pueden ra-
zonar, y a otro la implementación. De esta forma, los clientes sólo han de en-
tender la especificación para utilizar la entidad, a través de su abstracción. 
Tipos abstractos de datos 3 
 
La abstracción funcional 
 
 Hace tiempo que se descubrió y hoy en día es un lugar común en prácticamente 
todos los lenguajes de programación: las funciones y los procedimientos. 
— Se reúnen un conjunto de sentencias que realizan una determinada opera-
ción sobre unos datos (la implementación) 
— Se les asigna un nombre y se parametrizan por medio de argumentos, cons-
truyéndose así una abstracción del conjunto de sentencias: la cabecera del 
procedimiento o la función 
— Para que los clientes puedan usar esta abstracción (invocar al procedimien-
to o la función) se le adjunta una especificación que indica sus condiciones 
de uso –precondición– y la operación que realiza –postcondición–. La es-
pecificación también se puede realizar informalmente. 
De esta forma se puede utilizar ese conjunto de sentencias como si fuese una 
operación definida en el propio lenguaje, abstrayéndonos de los detalles de su 
implementación. 
 
La abstracción de datos 
 
 El nivel de abstracción de los datos ha ido aumentando a lo largo de los años 
— Tipos predefinidos 
Por ejemplo, en un lenguaje de programación de alto nivel, generalmente, 
no debemos preocuparnos por la representación binaria de los caracteres, 
los enteros o los reales. 
— Tipos de datos definidos por el programador 
Por ejemplo, un array de un cierto tipo se representa internamente como 
una sucesión de celdas de memoria adyacentes, donde el acceso directo se 
consigue mediante sencillas operaciones aritméticas. 
— Tipos abstractos de datos (TADs) definidos por el programador 
Una entidad donde se reúnen un tipo de datos y las operaciones que mani-
pulan los valores de ese tipo. 
 
 
Tipos abstractos de datos 4 
 
 Para entender el concepto de tipo abstracto de datos analicemos la diferencia 
entre los tipos predefinidos y los tipos definidos por el programador. 
— Un tipo predefinido es un tipo abstracto de datos porque 
— Está fijado el conjunto de valores permitidos y los compiladores se en-
cargan de garantizar que no se asignan valores erróneos. No es posible 
acceder directamente a la representación interna de los datos1. 
— Está fijado el conjunto de operaciones permitidas sobre dichos valores 
y el compilador se encarga de garantizar que se usan correctamente. 
— Como programadores sólo necesitamos conocer el comportamiento de 
las operaciones (por ejemplo, los enteros se rigen por unas leyes alge-
braicas conocidas por todos) sin que tengamos acceso a su implementa-
ción concreta, ni nos interese lo más mínimo. 
— Un lenguaje no soporta la implementación de tipos abstractos de datos si 
— La estructura de datos donde se representa la información se define por 
una parte y las operaciones que manipulan esos datos por otra. 
— No es posible definir una abstracción que reúna la estructura de datos y 
las operaciones de forma que 
— la representación de los datos y la implementación de las operacio-
nes sea oculta para los clientes y, por lo tanto, 
— el acceso a la información esté protegido y los clientes no puedan 
hacer ninguna suposición sobre la implementación de las operacio-
nes que no aparezca explícitamente en la especificación. 
Por ejemplo, si queremos definir un tipo de datos para representar fechas, po-
demos utilizar una representación como esta: 
typedef int TDia; 
typedef enum { enero, febrero, ... , diciembre } TMes; 
typedef int TAnyo; 
typedef struct { TDia dia; TMes mes; TAnyo anyo; } TFecha; 
Si se tiene libre acceso a la representación del tipo de datos, es posible realizar 
operaciones absurdas con respecto a la semántica pretendida, como: 
fecha.mes = febrero; 
fecha.dia = 30; 
 
 
1 Esto no es cierto en general, pues normalmente se pueden hacer manipulaciones binarias sobre los valores de tipos predefi-
nidos. Sin embargo, debemos considerar que este es un mecanismo “peligroso” con el cual saltarnos la barrera de la abstracción. 
Tipos abstractos de datos 5 
 
3.1.2 Tipos abstractos de datos 
 
 Un tipo abstracto de datos se compone de 
— Una especificación 
— Una implementación 
Especificación de un tipo abstracto de datos 
 
 La especificación de un TAD debe incluir: 
— El dominio de valores del tipo 
— Las operaciones que permiten manipular los valores del tipo –y que pueden 
hacer referencia a otros tipos– 
— El comportamiento de las operaciones (especificado formal o informal-
mente) 
 
 Como ejemplo, escribamos la especificación informal del TAD fecha 
— Dominio: las fechas desde el 1/1/1 
— Las operaciones, con su comportamiento especificado informalmente 
 
TFecha NuevaFecha ( TDia d, TMes m, TAnyo a ); 
// P: d/m/a cumplen las restricciones de una fecha, según el 
calendario 
// Q: devuelve una fecha que representa a la fecha d/m/a 
 
int distancia ( TFecha f1, TFecha f2 ); 
// P: 
// Q: devuelve la distancia, medida en número de días, entre 
f1 y f2 
 
TFecha suma ( TFecha f, int d ); 
// P: 
// Q: devuelve la fecha resultante de sumar d días a la 
fecha f 
 
¡Cuidado! d puede ser un número negativo de forma que g no sea una fecha 
posterior al 1/1/0. Algunas operaciones pueden fallar y es necesario deter-
minar algún mecanismo de tratamiento de errores. 
 
 
 
Tipos abstractos de datos 6 
 
TDia dia( TFecha f ); 
// P: 
// Q: devuelve el día de la fecha f 
 
TMes mes( TFecha f ); 
// P: 
// Q: devuelve el mes de la fecha f 
 
TAnyo anyo( TFecha f ); 
// P: 
// Q: devuelve el año de la fecha f 
 
TDiaSemana diaSemana( TFecha f ); 
// P: 
// Q: devuelve el día de la semana correspondiente a la 
fecha f 
 
TFecha primerDiaMes( TDiaSemana d, TMes m, TAnyo a ); 
// P: 
// Q: devuelve la primera fecha del mes m del año a cuyo 
// día de la semana es d 
 
 
 
Implementación de un tipo abstracto de datos 
 
 
 La implementación de un TAD consiste en: 
— Elegir una estructura de datos para representar los valores del TAD con 
ayuda de otros tipos ya disponibles. 
— Implementar las operaciones del TAD como funciones o procedimientos, 
usando la representación elegida, y de modo que se satisfaga la especifica-
ción. 
 
 En general cualquier TAD admite varias representaciones posibles. 
— Por ejemplo, una secuencia de números de longitud variable se puede im-
plementar como 
— un registro con un vector y un campo longitud, 
— o como un vector con una marca al final. 
Tipos abstractos de datos 7 
 
 Cuando hay varias representaciones posibles, normalmente una representación 
concreta facilita unas determinadas operaciones y dificulta otras, con respecto 
a una representación alternativa. 
— Por ejemplo, para el TAD fecha que especificamos más arriba podemos op-
tar entre 
— Los registros que vimos más arriba 
Con esta representa resulta trivial implementar NuevaFecha, dia, mes y anyo. 
Mientras que sería más difícil implementar el resto de las operaciones. 
— El número de días transcurridos desde el 1 de Enero de 1900 
Así es mucho más sencillo calcular el número de días transcurridos entre 
dos fechas dadas, pero resulta más difícil construir una fecha a partir del 
día/mes/año. 
 Un lenguaje soporta la implementación de tipos abstractos de datos si incluye 
mecanismos que permitan separar la especificación de la implementación, en 
particular 
— Privacidad: la representación interna (estructura de datos e implementación 
de las
operaciones) está oculta y es invisible para los usuarios. 
“Lo que no conoces no puede hacerte daño” 
— Protección: el tipo sólo puede usarse a través de sus operaciones; otros ac-
cesos incontrolados se hacen imposibles. 
Si un lenguaje no incluye estos mecanismos, entonces el respeto de la abstrac-
ción se convierte en una cuestión de disciplina del programador. 
 ¿Por qué es importante que el lenguaje soporte la implementación de TADs? 
Aceptando que los TADS son un mecanismo de abstracción adecuado resulta 
mucho más cómodo y fiable que el lenguaje nos prohíba los malos usos –o al 
menos nos avise de ellos– a que se trate de una cuestión de autodisciplina. Y 
más aún en un entorno colaborativo con decenas de personas participando en 
el desarrollo de un sistema. 
 En resumen, decimos que un lenguaje soporta el uso de tipos abstractos de 
datos si permite elevar los tipos definidos por el programador al rango de los 
tipos predefinidos, en el sentido indicado más arriba. 
El grado de soporte que proporcionan los lenguajes de programación para la 
definición de tipos abstractos de datos varía mucho. Aunque con el éxito de la 
programación orientada a objetos, cada vez es más habitual encontrar este tipo 
de mecanismos. 
Tipos abstractos de datos 8 
 
Los TADs como apoyo a la programación modular 
 
 El desarrollo de programas grandes y complejos se realiza descomponiendo el 
programa en unidades menores llamadas módulos. 
El problema es, dado un cierto problema ¿cómo descompongo su solución en 
módulos? ¿cuál es el diseño modular de la aplicación? 
— La Ingeniería del software nos aconseja realizar diseños modulares en torno 
a las clases de objetos que maneja la aplicación, en lugar de organizarla en base 
a las funcionalidades que se implementan 
clase de objetos ≡ datos + operaciones ≡ implementación de TADs 
 
 La abstracción en general y los TADs en particular facilitan el desarrollo mo-
dular y la división de tareas ya que la elección de una implementación concreta 
puede posponerse o variarse según convenga, sin afectar a los programas ya 
realizados, o en proceso de diseño, que utilicen el TAD en cuestión. 
 
Un ejemplo 
 Dado un vector v de números enteros con índices entre 1 y N y un número k, 
0 ≤ k ≤ N, se trata de determinar los k números mayores que aparecen en el 
vector –nótese que no tienen que ser k números diferentes–. 
 
 Para resolver este problema necesitamos una estructura auxiliar donde vaya-
mos almacenando los k mayores números encontrados hasta ahora. Tenemos 
dos opciones 
— Elegir una estructura de datos concreta –por ejemplo, un vector– e incluir 
su gestión dentro del propio algoritmo. 
Los inconvenientes de esta decisión: 
— La lógica del algoritmo queda oscurecida por los detalles de la represen-
tación. 
— La estructura de datos elegida a priori puede no ser la más eficiente, ya 
que para saber cuál es la elección más eficiente debemos saber cuáles 
son las operaciones necesarias sobre ella, y eso no lo sabremos hasta 
que hayamos diseñado el algoritmo que la utiliza. 
 
Tipos abstractos de datos 9 
 
— Pospongamos la elección de la estructura de datos, suponiendo que dispo-
nemos del TAD necesario para almacenar y acceder a la información nece-
saria, y concentrémonos en diseñar el algoritmo. 
Posteriormente, recolectaremos las operaciones que el algoritmo necesita y 
a partir de ellas especificaremos e implementaremos el TAD. En ese mo-
mento tendremos información adicional sobre qué operaciones debemos 
preocuparnos por optimizar. 
Ventajas de esta aproximación: 
— Descomponemos la tarea 
— El TAD resultante puede utilizarse para otros fines (reutilización) 
— La lógica del algoritmo debe quedar más clara 
 Idea del algoritmo 
— Inicializamos la estructura auxiliar m con los k primeros elementos de v 
— Desde k+1 hasta N 
— Si v[i] es mayor que el mínimo de m entonces sustituimos éste por v[i] 
Siguiendo esta idea, la implementación podría ser 
 
TMCjtoInt mayores( int k, int v[], int num ) { 
// P: ( 1 <= k <= num ) && ( num >= 1 ) 
// v contiene al menos num elementos 
 int n; 
 TMCjtoInt m; 
 
 vacio(m); 
 n = 0; 
 for ( n = 0; n < k; n++ ) 
 // m contiene los elementos de v[0..n] 
 inserta( v[n], m ); 
 
 for ( n = k; n < num; n++ ) 
 // m contiene los k elementos mayores de v[0..n-1] 
 if ( v[n] > min( m ) ) { 
 borraMin( m ); 
 inserta( v[n], m ); 
 } 
 return m; 
// Q: devuelve un multiconjunto con los k elementos 
// mayores de v[0..num-1] 
} 
 
Tipos abstractos de datos 10 
 
 El TAD ha de ser capaz de almacenar una colección de números enteros, al-
gunos de los cuales pueden estar repetidos (multiconjuntos) y proporcionar ope-
raciones para 
— Construir un multiconjunto vacío 
— Añadir un elemento 
— Quitar el mínimo de los elementos 
— Consultar por el mínimo de los elementos 
La especificación informal de las operaciones: 
void vacio( TMCjtoEnt& m ); 
// P: 
// Q: m representa ∅ 
void inserta( int x, TMCjtoEnt& m ); 
// P: m = M 
// Q: m = M ∪ {x} 
void borraMin( TMCjtoEnt& m ); 
// P: m = M ∧ m no es vacío 
// Q: m es M menos una copia del elemento mínimo de M 
int min( TMCjtoEnt m ); 
// P: m no es vacío 
// Q: devuelve el elemento mínimo de m 
 
Un multiconjunto se puede implementar de distintas formas: 
— Un registro formado por un vector de enteros y un entero que indica cuán-
tos elementos hay 
— Un registro con un entero que indica el número de elementos y un vector 
de registros de dos componentes, una que indica el número almacenado y 
otra el número de apariciones de ese valor 
— Para cada una de las dos anteriores 
— Sin orden 
— Ordenado creciente 
— Ordenado decreciente 
Si interesa optimizar las operaciones inserta, min y borraMin, ¿qué representación 
es más interesante? 
 
Tipos abstractos de datos 11 
 
Tipos abstractos de datos y estructuras de datos 
 
 Una estructura de datos permite representar la información gestionada por un 
TAD, es decir, es la implementación que se utiliza para almacenar los valores 
del tipo. 
 
 Sin embargo, en muchas ocasiones, se utiliza un TAD Taux para implementar 
otro TAD T. Entonces ¿qué es Taux? 
— ¿Una estructura de datos? 
— ¿Un TAD? 
— ¿Las dos cosas? 
 
 Definimos, de manera no del todo precisa, una estructura de datos como un 
tipo definido a partir de los tipos primitivos (enteros, caracteres, reales, …) y 
las constructoras de tipos (registros, arrays, punteros, …) que incluyen la ma-
yoría de los lenguajes de programación 
 
 Sin embargo, una parte importante de esta segunda parte del curso la dedica-
remos a estudiar las estructuras de datos más utilizadas junto con la implemen-
tación de las operaciones más habituales en su uso, explorando distintas 
alternativas. 
— Pero, ¡ esa es precisamente la definición de TAD ! : una estructura de datos 
junto con las operaciones que la manejan. 
— De hecho, resulta interesante abstraer las estructuras de datos más habitua-
les y convertirlas en TADs. 
 
 En resumen, nos vamos a dedicar a estudiar estructuras de datos diseñadas 
como tipos abstractos de datos, y a estudiar los tipos abstractos de datos más 
habituales junto con las estructuras de datos que permiten implementarlos efi-
cientemente. 
 
 
 
 
 
Tipos abstractos de datos 12 
 
3.2 Especificación algebraica de TADs 
 
 Una posible especificación para un tipo abstracto de datos consistiría en: 
— Identificar el conjunto de valores posibles 
— Escribir una especificación pre/post de cada una de las operaciones. 
 
 La especificación algebraica es un enfoque diferente que consiste en imaginar las 
operaciones de un TAD como análogas a las operaciones de un álgebra2 y des-
cribir su comportamiento por medio de ecuaciones: igualdades entre aplicaciones 
de las operaciones 
Las ecuaciones son más simples que las fórmulas lógicas utilizadas en la espe-
cificación pre/post formal y además 
— El razonamiento
con ecuaciones es relativamente sencillo y natural 
— Las ecuaciones no sólo especifican las propiedades de las operaciones, sino 
que especifican también cómo construir los valores del TAD 
 
Mecanismos básicos: especificación de los booleanos 
 
 Una especificación algebraica consta fundamentalmente de tres componentes 
— Tipos: son nombres de dominios de valores. Entre ellos está siempre el tipo 
principal del TAD, aunque puede haber también otros que se relacionen con 
éste. 
— Operaciones: deben ser funciones con un perfil asociado, que indique el ti-
po de cada uno de los argumentos y el tipo del resultado. En una especifi-
cación algebraica no se permiten funciones que devuelvan varios valores, ni 
tampoco procedimientos no funcionales. 
— Ecuaciones entre términos formados con las operaciones y variables de tipo 
adecuadas. 
 
 
2 Un álgebra es un dominio de valores equipado con operaciones que cumplen axiomas algebraicos dados. 
Tipos abstractos de datos 13 
 
Signatura de un TAD 
 
 Definimos la signatura de un TAD como los tipos que utiliza, junto con los 
nombres y los perfiles de las operaciones –sin incluir las ecuaciones–. 
 
 La signatura de los booleanos 
 tipo 
 Bool 
 operaciones 
 Cierto, Falso : → Bool 
 not : Bool → Bool 
 ( and ), ( or ) : (Bool, Bool) → Bool 
En la signatura de una operación utilizamos paréntesis para indicar notación 
infija, a diferencia de la notación prefija por defecto 
 
 Para escribir las ecuaciones es necesario clasificar las operaciones según el pa-
pel que queremos que jueguen en relación con el tipo principal: 
— Generadoras (o constructoras). Pensadas para construir todos los valores 
de tipo τ. Tienen un perfil de la forma 
c : τr → τ 
— Modificadoras. Pensadas para hacer cálculos que produzcan resultados de 
tipo τ, pero que no son generadoras. Tienen un perfil de la forma 
f : τr → τ 
— Observadoras. Pensadas para obtener valores de otros tipos a partir de va-
lores de tipo τ. Tienen un perfil de la forma 
g : τr → τ’ tal que τ’ ≡ τ; y algún τi ≡ τ 
 
 En este ejemplo 
— Generadoras: Cierto, Falso 
— Modificadoras: not, and, or 
 
 Por convenio se suele escribir el nombre de las generadoras con la primera le-
tra en mayúscula. 
 
 
 
Tipos abstractos de datos 14 
 
Términos 
 Dada la signatura de un TAD, con tipo principal τ, y un conjunto de variables 
X es posible construir un conjunto (normalmente infinito) de términos de tipo τ 
mediante la aplicación de las operaciones del TAD. 
Cada término representa una aplicación sucesiva de operaciones del TAD. 
 
 En el caso de los booleanos el conjunto de términos es de la forma 
TBool = { Cierto, Falso, not Cierto, Cierto, Falso or not 
Cierto, ... } 
 
 Un tipo especialmente importante de términos son aquellos que sólo 
contienen operaciones generadoras: los términos generados. 
Es necesario que las generadoras permitan construir al menos un término 
distinto para cada posible valor del tipo que pretendemos especificar 
TGBool = { Cierto, Falso } 
Ecuaciones 
 Las ecuaciones deben reflejar el comportamiento de las operaciones para 
cualquier aplicación correcta de las mismas. 
— Las ecuaciones deben permitir convertir cualquier término en un término 
generado: el resultado de la secuencia de operaciones que representa el 
término. 
— Mediante las ecuaciones ha de ser posible deducir todas las equivalencias 
que son válidas entre los términos, es decir, identificar las secuencias de 
operaciones que producen el mismo resultado. 
— Se deben evitar las ecuaciones redundantes. 
 
 ¿Qué ecuaciones necesitamos para especificar a los booleanos? 
— not 
 (1) not Cierto = Falso 
 (2) not Falso = Cierto 
— and 
 (3) Cierto and x = x 
 (4) Falso and x = Falso 
— or 
 (5) Cierto or x = Cierto 
 (6) Falso or x = x 
Tipos abstractos de datos 15 
 
 Usando estas ecuaciones podemos convertir cualquier término en un término 
generado. Por ejemplo, 
Cierto and Cierto and Falso =3 Cierto and Falso =3 Falso 
not Falso or Cierto =2 Cierto or Cierto =5 Cierto 
 
 Y es posible detectar las equivalencias entre los términos 
¿ Cierto and not Cierto = Falso and not Falso ? 
 
 Cierto and not Cierto Falso and not Falso 
 3= not Cierto 4= Falso 
 1= Falso 
Hemos obtenido el término generado al que era equivalente cada término y 
hemos comprobado que los dos son equivalentes al mismo. 
 Con todo lo anterior, la especificación de los booleanos queda finalmente 
 
tad BOOL 
 tipo 
 Bool 
 operaciones 
 Cierto, Falso : → Bool /* gen */ 
 not : Bool → Bool /* mod */ 
 ( and ), ( or ) : (Bool, Bool) → Bool /* mod */ 
 ecuaciones 
 ∀ x : Bool 
 not Cierto = Falso 
 not Falso = Cierto 
 Cierto and x = x 
 Falso and x = Falso 
 Cierto or x = Cierto 
 Falso or x = x 
ftad 
 
 
Tipos abstractos de datos 16 
 
TADs genéricos 
 
 Los TADs genéricos son TADs que dependen de otros –uno o más– de forma 
que es posible construir distintos ejemplares del TAD genérico según el tipo 
de los parámetros. 
 
 Los TADs genéricos representan típicamente colecciones de elementos. 
Estos TADs tienen un cierto comportamiento independiente del tipo de los 
elementos, aunque también puede haber operaciones que dependan de los 
elementos. 
Para expresar las exigencias sobre los elementos en especificación algebraica se 
utilizan clases de tipos. De esta forma, sólo se pueden construir ejemplares del 
TAD genérico utilizando TADs que pertenezcan a la clase indicada en su es-
pecificación. 
Clases de tipos 
 Una clase de tipos especifica el conjunto de operaciones que deben incluir los 
TADs que pertenecen a dicha clase. 
Los TADs pueden incluir operaciones adicionales 
 
 Las clases de tipos se organizan en una jerarquía con herencia, donde la clase 
ANY ocupa la raíz: 
clase ANY 
 tipo 
 Elem 
fclase 
 
Todos los TAD pertenecen a la clase de tipos ANY, siendo Elem un sinónimo 
del tipo principal del TAD. 
 
Tipos abstractos de datos 17 
 
 La clase de los tipos con igualdad 
clase EQ 
 hereda 
 ANY 
 usa 
 BOOL 
 operaciones 
 ( == ), ( /= ) : (Elem, Elem) → Bool 
 axiomas 
 // ( == ) es una operación de igualdad. 
 ecuaciones 
 ∀ x, y : Elem : 
 x /= y = not (x == y) 
fclase 
 
No se escriben las ecuaciones de la operación == porque no podemos 
suponer ningún conjunto de generadoras. 
 
 La clase de los tipos con orden 
 
clase ORD 
 hereda 
 EQ 
 operaciones 
 ( ≤ ), ( ≥ ), ( < ), ( > ) : (Elem, Elem) → Bool 
 axiomas 
 // ( ≤ ) es una operación de orden 
 ecuaciones 
 ∀ x, y : Elem : 
 x ≥ y = y ≤ x 
 x < y = (x ≤ y) and (x /= y) 
 x > y = y < x 
fclase 
 
Tipos abstractos de datos 18 
 
Operaciones parciales: especificación de las pilas 
 En muchas ocasiones tendremos que especificar TADs que incluyan operacio-
nes parciales. En ese caso existirán términos que no están definidos y que de-
ben recibir un tratamiento cuidadoso. 
 
 Como ejemplo construiremos la especificación del TAD genérico PILA. Una 
pila representa una colección de valores donde sólo es posible acceder al últi-
mo elemento añadido. 
tad PILA[E :: ANY] 
 usa 
 BOOL 
 tipo 
 Pila[Elem] 
 operaciones 
 PilaVacía: → Pila[Elem] /* gen */ 
 Apilar: (Elem, Pila[Elem]) → Pila[Elem] /* gen */ 
 desapilar: Pila[Elem] – → Pila[Elem] /* mod */ 
 cima: Pila[Elem] – → Elem /* obs */ 
 esVacía: Pila[Elem] → Bool /* obs */ 
 
Las operaciones desapilar y cima son parciales porque no tiene sentido quitar un 
elemento ni consultar por la cima de una pila vacía. El carácter parcial de una 
operación se nota con una flecha discontinua, − →, en su perfil. 
 
 Para escribir las ecuaciones de operaciones parciales utilizamos un nuevo ele-
mento en las especificaciones: los axiomas de definición. 
— Los axiomas de definición determinan la forma que tienen los términos de-
finidos que involucran
operaciones parciales. En el ejemplo de las pilas: 
 def desapilar(Apilar(x, xs)) 
 def cima(Apilar(x, xs)) 
 
 Cuando hay operaciones parciales, además de los axiomas de definición, in-
cluimos una sección errores es la especificación, donde se indica qué forma tie-
nen los términos indefinidos. Aunque, en general, esto es información 
redundante, se incluye por claridad. 
— En el caso de las pilas 
 errores 
 desapilar(Pilavacía) 
 cima(PilaVacía) 
Tipos abstractos de datos 19 
 
 Con todo esto, la especificación de las pilas queda 
 tad PILA[E :: ANY] 
 usa 
 BOOL 
 tipo 
 Pila[Elem] 
 operaciones 
 PilaVacía: → Pila[Elem] /* gen */ 
 Apilar: (Elem, Pila[Elem]) → Pila[Elem] /* gen */ 
 desapilar: Pila[Elem] – → Pila[Elem] /* mod */ 
 cima: Pila[Elem] – → Elem /* obs */ 
 esVacía: Pila[Elem] → Bool /* obs */ 
 
 ecuaciones 
 ∀ x : Elem : ∀ xs : Pila[Elem] : 
 def desapilar(Apilar(x, xs)) 
 desapilar(Apilar(x, xs)) = xs 
 def cima(Apilar(x, xs)) 
 cima(Apilar(x, xs)) = x 
 esVacía(PilaVacía) = Cierto 
 esVacía(Apilar(x, xs)) = Falso 
 
 errores 
 desapilar(Pilavacía) 
 cima(PilaVacía) 
ftad 
 
 
 
 
 
Tipos abstractos de datos 20 
 
3.3 Implementación de TADs 
 Dada la especificación de un TAD T, su implementación consiste en: 
— Un dominio concreto Dτ para cada tipo τ incluido en T. 
Los dominios concretos se implementan mediante declaraciones de tipos, 
usando otros tipos ya implementados –por nosotros o incluidos en el len-
guaje– que definen el tipo representante. 
— Una operación concreta 
fC : Dτ1, …, Dτn → Dτ 
para cada operación de la especificación 
f : τ1, …, τn → τ 
Las operaciones concretas pueden implementarse como procedimientos 
aunque en la especificación algebraica sólo se admitan funciones. 
 
 La implementación ha de satisfacer dos requisitos: 
— Corrección: la implementación debe satisfacer las ecuaciones de la especifi-
cación 
— Privacidad y protección: la estructura interna de los datos debe estar oculta 
y el único acceso posible al tipo debe ser a través de las operaciones públi-
cas de éste. 
Dependiendo del lenguaje de implementación y los mecanismos de modu-
laridad que proporcione, este requisito se podrá reflejar directamente en la 
implementación o se convertirá en una cuestión de disciplina de uso. 
 
 En las implementaciones utilizaremos C++. 
 
Tipos abstractos de datos 21 
 
3.3.1 Implementación correcta de un TAD: implementación 
de las pilas de enteros 
Implementación del tipo 
 
 Pretendemos implementar el TAD PILA[INT]. 
— En primer lugar, consideramos que el tipo predefinido int implementa co-
rrectamente el TAD INT, donde el tipo representante de Int es int y las 
operaciones del TAD están implementadas como las operaciones predefi-
nidas disponibles sobre int. 
— Una vez fijada la implementación del TAD de los elementos, debemos de-
cidir cuál es el tipo representante de Pila[Int]. 
En este caso elegimos representar las pilas como un registro con dos cam-
pos, uno que es un vector donde se almacenan los elementos y otro que es 
un índice que apunta a la cima de la pila dentro del vector. 
Por ejemplo la representación de 
Apilar( 2, Apilar( 7, Apilar( 5, PilaVacía ))) 
vendría dada por: 
5 7 2 …
indCima 
Esta implementación tiene el inconveniente de que impone a priori un lími-
te al tamaño máximo de la pila. 
 
 El tipo representante escogido es: 
int const limite = 100; 
 
typedef struct { 
 int espacio[limite]; 
 int indCima; 
} TPilaInt; 
 
 Para luego poder plantear una implementación correcta de las operaciones, 
conviene comenzar definiendo qué valores concretos queremos aceptar como 
representantes válidos de valores abstractos del TAD –la idea es que el tipo repre-
sentante elegido puede tomar valores que no consideremos válidos–. 
Tipos abstractos de datos 22 
 
Invariante de la representación 
 Empezamos formalizando qué condiciones les exigimos a los representantes 
válidos: el invariante de la representación. 
 RPila[Int] (xs) 
⇔def 
 xs : TPilaInt ∧ 
 -1 ≤ xs.indCima < limite ∧ 
 ∀ i : 0 ≤ i ≤ xs.indCima : RInt(xs.espacio[i]) 
 
 Los representantes válidos son aquellos valores del tipo representante que 
cumplen el invariante de la representación. 
 
 El invariante de la representación está relacionado con la corrección de la im-
plementación de la siguiente forma: 
— Las operaciones sólo están obligadas a funcionar sobre representantes váli-
dos. 
— Las operaciones que generen valores del tipo representante deben com-
prometerse a que éstos sean siempre representantes válidos. 
 
 Lo importante no es escribir el invariante de la representación formalmente, 
sino determinar (aún con una descripción informal) de entre todos los valores 
que puede tomar el tipo representante cuáles representan valores válidos del 
TAD que estamos diseñando. 
Tipos abstractos de datos 23 
 
Implementación de las operaciones 
 Para cada operación de la especificación 
f : (τ1, …, τn) – → τ 
debemos implementar una operación fC que satisfaga una especificación 
pre/post de la forma 
TRτ fC ( TRτ1 x1, … , TRτn xn ) { 
// P : Rτ1(x1) ∧ … ∧ Rτn(xn) ∧ DOMf(x1, …, xn) ∧ LIMf(x1, …, 
xn) 
 TRτ y; 
// Q : Rτ(y) ∧ y = f(x1, …, xn) 
 return y; 
} 
donde 
— TRτi, TRτ son los tipos representantes de los argumentos y el resultado 
— Rτi, Rτ son los invariantes de la representación para dichos tipos 
— DOMf es la condición de dominio, donde se imponen las restricciones 
necesarias para que la operación esté definida 
DOMf(x1, …, xn) ⇔ def f(x1, …, xn) 
— LIMf expresa limitaciones adicionales impuestas por la implementación 
 
 Por ejemplo, 
— la especificación de Apilar de Pila[Int] 
void Apilar ( int x, TPilaInt & xs ) { 
// P: RInt(x) ∧ RPila[Int](xs) ∧ xs.indCima < límite-1 ∧ xs = 
XS 
// Q : RPila[Int](xs) ∧ xs = Apilar(x, XS) 
} 
donde xs.indCima < límite – 1 es una limitación de la implementación 
— la especificación de cima de Pila[Int] 
int cima ( TPilaInt xs ) { 
// P : RPila[Int](xs) ∧ xs.indCima > -1 
 int x; 
// Q : RInt(x) ∧ x = cima(xs) 
 return x; 
} 
donde xs.indCima > –1 es una condición de dominio, ya que, de acuerdo 
con la especificación, cima no está definido sobre una pila vacía. 
 
Tipos abstractos de datos 24 
 
 Existen técnicas formales que permiten demostrar la corrección de la imple-
mentación de las operaciones con respecto a la especificación algebraica. Estas 
técnicas se basan en el uso de funciones de abstracción que permiten traducir los 
valores representantes al modelo formal de la especificación, en términos del 
cual se puede realizar la demostración. 
 
 Las anteriores especificaciones exponen detalles de la implementación elegida, 
violando así el principio de ocultación: xs.indCima < limite–1 y xs.indCima > –1. 
Esto se puede resolver sustituyendo estas condiciones por funciones boolea-
nas que las comprueben. 
— Normalmente las condiciones de dominio se podrán expresar directamente 
utilizando alguna operación de la especificación (esVacía). 
— Para expresar las limitaciones de la implementación será necesario añadir 
nuevas funciones a las que aparezcan en la especificación (esLlena). 
void Apilar ( int x, TPilaInt & xs ) 
// P: RInt(x) ∧ RPila[Int](xs) ∧ ¬ esLlena(xs) ∧ xs = XS 
 
int cima ( TPilaInt xs ) 
// P : RPila[Int](xs) ∧ ¬ esVacia(xs) 
Donde la función esVacia ya forma parte de la especificación y la función esLle-
na se puede especificar de la siguiente forma: 
bool esLlena ( TPilaInt xs ) { 
// P: RPila[Int](xs) 
// Q: xs esLlena si tiene 'limite' elementos 
} 
Nótese que aunque esta función viola el principio de ocultación (nos hace su-
poner que la pila se ha implementado en un array de limite elementos) es una 
información que se debe proporcionar a los usuarios del TAD. 
 
 El uso de funciones booleanas en las precondiciones y su consiguiente imple-
mentación proporciona además
la ventaja de permitir a los usuarios del TAD 
comprobar si una precondición se cumple antes de invocar a una determinada 
operación. 
if ( ! esLlena(xs) ) 
 Apilar( x, xs ); 
 
 
 
 
Tipos abstractos de datos 25 
 
 ¿Y qué hacer con el invariante de la representación? 
Podemos aplicar la misma idea e incluir en el TAD una función booleana es-
Valido que, dado un valor del tipo representante, determine si es un represen-
tante válido. 
En el ejemplo de Pila[Int] que nos ocupa, dicha operación se especifica como: 
bool esValido ( TPilaInt xs ) { 
// P: 
// Q: determina si xs es un representante válido de TPilaInt 
} 
Una función como esta puede resultar de gran utilidad para detectar errores en 
la implementación de un TAD. 
 
 Con todo esto, y considerando que 
— los valores de tipos primitivos siempre cumplen su invariante de la repre-
sentación, y 
— las condiciones de la forma y = f(x1, …, xn) aportan poca información, 
la especificación pre/post de las operaciones queda: 
void Apilar ( int x, TPilaInt & xs ) { 
// P: esValido(xs) && ! esLlena(xs) && xs = XS 
// Q: esValido(xs) && xs es el resultado de apilar x en XS 
} 
 
int cima ( TPilaInt xs ) { 
// P: esValido(xs) && ! esVacia(xs) 
// Q: devuelve la cima de xs 
} 
 
 Nótese que en la última versión de las especificaciones se han sustituido los 
operadores lógicos por sus equivalentes en C++. Esto nos hace pensar que las 
precondiciones son evaluables (salvo las condiciones sobre variables auxiliares 
de la especificación del tipo xs = XS) y que se pueden implementar las opera-
ciones de un TAD comprobando que se cumple la precondición: 
if ( P ) 
 // implementación de la operación 
else 
 error(“No se cumple la precondición”); 
Evidentemente, estas comprobaciones adicionales suponen una penalización 
en el tiempo de ejecución de las operaciones. 
Tipos abstractos de datos 26 
 
 Es una técnica habitual añadir comprobaciones adicionales durante el desarro-
llo de un TAD para ayudar a depurarlo, para luego suprimir dichas comproba-
ciones en la versión definitiva. Un forma sencilla de implementar esta idea 
consiste en añadir una variable booleana global que controle si se han de eva-
luar las precondiciones. 
int cima ( TPilaInt xs ) { 
// P: esValido(xs) && ! esVacia(xs) 
 int r; 
 if ( ! depurando || ( esValido(xs) && ! esVacia(xs) ) ) 
 r = xs.espacio[xs.indCima]; 
 else 
 error("No se cumple la precondición de cima"); 
 return r; 
// Q: devuelve la cima de xs 
} 
 
 El problema de esta técnica es que supone que en algún momento estaremos 
seguros de que nuestra aplicación es correcta y podremos desactivar la evalua-
ción de las condiciones de corrección. 
Sin embargo, en general, esta es una suposición demasiado arriesgada. 
 
 Una solución alternativa, y más habitual, es mantener en la versión final una 
parte de las condiciones de corrección, las que se refieren a las condiciones de 
dominio y los límites de la implementación, pero no así la comprobación de la 
validez de los valores. 
 
 Para facilitar el uso de los TADs incluiremos el tratamiento de los errores de-
bidos a las condiciones de dominio y los límites de la implementación, para lo 
cual existen distintas alternativas: 
— mostrar un mensaje y detener la ejecución 
— mostrar el mensaje y no detener la ejecución 
— implementar un mecanismo que permita a los clientes del TAD saber si se 
ha producido un error y actuar en consecuencia 
La más amigable para el cliente del TAD es esta última, sin embargo, para lle-
varla a la práctica es aconsejable que el lenguaje incluya algún mecanismo que 
facilite su implementación. 
 
 
Tipos abstractos de datos 27 
 
Implementación de las operaciones de TPilaInt 
 Con todo esto, la implementación de las operaciones de TPilaInt queda de la 
siguiente forma 
 
// función auxiliar para identificar los representantes 
válidos 
 
bool esValido ( TPilaInt xs ) { 
// P: 
 return ( xs.indCima >= -1 ) && ( xs.indCima < limite ); 
// Q: determina si xs es un representante válido de TPilaInt 
} 
 
// función auxiliar para determinar si una pila está llena 
 
bool esLlena ( TPilaInt xs ) { 
// P: esValido(xs) 
 return xs.indCima == limite - 1; 
// Q: xs esLlena si tiene 'limite' elementos 
} 
 
// función auxiliar privada para mostrar un mensaje de error 
 
void error( string mensaje ) { 
 cout << "Error: " + mensaje; 
} 
 
// operaciones incluidas en la especificación 
 
void PilaVacia( TPilaInt & xs ) { 
// P: 
 xs.indCima = -1; 
// Q: esValido(xs) && xs representa a la pila vacía 
} 
 
void Apilar ( int x, TPilaInt & xs ) { 
// P: esValido(xs) && ! esLlena(xs) && xs = XS 
 if ( esLlena(xs) ) 
 error("No se puede apilar porque la pila está llena"); 
 else { 
 xs.indCima = xs.indCima + 1; 
 xs.espacio[xs.indCima] = x; 
 } 
// Q: esValido(xs) && xs es el resultado de apilar x en XS 
} 
bool esVacia ( TPilaInt xs ) { 
// P: esValido(xs) 
 return xs.indCima == -1; 
// Q: determina si xs está vacía 
} 
 
Tipos abstractos de datos 28 
 
int cima ( TPilaInt xs ) { 
// P: esValido(xs) && ! esVacia(xs) 
 int r; 
 if ( esVacia( xs ) ) 
 error("La pila vacía no tiene cima"); 
 else 
 r = xs.espacio[xs.indCima]; 
 return r; 
// Q: devuelve la cima de xs 
} 
 
void desapilar( TPilaInt & xs ) { 
// P: esValido(xs) && ! esVacia(xs) && xs = XS 
 if ( esVacia(xs) ) 
 error("no se puede desapilar de la pila vacía"); 
 else 
 xs.indCima = xs.indCima - 1; 
// Q: esValido(xs) && xs es el resultado de desapilar de XS 
} 
 
 
Tipos abstractos de datos 29 
 
3.3.2 Distintas implementaciones de un mismo TAD: 
implementación de los conjuntos de enteros 
 
 Vamos a plantear tres posibles implementaciones para el TAD CJTO[INT]: 
— Vectores de elementos de tipo int 
— Vectores de int sin repetición 
— Vectores de int sin repetición y ordenados 
 
 En los tres casos elegimos el mismo tipo representante 
int const longMax = 100; 
typedef struct { 
 int espacio[longMax]; 
 int longitud; 
} TCjtoInt; 
 
 El invariante de la representación 
— Vectores 
 RCjto[Int] (xs) 
⇔def 
 xs : TCjtoInt ∧ 
 0 ≤ xs.longitud ≤ longMax ∧ 
 ∀ i : 0 ≤ i < xs.longitud : RInt(xs.espacio[i]) 
 
— Vectores sin repetición 
 RCjto[Int] (xs) 
⇔def 
 xs : TCjtoInt ∧ 
 0 ≤ xs.longitud ≤ longMax ∧ 
 ∀ i : 0 ≤ i < xs.longitud : RInt(xs.espacio[I]) ∧ 
 ∀ i, j : 0 ≤ i < j < xs.longitud : xs.espacio[i] != 
xs.espacio[j] 
 
— Vectores sin repetición y ordenados 
 RCjto[Int] (xs) 
⇔def 
 xs : TCjtoInt ∧ 
 0 ≤ xs.longitud ≤ longMax ∧ 
 ∀ i : 0 ≤ i < xs.longitud : RInt(xs.espacio[i]) ∧ 
 ∀ i, j : 0 ≤ i < j < xs.longitud: xs.espacio[i] < 
xs.espacio[j] 
Tipos abstractos de datos 30 
 
Implementación de las operaciones 
Operaciones auxiliares 
 
 En los tres casos necesitamos una función de búsqueda, en los dos primeros 
búsqueda secuencial mientras que en el tercero podemos usar búsqueda binaria 
 
 Búsqueda secuencial en un intervalo de un array de int. Necesitamos especifi-
car a partir de dónde buscar porque tendremos que realizar búsquedas sucesi-
vas (posibles repeticiones). 
 
void busca ( int x , int v[], int ini, int num, 
 bool & encontrado, int & pos ) { 
// P: v tiene al menos num elementos 
 int i; 
 
 if ( ini >= num ) { 
 encontrado = false; 
 pos = num; 
 } 
 else { 
 encontrado = false; 
 i = ini; 
 while ( ! encontrado && (i < num) ) { 
 encontrado = v[i] == x; 
 if ( ! encontrado ) i++; 
 } 
 pos = i; 
 } 
// Q : encontrado es true si existe un i en el intervalo 
ini..num-1 
// tal que v[i] == x 
// si encontrado es true entonces pos = el minimo i en 
// el intervalo ini..num-1 tal que v[i] == x 
// si no encontrado entonces pos == num 
} 
Tipos abstractos de datos 31 
 
 Búsqueda binaria en un array de enteros 
int buscaBin( int v[], int x, int a, int b ) { 
// Pre: v está ordenado entre 0 ..
num-1 
// ( 0 <= a <= num ) && ( -1 <= b <= num ) && ( a <= b+1 
) 
// todos los elementos a la izquierda de 'a' son <= x 
// todos los elementos a la derecha de 'b' son > x 
 
 int p, m; 
 
 if ( a == b+1 ) 
 p = a - 1; 
 else if ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 p = buscaBin( v, x, m+1, b ); 
 else 
 p = buscaBin( v, x, a, m-1 ); 
 } 
 return p; 
// Post: devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
} 
 
void buscaBin( int x, int v[], int num, bool & encontrado, int 
& pos ) { 
// Pre: los num primeros elementos de v están ordenados 
// num >= 0 
 pos = buscaBin(v, x, 0, num-1); 
 encontrado = ( pos >= 0 ) && ( pos < num ) && ( v[pos] == x 
); 
 
// Post : devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
// encontrado es true si x esta en v[0..num-1] 
} 
 
 En los tres casos necesitamos una función esLleno para comprobar la condición 
impuesta por las limitaciones de la implementación 
bool esLleno ( TCjtoInt xs ) { 
// P: esValido(xs) 
 return xs.longitud == longMax; 
// Q: esLleno si xs tiene 'longMax' elementos 
} 
Tipos abstractos de datos 32 
 
Operaciones de la especificación 
 La generadora Vacio y la observadora esVacio se implementan de la misma 
forma en los tres casos 
void Vacio( TCjtoInt & xs ) { 
// P: 
 xs.longitud = 0; 
// Q: esValido(xs) && xs representa al conjunto vacío 
} 
 
 
bool esVacio ( TCjtoInt xs ) { 
// P: esValido(xs) 
 return xs.longitud == 0; 
// Q: determina si el conjunto está vacío 
} 
 
Vectores 
 Función que comprueba la validez de los representantes 
bool esValido ( TCjtoInt xs ) { 
// P: 
 return xs.longitud >= 0 && xs.longitud <= longMax; 
// Q: determina si xs es un representante valido de TCjtoInt 
} 
 
 La generadora Pon 
 
void Pon( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) && ! esLleno(xs) 
 if ( esLleno(xs) ) 
 error("No se puede insertar el elemento"); 
 else { 
 xs.espacio[xs.longitud] = x; 
 xs.longitud++; 
 } 
// Q: esValido(xs) && xs es el resultado de añadir x a XS 
} 
 
 
 
 
 
Tipos abstractos de datos 33 
 
 La modificadora quita 
void quita( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) 
 bool encontrado; 
 int pos; 
 
 busca( x, xs.espacio, 0, xs.longitud, encontrado, pos); 
 while ( encontrado ) { 
 xs.longitud--; 
 xs.espacio[pos] = xs.espacio[xs.longitud]; 
 busca( x, xs.espacio, pos, xs.longitud, encontrado, 
pos); 
 } 
// Q: esValido(xs) && xs es el resultado de quitar x de XS 
} 
 
 La observadora pertenece 
bool pertenece( int x, TCjtoInt xs ) { 
// P: esValido(xs) 
 bool encontrado; 
 int pos; 
 
 busca( x, xs.espacio, 0, xs.longitud, encontrado, pos); 
 return encontrado; 
// Q: determina si x está en xs 
} 
 
Tipos abstractos de datos 34 
 
Vectores sin repetición 
 Función que comprueba la validez de los representantes 
bool esValido ( TCjtoInt xs ) { 
// P: 
 bool valido; 
 int i, j; 
 
 valido = (xs.longitud >= 0) && (xs.longitud <= longMax); 
 i = 0; 
 while ( valido && (i < xs.longitud - 1) ) { 
 j = i + 1; 
 while ( valido && (j < xs.longitud) ) { 
 valido = xs.espacio[i] != xs.espacio[j]; 
 j++; 
 } 
 i++; 
 } 
 return valido; 
// Q: determina si xs es un representante valido de TCjtoInt 
} 
 
 
 La observadora pertenece tiene la misma implementación que en el caso anterior 
 
 
 La generadora Pon 
void Pon( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) && ( ! esLleno(xs) || 
pertenece(x, xs) ) 
 if ( ! pertenece(x, xs) ) 
 if ( esLleno(xs) ) 
 error("No se puede insertar el elemento"); 
 else { 
 xs.espacio[xs.longitud] = x; 
 xs.longitud++; 
 } 
// Q: esValido(xs) && xs es el resultado de añadir x a XS 
} 
 
 
 
 
 
 
Tipos abstractos de datos 35 
 
 La modificadora quita 
void quita( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) 
 bool encontrado; 
 int pos; 
 
 busca( x, xs.espacio, 0, xs.longitud, encontrado, pos); 
 if ( encontrado ) { 
 xs.longitud--; 
 xs.espacio[pos] = xs.espacio[xs.longitud]; 
 } 
// Q: esValido(xs) && xs es el resultado de quitar x de XS 
} 
 
Vectores sin repetición y ordenados 
 
 Función que comprueba la validez de los representantes 
 
bool esValido ( TCjtoInt xs ) { 
// P: 
 bool valido; 
 int i; 
 
 valido = (xs.longitud >= 0) && (xs.longitud <= longMax); 
 i = 0; 
 while ( valido && (i < xs.longitud - 1) ) { 
 valido = xs.espacio[i] < xs.espacio[i+1]; 
 i++; 
 } 
 return valido; 
// Q: determina si xs es un representante valido de TCjtoInt 
} 
 
 
Tipos abstractos de datos 36 
 
 La generadora Pon 
 
void Pon( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) && ( ! esLleno(xs) || 
pertenece(x, xs) ) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, xs.espacio, xs.longitud, encontrado, pos); 
 if ( ! encontrado ) { 
 if ( esLleno(xs) ) 
 error("No se puede insertar el elemento"); 
 else { 
 for ( int k = xs.longitud; k > pos+1; k-- ) 
 xs.espacio[k] = xs.espacio[k-1]; 
 xs.espacio[pos+1] = x; 
 xs.longitud++; 
 } 
 } 
// Q: esValido(xs) && xs es el resultado de añadir x a XS 
} 
 
 
 La modificadora quita 
void quita( int x, TCjtoInt & xs ) { 
// P: xs = XS && esValido(xs) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, xs.espacio, xs.longitud, encontrado, pos); 
 if ( encontrado ) { 
 for ( int k = pos; k < xs.longitud-1; k++ ) 
 xs.espacio[k] = xs.espacio[k+1]; 
 xs.longitud--; 
 } 
// Q: esValido(xs) && xs es el resultado de quitar x de XS 
} 
 
 
Tipos abstractos de datos 37 
 
 La observadora pertenece 
bool pertenece( int x, TCjtoInt xs ) { 
// P: esValido(xs) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, xs.espacio, xs.longitud, encontrado, pos); 
 return encontrado; 
// Q: determina si x está en xs 
} 
 
Comparación de las distintas implementaciones 
 
 Para determinar cuál es la complejidad temporal en el caso peor de cada una 
de las implementaciones, debemos establecer qué consideramos como tamaño 
de los datos: 
— En los vectores con repetición 
n1 = nº de elementos del array 
— En los vectores sin repetición, ordenados o no, 
n2 = nº de elementos del conjunto 
 
 Vector Vector sin repetición Vector ordenado 
Vacio O(1) O(1) O(1) 
Pon O(1) O(n2) O(n2) 
quita O(n1) O(n2) O(n2) 
esVacio O(1) O(1) O(1) 
pertenece O(n1) O(n2) O(log n2) 
 
¿Qué implementación es mejor? 
Depende de la información adicional que podamos utilizar: 
— ¿n1 es mucho mayor que n2 o son parecidas? 
— ¿la operación pertenece es muy habitual o no? 
— ¿queremos minimizar el consumo de espacio? 
Si no dispusiésemos de información adicional, deberíamos ser pesimistas y ele-
gir la implementación con vectores ordenados. 
Tipos abstractos de datos 38 
 
3.4 Implementación modular de TADs 
 La modularidad es un mecanismo que permite descomponer el código de un 
sistema software. Esta descomposición puede resultar superflua en programas 
pequeños, pero es fundamental en aplicaciones de tamaño real. 
Al preocuparnos por la modularidad, vamos abandonando el ámbito de la 
Programación para adentrarnos en el de la Ingeniería del Software. 
 
 El objetivo de la Ingeniería del Software es el estudio de las técnicas que per-
miten la construcción eficiente (en términos de recursos humanos y económi-
cos) de sistemas software de calidad. 
 
 Las fases del “ciclo de vida” del software, en su versión clásica, son 
— Análisis. De la interacción con los usuarios se obtiene una especificación de 
los requisitos. 
— Diseño. Se determina la estructura general del sistema. Dependiendo del 
tamaño puede convenir distinguir entre el “diseño de alto nivel” y el “dise-
ño de bajo nivel”.
El resultado del diseño es una descomposición del sis-
tema a desarrollar. 
— Implementación. Se escribe el código de cada uno de las partes identifica-
das en la fase de diseño, que, una vez implementadas y probadas, se inte-
gran para obtener el sistema final. 
— Prueba. Se intentan detectar y subsanar los errores que se hayan cometido 
en el desarrollo. 
— Mantenimiento. Una vez que el sistema ya está en uso, será necesario seguir 
incorporando cambios y correcciones. 
 
 Los módulos son un mecanismo a nivel de implementación que permiten re-
flejar en el código de la aplicación la descomposición definida por el diseño: 
— Separación física en archivos que se pueden compilar por separado 
— Separación lógica en ámbitos de visibilidad separados 
En general, se entiende que el diseño de una aplicación es la descomposición 
en módulos de la misma, junto con las relaciones entre dichos módulos. 
 
 
 
Tipos abstractos de datos 39 
 
3.4.1 Implementación modular de TADs en C++ 
Utilizando unidades 
 
 En el archivo de cabecera (.h) se incluye: 
— El tipo representante 
— La cabecera de los procedimientos o funciones que implementan las opera-
ciones del TAD. 
— La cabecera de otras operaciones auxiliares que aunque no apareciesen en 
la especificación pueden resultar útiles a los clientes del TAD 
 
 En el archivo de implementación (.cpp) se incluye: 
— La implementación de las operaciones que aparecen en la interfaz 
— Otros tipos auxiliares 
— Otras operaciones auxiliares 
 
 Por ejemplo, la implementación de los conjuntos de enteros 
 
//------------------------------------------------------------
------------- 
#ifndef cjto_intH 
#define cjto_intH 
//------------------------------------------------------------
------------- 
 
/* 
 Implementación de los conjuntos de enteros en vectores 
ordenados sin 
 repetición. 
 Tamaño de los datos n = número de elementos almacenados en 
el conjunto. 
 La constructora Vacio proporciona un representante válido 
del tipo, 
 Todas las operaciones esperan representantes válidos y 
devuelven 
 representantes válidos. 
 Como máximo se pueden almacenar longMax elementos 
*/ 
 
int const longMax = 100; 
 
typedef struct { 
 int espacio[longMax]; 
 int longitud; 
} TCjtoInt; 
Tipos abstractos de datos 40 
 
 
void Vacio( TCjtoInt & xs ); 
// P: true 
// Q: esValido(xs) && xs representa al conjunto vacío 
// O(1) 
 
bool esVacio ( TCjtoInt xs ); 
// P: esValido(xs) 
// Q: determina si el conjunto está vacío 
// O(1) 
 
void Pon( int x, TCjtoInt & xs ); 
// P: xs = XS && esValido(xs) && ( ! esLleno(xs) || 
pertenece(x, xs) ) 
// Q: esValido(xs) && xs es el resultado de añadir x a XS 
// O(n) 
 
void quita( int x, TCjtoInt & xs ); 
// P: xs = XS && esValido(xs) 
// Q: esValido(xs) && xs es el resultado de quitar x de XS 
// O(n) 
 
bool pertenece( int x, TCjtoInt xs ); 
// P: esValido(xs) 
// Q: determina si x está en xs 
// O(log n) 
 
bool esLleno ( TCjtoInt xs ); 
// P: esValido(xs) 
// Q: esLleno si xs tiene 'longMax' elementos 
// O(1) 
 
bool esValido ( TCjtoInt xs ); 
// P: true 
// Q: determina si xs es un representante valido de TCjtoInt 
// O(n) 
 
void escribe( TCjtoInt xs ); 
// P: esValido(xs) 
// Q: muestra por pantalla el contenido de xs 
 
#endif 
 
 
 
 
 
Tipos abstractos de datos 41 
 
 Y el archivo de implementación cjto_int.cpp 
 
//------------------------------------------------------------
------------- 
 
#include "cjto_int.h" 
#include <iostream> 
#include <string> 
using namespace std; 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILIARES PRIVADAS 
// 
 
// 
// función auxiliar privada para mostrar un mensaje de error 
// 
void error( string mensaje ) { 
 cout << "Error: " + mensaje; 
} 
 
// 
// función auxiliar de búsqueda en el array 
// 
 
... 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES PUBLICAS 
// 
 
void Vacio( TCjtoInt & xs ) { 
// P: true 
 xs.longitud = 0; 
// Q: esValido(xs) && xs representa al conjunto vacío 
} 
 
... 
 
Tipos abstractos de datos 42 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES DE ESCRITURA 
// 
 
void escribe( TCjtoInt xs ) { 
// P: esValido(xs) 
 cout << "("; 
 for ( int i = 0; i < xs.longitud; i++ ) { 
 cout << xs.espacio[i]; 
 if ( i < xs.longitud-1 ) 
 cout << ", "; 
 } 
 cout << ")" << endl; 
// Q: muestra por pantalla el contenido de xs 
} 
 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILARES DE LA ESPECIFICACION 
// 
 
// 
// Limitación impuesta por la implementación (esLleno) 
// 
bool esLleno ( TCjtoInt xs ) { 
// P: esValido(xs) 
 return xs.longitud == longMax; 
// Q: esLleno si xs tiene 'longMax' elementos 
} 
 
// 
// Función que determina si un valor del tipo es un 
representante válido 
// 
 
... 
 
//------------------------------------------------------------
------------- 
 
Tipos abstractos de datos 43 
 
Utilizando unidades y clases 
 
 Implementando los TADs como clases en C++, podemos proteger el acceso a 
la representación de la información. 
 
 En el archivo de cabecera se escribe la definición de la clase 
— Implementamos al menos una de las generadoras como una constructora 
de objetos de la clase. 
— La estructura de datos se declara como variables privadas de la clase. 
— El dato del tipo principal que las operaciones observan o modifican se 
convierte en el parámetro implícito de los métodos de la clase: el objeto 
que recibe el mensaje. 
— En las especificaciones pre/post sustituimos el parámetro del tipo principal 
por la pseudo-variable this o simplemente lo obviamos. 
— Dado que la implementación con clases sí que garantiza la privacidad y 
protección del tipo representante, asumimos que los clientes del TAD no 
pueden construir valores del tipo que no sean representantes válidos y eli-
minamos la condición esValido de las especificaciones. 
 
 
/* 
 Implementación de los conjuntos de enteros en vectores 
ordenados sin 
 repetición. 
 Tamaño de los datos n = número de elementos almacenados en 
el conjunto. 
 Como máximo se pueden almacenar longMax elementos 
*/ 
 
 
class TCjtoInt { 
 public: 
 
 // Tamaño máximo 
 static const int longMax = 100; 
 
 // Constructora 
 TCjtoInt( ); 
 // Pre: true 
 // Post: construye un valor que representa al 
 //conjunto vacío O(1) 
 
 
Tipos abstractos de datos 44 
 
 // Operaciones de los conjuntos 
 bool esVacio ( ); 
 // Pre: true 
 // Post: determina si el conjunto está vacío 
 // O(1) 
 
 void Pon( int x ); 
 // Pre: ! esLleno( ) || pertenece(x) 
 // Post: modifica el conjunto, añadiéndole x 
 // O(n) 
 
 void quita( int x ); 
 // Pre: true 
 // Post: modifica el conjunto, quitando x 
 // O(n) 
 
 bool pertenece( int x ); 
 // Pre: true 
 // Post: determina si x está en el conjunto 
 // O(log n) 
 
 bool esLleno ( ); 
 // Pre: true 
 // Post: determina si el conjunto tiene 'longMax' 
elementos 
 // O(1) 
 
 // Escritura 
 void escribe( ); 
 // Pre: true 
 // Post: muestra por pantalla el contenido del conjunto 
 
 private: 
 // Variables privadas 
 int _espacio[longMax]; 
 int _longitud; 
}; 
 
Por convenio escribimos los nombres de las variables miembro precedidas de 
un guión bajo _. 
 
 Al construirse un valor de un tipo definido por una clase,
se ejecuta necesa-
riamente una constructora de esa clase, asegurando así que los objetos están 
inicializados. 
TCjtoInt cjto; // se ejecuta la constructora por defecto 
 En la implementación de las operaciones eliminamos las referencias explícitas 
al parámetro del tipo principal. 
Tipos abstractos de datos 45 
 
Por ejemplo 
 
bool pertenece( int x, TCjtoInt xs ) { 
// P: esValido(xs) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, xs.espacio, xs.longitud, encontrado, pos); 
 return encontrado; 
// Q: determina si x está en xs 
} 
 
Se convierte en 
 
bool TCjtoInt::pertenece( int x ) { 
// Pre: true 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, _espacio, _longitud, encontrado, pos); 
 return encontrado; 
// Post: determina si x está en el conjunto 
} 
 
Nótese que la versión orientada a objetos es equivalente a 
 
bool TCjtoInt::pertenece( int x ) { 
// Pre: true 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, this->_espacio, this->_longitud, encontrado, 
pos); 
 return encontrado; 
// Post: determina si x está en el conjunto 
} 
 
Tipos abstractos de datos 46 
 
 Y el archivo de implementación cjto_int.cpp 
 
//------------------------------------------------------------
------------- 
 
#include "cjto_int.h" 
#include <iostream> 
#include <string> 
using namespace std; 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILIARES PRIVADAS 
// 
 
// 
// función auxiliar privada para mostrar un mensaje de error 
// 
void error( string mensaje ) { 
 cout << "Error: " + mensaje; 
} 
 
// 
// función auxiliar de búsqueda en el array 
// 
 
... 
 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES PUBLICAS 
// 
 
 
TCjtoInt::TCjtoInt( ) { 
// Pre: true 
 _longitud = 0; 
// Post: construye un valor que representa al conjunto vacío 
} 
 
bool TCjtoInt::esVacio ( ) { 
// P: true 
 return _longitud == 0; 
// Q: determina si el conjunto está vacío 
} 
 
 
Tipos abstractos de datos 47 
 
void TCjtoInt::Pon( int x ) { 
// Pre: ! esLleno( ) || pertenece(x) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, _espacio, _longitud, encontrado, pos); 
 if ( ! encontrado ) { 
 if ( esLleno( ) ) 
 error("No se puede insertar el elemento"); 
 else { 
 for ( int k = _longitud; k > pos+1; k-- ) 
 _espacio[k] = _espacio[k-1]; 
 _espacio[pos+1] = x; 
 _longitud++; 
 } 
 } 
// Post: modifica el conjunto, añadiéndole x 
} 
 
... 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES DE ESCRITURA 
// 
 
... 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILARES DE LA ESPECIFICACION 
// 
 
// 
// Limitación impuesta por la implementación (esLleno) 
// 
bool TCjtoInt::esLleno ( ) { 
// Pre: true 
 return _longitud == longMax; 
// Post: determina si el conjunto tiene 'longMax' elementos 
} 
 
//------------------------------------------------------------
------------- 
 
 
 
Tipos abstractos de datos 48 
 
Implementación de TADs genéricos 
 
 En C++ las clase genéricas se implementan mediante plantillas (templates). 
Una plantilla es una clase con una o más variables de tipo en términos de las cua-
les se realiza la implementación. Luego se declaran ejemplares concretos de la 
plantilla instanciando las variables con tipos concretos. 
 
 Podemos definir tanto clases como funciones plantilla. 
— Una plantilla se declara precediendo la declaración con 
template<class VarTipo1, ... , class VarTipoN> 
donde VarTipo1, ... , VarTipoN identifican variables de tipo cuyo ámbito coin-
cide con el de la declaración de la clase o la función plantilla. 
— En un uso particular de un plantilla se ha de proporcionar un tipo concreto 
para cada parámetro de tipo. Se pueden utilizar tipos primitivos o declara-
dos por el programador. 
— El compilador comprueba que el uso de la plantilla es correcto y genera la 
instancia solicitada, i.e., es como si generase una copia del código de la 
plantilla donde se han sustituido las apariciones de las variables de tipo por 
los tipos concretos. 
— No supone ineficiencias en tiempo de ejecución. 
— En las plantillas tanto la definición de la clase como la implementación de 
las operaciones deben estar en el mismo archivo de cabecera. 
 
 Primer ejemplo: la clase de las parejas de valores. 
Declaración de la clase: 
template <class T1, class T2> 
class TPareja { 
 public: 
 TPareja( T1 v1, T2 v2 ); 
 T1 v1( ); 
 T2 v2( ); 
 void ponV1( T1 v1 ); 
 void ponV2( T2 v2 ); 
 bool operator<=( TPareja<T1,T2>& par ); 
 private: 
 T1 _v1; 
 T2 _v2; 
}; 
Tipos abstractos de datos 49 
 
Implementación de las operaciones: 
template <class T1, class T2> 
TPareja<T1,T2>::TPareja( T1 v1, T2 v2 ) : 
 _v1(v1), _v2(v2) {}; 
 
template <class T1, class T2> 
T1 TPareja<T1,T2>::v1( ) { return _v1; }; 
 
template <class T1, class T2> 
T2 TPareja<T1,T2>::v2( ) { return _v2; }; 
 
template <class T1, class T2> 
void TPareja<T1,T2>::ponV1( T1 v1 ) { _v1 = v1; }; 
 
template <class T1, class T2> 
void TPareja<T1,T2>::ponV2( T2 v2 ) { _v2 = v2; }; 
 
template <class T1, class T2> 
bool TPareja<T1,T2>::operator<=( TPareja<T1,T2>& par ) { 
 return (_v1 <= par._v1) && (_v2 <= par._v2); }; 
 
template <class T1, class T2> 
ostream& operator<<( ostream& salida, TPareja<T1,T2> par ) { 
 salida << "(" << par.v1() << ", " << par.v2() << ")"; 
 return salida; 
}; 
 
 Ejemplo de uso 
 TPareja<int,int> p1(1, 1), p2(1, 2); 
 TPareja<int,TPareja<int,int> > q1(1, p1), q2(1, p2); 
 // el espacio entre los dos > > es imprescindible 
 
 cout << p1 << p2 << q1 << q2 << endl; 
 cout << ((q1 <= q2) ? "menor" : "mayor"); 
 
 La implementación de una clase plantilla puede imponer restricciones sobre las 
variables de tipo relativas a las operaciones que ese tipo debe implementar. 
Por ejemplo, las parejas sólo se pueden instanciar con tipos que implementen 
el operador <=. Tipos que pertenecen a la clase de tipos ordenados. 
El compilador se encarga de comprobar que la instanciación es correcta, pero 
no hay ningún mecanismo para indicar explícitamente estas restricciones, por 
lo que es aconsejable incluirlo como comentario. 
Tipos abstractos de datos 50 
 
Implementación genérica de los conjuntos 
 
/* 
 Implementación de los conjuntos en vectores ordenados sin 
repetición. 
 Tamaño de los datos n = número de elementos almacenados en 
el conjunto. 
 Como máximo se pueden almacenar longMax elementos 
*/ 
 
#include <iostream> 
#include <string> 
using namespace std; 
 
template<class TElem> 
class TCjto { 
 public: 
 
 // Tamaño máximo 
 static const int longMax = 100; 
 
 // Constructora 
 TCjto( ); 
 // Pre: true 
 // Post: construye un valor que representa al conjunto 
vacío 
 // O(1) 
 
 // Operaciones de los conjuntos 
 bool esVacio ( ); 
 // Pre: true 
 // Post: determina si el conjunto está vacío 
 // O(1) 
 
 void Pon( TElem x ); 
 // Pre: ! esLleno( ) || pertenece(x) 
 // Post: modifica el conjunto, añadiéndole x 
 // O(n) 
 
 void quita( TElem x ); 
 // Pre: true 
 // Post: modifica el conjunto, quitando x 
 // O(n) 
 
 bool pertenece( TElem x ); 
 // Pre: true 
 // Post: determina si x está en el conjunto 
 // O(log n) 
 
Tipos abstractos de datos 51 
 
bool esLleno ( ); 
 // Pre: true 
 // Post: determina si el conjunto
tiene 'longMax' 
 //elementos O(1) 
 
 // Escritura 
 void escribe( ostream& salida ); 
 // Pre: true 
 // Post: escribe en 'salida' el contenido del conjunto 
 
 private: 
 // Variables privadas 
 TElem _espacio[longMax]; 
 int _longitud; 
}; 
 
template <class TElem> 
ostream& operator<< ( ostream& salida, TCjto<TElem> cjto ); 
 
 
Tipos abstractos de datos 52 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILIARES PRIVADAS 
// 
 
// 
// Búsqueda en el array 
// 
template<class TElem> 
int buscaBin( TElem v[], TElem x, int a, int b ) { 
// Pre: v está ordenado entre 0 .. num-1 
// ( 0 <= a <= num ) && ( -1 <= b <= num ) && ( a <= b+1 
) 
// todos los elementos a la izquierda de 'a' son <= x 
// todos los elementos a la derecha de 'b' son > x 
 
 int p, m; 
 
 if ( a == b+1 ) 
 p = a - 1; 
 else if ( a <= b ) { 
 m = (a+b) / 2; 
 if ( v[m] <= x ) 
 p = buscaBin( v, x, m+1, b ); 
 else 
 p = buscaBin( v, x, a, m-1 ); 
 } 
 return p; 
// Post: devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
} 
 
template<class TElem> 
void buscaBin( TElem x, TElem v[], int num, bool & encontrado, 
int & pos ) { 
// Pre: los num primeros elementos de v están ordenados 
// num >= 0 
 
 pos = buscaBin(v, x, 0, num-1); 
 encontrado = ( pos >= 0 ) && ( pos < num ) && ( v[pos] == x 
); 
 
// Post : devuelve el mayor índice i (0 <= i <= num-1) que 
cumple 
// v[i] <= x 
// si x es menor que todos los elementos de v, devuelve 
-1 
// encontrado es true si x esta en v[0..num-1] 
Tipos abstractos de datos 53 
 
} 
// 
// función auxiliar privada para mostrar un mensaje de error 
// 
void error( string mensaje ) { 
 cout << "Error: " + mensaje; 
} 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES PUBLICAS 
// 
 
template<class TElem> 
TCjto<TElem>::TCjto( ) { 
// Pre: true 
 _longitud = 0; 
// Post: construye un valor que representa al conjunto vacío 
} 
 
template<class TElem> 
bool TCjto<TElem>::esVacio ( ) { 
// P: true 
 return _longitud == 0; 
// Q: determina si el conjunto está vacío 
} 
 
template<class TElem> 
void TCjto<TElem>::Pon( TElem x ) { 
// Pre: ! esLleno( ) || pertenece(x) 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, _espacio, _longitud, encontrado, pos); 
 if ( ! encontrado ) { 
 if ( esLleno( ) ) 
 error("No se puede insertar el elemento"); 
 else { 
 for ( int k = _longitud; k > pos+1; k-- ) 
 _espacio[k] = _espacio[k-1]; 
 _espacio[pos+1] = x; 
 _longitud++; 
 } 
 } 
// Post: modifica el conjunto, añadiéndole x 
} 
 
Tipos abstractos de datos 54 
 
template<class TElem> 
void TCjto<TElem>::quita( TElem x ) { 
// Pre: true 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, _espacio, _longitud, encontrado, pos); 
 if ( encontrado ) { 
 for ( int k = pos; k < _longitud-1; k++ ) 
 _espacio[k] = _espacio[k+1]; 
 _longitud--; 
 } 
// Post: modifica el conjunto, quitando x 
} 
 
template<class TElem> 
bool TCjto<TElem>::pertenece( TElem x ) { 
// Pre: true 
 bool encontrado; 
 int pos; 
 
 buscaBin( x, _espacio, _longitud, encontrado, pos); 
 return encontrado; 
// Post: determina si x está en el conjunto 
} 
 
 
Tipos abstractos de datos 55 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES DE ESCRITURA 
// 
 
template<class TElem> 
void TCjto<TElem>::escribe( ostream& salida ) { 
// Pre: true 
 salida << "("; 
 for ( int i = 0; i < _longitud; i++ ) { 
 salida << _espacio[i]; 
 if ( i < _longitud-1 ) 
 salida << ", "; 
 } 
 cout << ")" << endl; 
// Post: muestra por pantalla el contenido del conjunto 
} 
 
template <class TElem> 
ostream& operator<< ( ostream& salida, TCjto<TElem> cjto ) { 
 cjto.escribe(salida); 
 return salida; 
} 
 
 
//////////////////////////////////////////////////////////////
///////////// 
// 
// 
// OPERACIONES AUXILARES DE LA ESPECIFICACION 
// 
 
// 
// Limitación impuesta por la implementación (esLleno) 
// 
template<class TElem> 
bool TCjto<TElem>::esLleno ( ) { 
// Pre: true 
 return _longitud == longMax; 
// Post: determina si el conjunto tiene 'longMax' elementos 
} 
Tipos abstractos de datos 56 
 
Prueba de los módulos 
 Una de las ventajas de la implementación modular es que resulta más sencillo 
probar cada módulo por separado. 
 
 
 
 Para llevar a cabo esta prueba, es necesario que el TAD exporte una operación 
que genere una cadena con los elementos separados por saltos de línea, a partir 
de la cual se genera el contenido del cuadro de lista. 
template<class TElem> 
class TCjto { 
 public: 
 ... 
 void escribeLn( ostream& salida ); 
 // Pre: true 
 // Post: escribe en 'salida' el contenido del conjunto 
 // incluyendo saltos de línea entre cada dos 
datos 
 ... 
}; 
... 
template<class TElem> 
void TCjto<TElem>::escribeLn( ostream& salida ) { 
// Pre: true 
 for ( int i = 0; i < _longitud; i++ ) 
 salida << _espacio[i] << endl; 
// Post: muestra por pantalla el contenido del conjunto 
// incluyendo saltos de línea entre cada dos datos 
} 
 La clase que implementa el formulario 
La definición de la clase que genera automáticamente el entorno. 
Tipos abstractos de datos 57 
 
 
class TForm1 : public TForm 
{ 
__published: // IDE-managed Components 
 TGroupBox *GroupBox1; 
 TListBox *List; 
 TButton *Pon; 
 TEdit *Ponx; 
 TButton *Quita; 
 TButton *EsLleno; 
 TButton *Pertenece; 
 TEdit *Quitax; 
 TEdit *Pertenecex; 
 TButton *esVacio; 
 void __fastcall PonClick(TObject *Sender); 
 void __fastcall QuitaClick(TObject *Sender); 
 void __fastcall PerteneceClick(TObject *Sender); 
 void __fastcall esVacioClick(TObject *Sender); 
 void __fastcall EsLlenoClick(TObject *Sender); 
private: // User declarations 
public: // User declarations 
 __fastcall TForm1(TComponent* Owner); 
}; 
 
 
La implementación de la clase 
 
#include <iostream> 
#include <sstream> //STR 640 
#include <string> 
#include <list> 
#include "cjto.h" 
 
using namespace std; 
 
typedef TCjto<int> TCjtoInt; 
 
TCjtoInt c1; 
 
TForm1 *Form1; 
 
 
Tipos abstractos de datos 58 
 
Función que actualiza el contenido del cuadro de lista 
 
//----------------------------------------------------------
---------- 
void actualizaContenido( TCjtoInt cjto, TStrings* lista ) 
{ 
 ostringstream aux; 
 int n, inicio, fin; 
 string contenido; 
 string separadores = "\n"; 
 
 cjto.escribeLn(aux); 
 contenido = aux.str(); 
 n = contenido.length(); 
 inicio = contenido.find_first_not_of(separadores); 
 lista->Clear(); 
 
 while ((inicio >= 0) && (inicio < n)) { 
 fin = contenido.find_first_of(separadores, inicio); 
 if ((fin < 0) || (fin > n)) fin = n; 
 lista->Add((contenido.substr(inicio, fin - 
inicio)).data()); 
 inicio = contenido.find_first_not_of(separadores, 
fin+1); 
 } 
} 
 
Creación del formulario 
//----------------------------------------------------------
---------- 
__fastcall TForm1::TForm1(TComponent* Owner) 
 : TForm(Owner) 
{ 
} 
 
Funciones asociadas con la pulsación de los botones 
//----------------------------------------------------------
---------- 
void __fastcall TForm1::PonClick(TObject *Sender)
{ 
 c1.Pon(StrToInt(Ponx->Text)); 
 actualizaContenido(c1, List->Items); 
} 
//----------------------------------------------------------
---------- 
void __fastcall TForm1::QuitaClick(TObject *Sender) 
{ 
 c1.quita(StrToInt(Quitax->Text)); 
 actualizaContenido(c1, List->Items); 
} 
Tipos abstractos de datos 59 
 
//----------------------------------------------------------
---------- 
void __fastcall TForm1::PerteneceClick(TObject *Sender) 
{ 
 if( c1.pertenece(StrToInt(Pertenecex->Text)) ) 
 ShowMessage("Sí está"); 
 else 
 ShowMessage("No está"); 
} 
//----------------------------------------------------------
---------- 
void __fastcall TForm1::esVacioClick(TObject *Sender) 
{ 
 if( c1.esVacio( ) ) 
 ShowMessage("Está vacío"); 
 else 
 ShowMessage("No está vacío"); 
} 
//----------------------------------------------------------
---------- 
void __fastcall TForm1::EsLlenoClick(TObject *Sender) 
{ 
 if( c1.esLleno( ) ) 
 ShowMessage("Está lleno"); 
 else 
 ShowMessage("No está lleno"); 
} 
//----------------------------------------------------------
---------- 
 
Tipos abstractos de datos 60 
 
Implementación de TADs con operaciones parciales: 
manejo de excepciones 
 
 La mayoría de los lenguajes de programación modernos incluyen algún meca-
nismo para facilitar el tratamiento de los errores y las situaciones no previstas 
en el código: excepciones 
 
 En C++ una excepción se representa como un objeto de una cierta clase. 
— Para definir un cierto tipo de excepción, se declara un nuevo tipo de clase. 
— Cuando el código cliente es susceptible de generar una excepción, se utiliza 
la sentencia try ... catch para capturarla y tratarla. 
— Para lanzar una excepción se utiliza la sentencia throw que recibe como pa-
rámetro un objeto del tipo de excepción que se quiere lanzar. 
throw objeto; 
 
 La sintaxis de la sentencia try ... catch 
try { 
 lista_sentencias 
} 
catch( identificador_clase1 ) { 
 lista_sentencias_excepcion1 
} 
... 
catch( identificador_claseN ) { 
 lista_sentencias_excepcionN 
} 
catch( ... ) { // captura cualquier excepción 
 lista_sentencias_otras_excepciones 
} 
 
Se ejecuta la lista de sentencias que siguen a try 
— Si las sentencias se ejecutan sin que se lancen excepciones, se ignoran los 
bloques de tratamiento de excepciones. 
— Cuando se genera una excepción, se transfiere el control al manejador de ex-
cepciones más interno (puede haber sentencias try ... catch anidadas) que puede 
manejar las excepciones de la clase dada. 
La propagación de la excepción continua hasta que se encuentra el maneja-
dor adecuado o hasta que no hay más sentencias try ... catch activas, en cuyo 
caso se produce un error en tiempo de ejecución y se finaliza la aplicación. 
Tipos abstractos de datos 61 
 
 Para determinar si el bloque de excepciones puede manejar una excepción, se exa-
minan los manejadores de excepción en orden de aparición. 
— Un manejador puede tratar una excepción si la clase de ésta coincide con el 
correspondiente identificador de clase o es una especialización suya. 
— Un manejador de la forma catch ( ... ) captura cualquier excepción. 
 
 En una cláusula catch se puede declarar una variable local cuyo ámbito es el 
bloque de código asociado. 
catch( identificador_clase identificador_variable ) 
Esta variable se inicializa con el objeto excepción. 
 
 En el bloque asociado con una cláusula catch se puede volver a lanzar el mismo 
objeto sin más que realizar una nueva invocación de throw; (sin parámetros). 
 
 En la cabecera de una función se puede indicar explícitamente qué excepcio-
nes puede generar esa función, de forma que si genera cualquier otra acabe la 
ejecución del programa. 
 
Tipos abstractos de datos 62 
 
Uso de excepciones en la implementación de los TADs 
 Definimos dos tipos de excepciones comunes a las implementaciones de los 
TADs: 
— Inserciones en estructuras que están llenas: EDesbordamiento 
— Accesos ilegales a los valores del TAD: EAccesoIndebido 
 
class EDesbordamiento { 
 public: 
 EDesbordamiento( string mensaje = "" ) : 
_mensaje(mensaje) { }; 
 string mensaje ( ) { 
 return _mensaje; 
 }; 
 private: 
 string _mensaje; 
}; 
 
class EAccesoIndebido { 
 public: 
 EAccesoIndebido( string mensaje = "" ) : 
_mensaje(mensaje) { }; 
 string mensaje ( ) { 
 return _mensaje; 
 }; 
 private: 
 string _mensaje; 
}; 
 
Aunque podemos definir estas clases en los archivos donde implementemos 
las clases que las pueden lanzar, es mejor idea incluirlas en un archivo aparte 
(excepciones.h) de forma que no se produzcan redefiniciones cuando usemos va-
rios TADs en una misma aplicación. 
 
 En las implementaciones de los TAD con operaciones parciales, ya sea por si-
tuaciones indicadas en la especificación o por limitaciones impuestas por la 
propia implementación, elevamos excepciones cuando se viola la precondi-
ción. En algunos casos puede ser conveniente definir un nuevo tipo de excep-
ción, propio del TAD en cuestión. 
 
Tipos abstractos de datos 63 
 
 Por ejemplo, en la implementación genérica de las pilas, utilizando arrays. 
Anotamos las excepciones y las añadimos a los comentarios, en la definición 
de la clase. 
template<class TElem> 
class TPila { 
 public: 
 ... 
 void apila( TElem x ) throw (EDesbordamiento); 
 // Pre: ! esLleno( ) 
 // Post: modifica la pila, añadiéndole x en la cima 
 // Lanza la excepción EDesbordamiento si la pila está 
llena 
 // O(1) 
 
 void desapila( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio() 
 // Post: modifica la pila, quitando el elemento de la 
cima 
 // Lanza la excepción EAccesoIndebido si la pila está 
vacía 
 // O(1) 
 
 TElem cima( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio() 
 // Post: devuelve el elemento que está en la cima de la 
pila 
 // Lanza la excepción EAccesoIndebido si la pila está 
vacía 
 // O(1) 
 ... 
}; 
 
 Y en las implementaciones de las operaciones parciales lanzamos una excep-
ción cuando no se cumple la precondición. 
 
template<class TElem> 
void TPila<TElem>::apila( TElem x ) throw (EDesbordamiento){ 
// Pre: ! esLleno( ) 
 if ( esLleno() ) 
 throw EDesbordamiento("Pila llena"); 
 else { 
 _indCima++; 
 _espacio[_indCima] = x; 
 } 
// Post: modifica la pila, añadiéndole x en la cima 
} 
 
Tipos abstractos de datos 64 
 
template<class TElem> 
void TPila<TElem>::desapila( ) throw (EAccesoIndebido){ 
// Pre: ! esVacio() 
 if ( esVacio() ) 
 throw EAccesoIndebido("No se puede desapilar de la pila 
vacía"); 
 else 
 _indCima--; 
// Post: modifica la pila, quitando el elemento que está en 
la cima 
} 
 
template<class TElem> 
TElem TPila<TElem>::cima( ) throw (EAccesoIndebido){ 
// Pre: ! esVacio() 
 if ( esVacio() ) 
 throw EAccesoIndebido("No existe la cima de la pila 
vacía"); 
 else 
 return _espacio[_indCima]; 
// Post: devuelve el elemento que está en la cima de la pila 
} 
 
 
 En el uso de estas operaciones parciales, se deben capturar las excepciones que 
se puedan producir. 
 
 TPila<char> pila; 
 
 try { 
 pila.apila('a'); 
 pila.desapila(); 
 pila.desapila(); 
 pila.apila('b'); 
 } 
 catch( EDesbordamiento e ) { 
 cout << e.mensaje(); 
 } 
 catch ( EAccesoIndebido e ) { 
 cout << e.mensaje(); 
 } 
 catch ( ... ) { 
 cout << "otro tipo de excepción"; 
 } 
 
Tipos abstractos de datos 65 
 
3.5 Estructuras de datos dinámicas 
 Una estructura de datos se llama estática cuando el espacio que va a ocupar 
está determinado en tiempo de compilación. 
Puede que ese espacio se ubique al principio de la ejecución –variables globa-
les– o en la invocación a procedimientos o funciones –variables locales–, pero, 
en cualquier caso, no es responsabilidad del programador ocuparse de su ges-
tión. 
 
 Un estructura
de datos se llama dinámica cuando el espacio que ocupa en me-
moria puede variar durante la ejecución del programa, y no está determinado 
en tiempo de compilación. 
 
 Una de las razones para utilizar estructuras de datos dinámicas es la represen-
tación de colecciones de valores. 
— La estructura de datos estática apta para representar colecciones de datos es 
el array. Sin embargo, se presentan dos problemas por el hecho de tener 
que reservar el espacio a priori: 
— Se desaprovecha memoria si el espacio reservado resulta ser excesivo. 
— Se pueden producir errores de desbordamiento en tiempo de ejecución 
si el espacio reservado resulta ser insuficiente. 
— Las estructuras de datos dinámicas, por su parte, permiten representar co-
lecciones de datos, de forma que 
— no se malgasta espacio a priori, y 
— el único límite en tiempo de ejecución es la cantidad total de memoria 
disponible. 
 
 Los punteros son el mecanismo que permite construir y manejar estructuras de 
datos dinámicas. 
 
Tipos abstractos de datos 66 
 
3.5.1 Punteros 
 
 Una variable de un cierto tipo lleva asociado: un nombre, un espacio de me-
moria y un valor del tipo declarado para la variable. 
Una variable de tipo puntero tiene igualmente asignado un identificador, un 
espacio de memoria y su valor es la dirección de otra variable, tal que 
— El nombre de la variable apuntada por un puntero se puede obtener a par-
tir del identificador del puntero mediante el operador de indirección. En C++ 
el operador de indirección (o dereferencia) se escribe *. 
— El espacio de memoria de la variable a la que apunta un puntero se puede 
ubicar dinámicamente durante la ejecución. 
— Llamamos variable dinámica a una variable creada de esta forma. 
— Una variable dinámica no existe hasta que es creada explícitamente en 
tiempo de ejecución. 
— Es posible “anular” una variable apuntada por un puntero, liberando así el 
espacio que ocupa. 
Gráficamente: 
p *p
3 
 
 Como estructura para representar colecciones de datos, la propiedad funda-
mental de los punteros es que permiten obtener y devolver memoria dinámi-
camente durante la ejecución, de forma que sólo solicitaremos el espacio 
imprescindible para los datos que en cada momento necesitemos representar. 
Uso de punteros 
 Para cualquier tipo τ admitimos que puede formarse un nuevo tipo de punte-
ros a variables de tipo τ, declarado como 
τ* 
— Independientemente del tipo de variable al que apunte, siempre se reserva 
la misma cantidad de espacio para un puntero: la necesaria para representar 
una dirección de memoria. 
— El espacio reservado para la variable dinámica es el necesario para almace-
nar un valor de tipo τ. 
Tipos abstractos de datos 67 
 
 Las dos operaciones básicas con los punteros son ubicar la variable a la que 
apuntan y anular dicha variable, liberando el espacio que ocupa. 
En C++ la ubicación y la liberación de punteros se realiza mediante los opera-
dores new y delete. 
— La ubicación de un puntero se hace con el operador new 
variable_puntero = new nombre_tipo 
El efecto de new es: 
— Crear una variable del tipo nombre_tipo. 
— Devolver la dirección del espacio de memoria asignado a dicha variable. 
Ejemplo: 
int* p; 
 
p = new int; 
 
— La liberación de una variable apuntada por un puntero se hace con el ope-
rador delete 
delete variable_puntero 
El efecto de delete es: 
— destruir la variable apuntada por variable_puntero, liberando el espacio de 
memoria que ocupaba 
Nótese que 
— no se debe liberar una variable que no esté ubicada, y 
— no se debe utilizar la variable *p después de ejecutar delete p 
Ejemplo: 
 int *p, *q; // si se declara más de un puntero en la 
misma 
 // declaración, hay que escribir * delante 
de cada uno 
 
 p = new int; 
 *p = 2; 
 delete p; 
 *p = 3; 
 q = new int; 
 *q = 4; 
 cout << *p << endl; 
— ¿qué se muestra por pantalla? 
— ¿qué sentencias son erróneas? 
Tipos abstractos de datos 68 
 
 Otras operaciones que podemos realizar con los punteros 
— Asignaciones entre punteros del mismo tipo 
p = q 
Con el siguiente efecto 
— p pasa a apuntar al mismo sitio al que esté apuntado q. 
— Si antes de la asignación p apuntaba a una variable, después de la asigna-
ción *p ya no es un identificador válido para dicha variable. 
No debemos perder el puntero a una variable dinámica sin liberar pre-
viamente el espacio que ésta ocupa, a menos que la variable sea accesi-
ble desde otro puntero. 
— Si *q no está ubicada entonces *p tampoco lo está. 
— *p y *q son dos identificadores de la misma variable. 
 
— Comparaciones entre punteros del mismo tipo 
p == q p != q 
Nótese que comparamos direcciones y no los valores de las variables a las 
que apuntan los punteros, *p y *q. 
 
 Como conclusión de este apartado, vamos a seguir gráficamente con detalle un 
ejemplo del uso de las operaciones sobre punteros. 
 
 int *p, *q; 
 
 
q
#
p
#
 
 
 p = new int; 
 
p *p
#
q
#
 
 
 *p = 3; 
 *p = 2 * *p; 
 
p *p
6
q
#
 
 q = new int; 
 
Tipos abstractos de datos 69 
 
p q
#
*p
6
*q
 
 
 *q = 3; 
 *q = *p + *q; 
 
p q
9
*p
6
*q
 
 
 q = p; 
 
 
*q
p q
9
*p
6
 
 
 *q = 2; 
 
*q
p q
9
*p
2
 
 
 delete p; 
 
p q
9#
 
 
 
 
 
 
— Dos punteros comparten estructura si permiten acceder a las mismas variables 
dinámicas. 
— Una variable ubicada dinámicamente es basura si no es posible acceder a 
ella mediante ningún puntero (estático). 
— Un puntero almacena una referencia perdida si apunta a una zona de la memo-
ria dinámica que ha sido marcada como libre. 
basura 
referencia perdida 
estructura compartida 
efectos colaterales 
Tipos abstractos de datos 70 
 
3.5.2 Construcción, destrucción, copia y asignación de 
estructuras dinámicas 
 
 Cuando se utilizan punteros en los programas hay que ser especialmente cui-
dadoso por dos razones: 
— el programador es el responsable de ubicar y anular las variables dinámicas, 
— la asignación entre punteros produce compartición de estructura que puede 
provocar efectos colaterales y, unida a la anulación, generar basura o refe-
rencias perdidas. 
 
 ¿Por qué no ocurre esto con las variables estáticas? 
Porque: 
— es el compilador quien genera el código encargado de ubicar y anular au-
tomáticamente las variables estáticas (también llamadas automáticas por ra-
zones obvias), y 
— la asignación no da lugar a compartición de estructura. 
void suma ( int x, int y, int & z ) { // 4 
 int r ; // 5 
 
 r = x + y; // 6 
 z = r; // 7 
} 
void main() { // 1 
 int a = 2, b, c; // 2 
 
 b = 3; // 3 
 suma( a, b, c ); // 8 
} // 9 
 
2
#
#
a
b
c
2
3
#
a
b
c
2
3
#
2
3
a
b
z,c
x
y
z,c
2
3
#
2
3
r #
a
b
x
y
z,c
r 5
2
3
#
2
3
a
b
x
y
z,c
r 5
2
3
5
2
3
a
b
x
y
2
3
5
a
b
c
(1) (2) (3) (4) (5) (6) (7) (8) (9) 
Tipos abstractos de datos 71 
 
 Cuando manejamos punteros la cosa se complica, sobre todo cuando maneja-
mos punteros a estructuras que contienen a su vez punteros a otras estructu-
ras. Afortunadamente, las clases de C++ incluyen los mecanismos necesarios 
para controlar los procesos de creación, destrucción copia y asignación. 
— Siempre que se crea un objeto, se ejecuta una de sus constructoras. Un ob-
jeto puede, al igual que cualquier otro valor, ser: 
— estático, si se declara explícitamente, o 
— dinámico si se crea mediante una invocación a new. 
Si en una clase no definimos ninguna constructora, entonces el compilador 
genera una que realiza inicializaciones por defecto. 
Si se define alguna constructora, entonces el compilador no genera ningu-
na, lo que quiere decir que si la constructora definida requiere parámetros, 
entonces no será posible construir ningún objeto sin suministrar
esos pa-
rámetros. 
A una constructora sin parámetros se le denomina constructora por defecto y, 
en general, es conveniente que todas las clases tengan una. Si una clase no 
tiene constructora por defecto, entonces no es posible construir un array 
de elementos de ese tipo. 
— Siempre que se destruye un objeto, se invoca a su destructora. Un objeto, 
igual que cualquier otro valor, se destruye cuando sale de ámbito (si es está-
tico) o cuando se invoca el operador delete sobre un puntero que lo apunta. 
Si en una clase no definimos destructora, entonces el compilador genera 
una que se limita a devolver el espacio ocupado por los datos estáticos del 
objeto. 
— Si un objeto se construye a partir de otro, entonces es la constructora de copia 
quien se encarga de la construcción. 
La constructora de copia se invoca cuando: 
— Se declara un objeto inicializado con el valor de otro. 
— Se construye un parámetro por valor. Los parámetros por valor son co-
pias de los parámetros reales. 
— Se devuelve el valor de una función, siempre que ese valor no sea de ti-
po referencia. 
— Es posible redefinir el operador de asignación para objetos de un tipo de-
terminado. 
 
Tipos abstractos de datos 72 
 
 Vamos a desarrollar un ejemplo que maneja estructuras dinámicas complejas. 
En primer lugar definimos la clase TComplejo 
 
class Complejo { 
 private: 
 double _real; 
 double _img; 
 public: 
 Complejo( ) : _real(0), _img(0) { }; 
 Complejo( double r, double i = 0 ) : _real(r), _img(i) 
{ }; 
 double real() { return _real; } 
 double img() { return _img; } 
}; 
 
void main( ) 
{ 
 Complejo z1, z2(1), z3(1,1); 
 Complejo *pz1 = new Complejo, *pz2 = new Complejo(1); 
 delete pz1; 
 delete pz2; 
} 
 
Esta clase no necesita destructora ni constructora de copia porque todos sus 
atributos son estáticos. 
 
Definimos ahora una clase que almacena punteros a objetos de tipo Complejo 
 
class PuntoComplejo { 
 private: 
 Complejo *_x, *_y; 
 public: 
 PuntoComplejo( double r = 0, double i = 0 ) { 
 _x = new Complejo(r, i); 
 _y = new Complejo(r, i); }; 
 Complejo x() { return *_x; } 
 Complejo y() { return *_y; } 
 ~PuntoComplejo( ) { 
 delete _x; 
 delete _y; }; 
}; 
 
En general, la destructora ha de liberar, al menos, lo que la constructora ubicó. 
 ¿Qué ocurre al ejecutar el siguiente programa? 
 
Tipos abstractos de datos 73 
 
void trampa( PuntoComplejo& a ) 
{ 
 PuntoComplejo b(1,1); 
 
 a = b; 
} 
 
void main( ) 
{ 
 PuntoComplejo p; 
 
 trampa( p ); 
 
 cout << p.x().real() << "+" << p.x().img() << "i\n"; 
 cout << p.y().real() << "+" << p.y().img() << "i\n"; 
} 
 
La memoria evoluciona de la siguiente manera: 
— Construcción del objeto referenciado por p 
PuntoComplejo p; 
— obtiene automáticamente espacio para p y ejecuta su constructora 
— la constructora de PuntoComplejo solicita explícitamente espacio para 
_x e _y, para cada uno de los cuales se ejecuta la constructora de 
Complejo 
 
 
p
_x
_y
*(p._x)
_real
0
_img
0 *(p._y)
_real
0
_img
0
 
— Invocación a la función trampa 
void trampa( PuntoComplejo& a ) 
en este ámbito, a es un alias de p y p no es visible 
 
Tipos abstractos de datos 74 
 
 
*(a._y)
a
p
_x
_y
*(a._x)
*(p._x)
_real
0
_img
0 *(p._y)
_real
0
_img
0
 
 
— Construcción del objeto referenciado por b 
PuntoComplejo b(1,1); 
— obtiene automáticamente espacio para b y ejecuta su constructora 
— la constructora de PuntoComplejo solicita explícitamente espacio para 
_x e _y, para cada uno de los cuales se ejecuta la constructora de 
Complejo 
b
_x
_y
*(b._x)
_real
1
_img
1 *(b._y)
_real
1
_img
1
 
*(a._y)
a
p
_x
_y
*(a._x)
*(p._x)
_real
0
_img
0 *(p._y)
_real
0
_img
0
 
— Asignación a = b; 
— se realizan las asignaciones 
 a._x = b._x; a._y = b._y; 
 
Tipos abstractos de datos 75 
 
*(a._y)
*(p._y)
*(a._x)
*(p._x)
b
_x
_y
*(b._x)
_real
1
_img
1 *(b._y)
_real
1
_img
1
a
p
_x
_y
_real
0
_img
0
_real
0
_img
0
 
— Termina la ejecución de trampa 
— se destruye automáticamente el espacio ocupado por b, lo que da lugar a 
que se ejecute la destructora ~PuntoComplejo, quien libera explícitamente 
el espacio ocupado por b._x y b._y 
— b y a salen de ámbito y dejan de existir 
— Se ha generado basura y referencias perdidas 
 
p
_x
_y
_real
0
_img
0
_real
0
_img
0
¿ ?
¿ ?
 
 El problema que se presenta en el ejemplo anterior es que estamos realizando 
una asignación entre objetos que contienen variables de tipo puntero. 
— El comportamiento por defecto de la asignación entre objetos consiste en 
asignar sucesivamente cada uno de sus datos miembro. De esta forma, es-
Tipos abstractos de datos 76 
 
tamos realizando una asignación entre punteros, con la consiguiente com-
partición de estructura. 
— Para evitar este problema, en C++ es posible redefinir el operador de asig-
nación para asignaciones entre objetos de una clase determinada. De esta 
forma, será este método el que se ejecute, en lugar del comportamiento por 
defecto de la asignación. 
 
 La implementación habitual para las redefiniciones del operador de asignación 
–salvo optimizaciones– 
— comprueba que el origen y el destino de la asignación son diferentes, 
— destruye el valor original del objeto, 
— copia el nuevo valor, 
— y devuelve un puntero al propio objeto 
 
 En el ejemplo, añadimos el nuevo método a la declaración de la clase 
class PuntoComplejo { 
 ... 
 PuntoComplejo& operator=(const PuntoComplejo& p); 
 ... 
}; 
Y lo implementamos 
PuntoComplejo& PuntoComplejo::operator=(const 
PuntoComplejo& p) 
{ 
 if (this != &p) { 
 delete _x; 
 delete _y; 
 _x = new Complejo(p.x().real(), p.x().img()); 
 _y = new Complejo(p.y().real(), p.y().img()); 
 } 
 return *this; 
} 
donde, 
— la pseudovariable this contiene un puntero al objeto receptor el mensaje, y 
— el operador & devuelve la dirección en la que está ubicada una variable. 
 Equipados con nuestro flamante operador de asignación, ¿qué ocurrirá en este 
ejemplo? 
 
void trampa2( PuntoComplejo a ) { 
 cout << a.x().real() << "+" << a.x().img() << "i\n"; 
 cout << a.y().real() << "+" << a.y().img() << "i\n"; 
} 
Tipos abstractos de datos 77 
 
 
void main( ) { 
 PuntoComplejo p, q = p; 
 
 trampa2( p ); 
 
 cout << p.x().real() << "+" << p.x().img() << "i\n"; 
 cout << p.y().real() << "+" << p.y().img() << "i\n"; 
 cout << q.x().real() << "+" << q.x().img() << "i\n"; 
 cout << q.y().real() << "+" << q.y().img() << "i\n"; 
} 
 
La memoria evoluciona de la siguiente manera: 
— Construcción de los objetos referenciados por p y q 
PuntoComplejo p, q = p; 
Mientras que p se construye con la constructora por defecto, en la cons-
trucción de q se obtiene automáticamente espacio para un objeto Punto-
Complejo cuyos datos se inicializan con el valor de los datos de p –
comportamiento de la constructora de copia por defecto– 
 
*(q._x)
*(q._y)
p
_x
_y
q
_x
_y
*(p._x)
_real
0
_img
0 *(p._y)
_real
0
_img
0
 
— Invocación de la función trampa2 
— Se crea automáticamente el objeto al que referencia el parámetro formal 
a, que es inicializado como una copia del parámetro real p mediante 
asignaciones consecutivas de sus datos miembro –comportamiento por 
de la constructora de copia por defecto–. 
— Los identificadores p y q están fuera de ámbito 
Tipos abstractos de datos 78 
 
*(a._x)
*(q._x)
p
_x
_y
q
_x
_y
*(p._x)
_real
0
_img
0 *(p._y)
_real
0
_img
0
*(a._y)
*(q._y)
a
_x
_y
 
— Termina la ejecución de trampa2 
— se destruye automáticamente el espacio ocupado por a, lo que da lugar a 
que se ejecute la destructora ~PuntoComplejo, quien libera explícitamente 
el espacio
ocupado por a._x y a._y 
— a sale de ámbito y deja de existir 
— Se han generado referencias perdidas 
p
_x
_y
q
_x
_y
¿ ?
¿ ?
 
 La solución radica en añadir a la clase una constructora de copia que será el méto-
do que se ejecute cuando se cree un objeto a partir de otro creado previamente 
class PuntoComplejo { 
 ... 
 PuntoComplejo(const PuntoComplejo& p); 
 ... 
}; 
 
Tipos abstractos de datos 79 
 
e implementarla de forma que se evite la compartición de estructura 
PuntoComplejo::PuntoComplejo(const PuntoComplejo& p) 
{ 
 _x = new Complejo(p.x().real(), p.x().img()); 
 _y = new Complejo(p.y().real(), p.y().img()); 
} 
Nótese que el parámetro se pasa como una referencia constante para evitar 
que se ejecute la constructora de copia al invocar a la constructora de copia. 
 
 Cabe preguntarse ¿por qué es tan complicado C++? 
La respuesta es que lo complicado no es –sólo– C++ sino la gestión de la 
memoria dinámica. Es esta complejidad la razón de que algunos lenguajes in-
corporen recolección automática de basura, un mecanismo que se encarga de liberar 
las zonas de la memoria dinámica que dejan de ser accesibles. De esta forma, el 
programador sólo se debe ocupar de ubicar las variables dinámicas, pero no así 
de liberarlas, con lo cual se evita el problema de las referencias perdidas (aun-
que no el problema de los efectos colaterales debidos a la compartición de es-
tructura que, de todas formas, puede resultar útil). El precio a pagar es el coste 
temporal de la recolección automática de basura. 
C++ ofrece mecanismos para tratar de forma elegante todos los problemas re-
lativos a la gestión de la memoria dinámica: 
— Todos los objetos se inicializan pues siempre se ejecuta una constructora 
en el momento de su creación. 
— Cuando un objeto deja de existir, se invoca automáticamente a su destruc-
tora, si la hay. 
— Es posible redefinir la asignación. 
— Es posible redefinir el mecanismo de copia por defecto que se utiliza el pa-
so de parámetros por valor y la inicialización de variables. 
En cualquier caso, es necesario haberse enfrentado a este tipo de problemas 
para apreciar las bondades de un recolector de basura. 
Tipos abstractos de datos 80 
 
3.5.3 Construcción de estructuras de datos dinámicas 
 
 La utilidad de los punteros para construir colecciones de datos se obtiene 
cuando un puntero p apunta a una variable de tipo registro que contiene en 
uno de sus campos un puntero del mismo tipo que p. 
Pueden usarse entonces los punteros para encadenar entre sí registros, for-
mando estructuras dinámicas, ya que new y delete podrán usarse en tiempo de 
ejecución para añadir o eliminar registros de la estructura. 
Por ejemplo: 
struct TNodo { 
 int info; 
 TNodo* sig; 
}; 
 
 Dado que en C++ el operador de acceso a los campos de un registro es más 
prioritario que el de indirección, debemos utilizar paréntesis cuando accede-
mos a un registro a través de un puntero: 
TNodo *p; 
... 
(*p).info = 2; 
 
Afortunadamente, existe una notación alternativa: 
p->info = 2; 
 
 Para poder construir estructuras dinámicas es necesario poder indicar de algu-
na forma el final de las estructuras. Esto se hace con un valor especial de tipo 
puntero: el puntero vacío que en C++ se representa con el valor 0. 
0 es una constante polimórfica, compatible con cualquier variable de tipo pun-
tero, que representa un puntero ficticio que no apunta a ninguna variable, de 
forma que 
— *0 está indefinido, y 
— new 0 y delete 0 no tienen sentido 
 
 Utilizando registros con punteros como el anterior y la constante 0, podemos 
construir estructuras dinámicas como esta 
p
1 2 3
 
Tipos abstractos de datos 81 
 
 Por ejemplo 
 TNodo *p, *q; 
 
 q = new TNodo; 
 q->info = 3; 
 q->sig = 0; 
 p = q; 
 
 
p
3
q
 
 
 q = new TNodo; 
 q->info = 2; 
 q->sig = p; 
 p = q; 
 
 
 
p
2 3
q
 
 
 q = new TNodo; 
 q->info = 1; 
 q->sig = p; 
 p = q; 
 
 
p
1 2 3
q
 
 
 
 
 
Tipos abstractos de datos 82 
 
3.5.4 Implementación de TADs mediante estructuras de datos 
dinámicas: implementación de las pilas 
Tipo representante 
 Representamos las pilas como una lista enlazada de nodos, donde la cima es el 
primer elemento de la lista. 
 
p1
1 2 3
 
 
En muchas ocasiones necesitaremos definir estructuras auxiliares para repre-
sentar a los nodos de una estructura dinámica. Para aprovecharnos de los meca-
nismos que C++ proporciona para las clases, en lugar de registros 
template <class TElem> 
struct TNodoPila { 
 TElem elem; 
 TNodoPila* sig; 
}; 
 
utilizaremos clases para los nodos de las estructuras dinámicas 
template <class TElem> 
class TNodoPila { 
 TElem _elem; 
 TNodoPila<TElem>* _sig; 
 ... 
} 
 
de forma que el tipo representante de las pilas utilice esta clase auxiliar 
template <class TElem> 
class TPilaDinamica { 
 ... 
 private: 
 TNodoPila<TElem>* _cima; 
} 
 
 
Tipos abstractos de datos 83 
 
 Invariante de la representación 
Dada la cima de una pila p : TNodoPila*, definimos 
 RPila[Elem](p) 
⇔def 
 p = 0 ∨ 
 ( p ≠ 0 ∧ ubicado(p) ∧ 
 RTElem(p->_elem) ∧ RPila[TElem](p->_sig) ∧ 
 p ∉ cadena(p->_sig) ) 
 
donde la función auxiliar cadena permite obtener el conjunto de punteros ac-
cesibles a partir de uno dado: 
cadena( 0 ) =def ∅ 
cadena( p ) =def {p} ∪ cadena(p^.sig) si p ≠ 0 
 
En este invariante de la representación se expresa que 
— la lista enlazada está vacía (p=0) o no lo está 
— si no está vacía, entonces 
— todos los nodos están ubicados, 
— el campo _elem de cada nodo es un representante válido del tipo TElem, 
— no hay ciclos en la estructura. 
 
El problema para implementar una función que determine si un valor de tipo 
TNodoPila* es un representante válido radica en que no existe forma de saber 
si un puntero está o no ubicado. 
 
Tipos abstractos de datos 84 
 
 Dado que las clases que implementan a los nodos sólo han de ser accesibles 
para la clase en cuya implementación colaboran, declararemos sus datos y su 
constructora como privadas, dando acceso privilegiado, en forma de clase ami-
ga, a la clase principal, que ha de ser declarada por anticipado debido a la exis-
tencia de referencias cruzadas. 
template <class TElem> 
class TPilaDinamica; 
template <class TElem> 
class TNodoPila { 
 private: 
 TElem _elem; 
 TNodoPila<TElem>* _sig; 
 TNodoPila( const TElem&, TNodoPila<TElem>* ); 
 public: 
 const TElem& elem() const; 
 TNodoPila<TElem> * sig() const; 
 friend TPilaDinamica<TElem>; 
}; 
 
La implementación de la clase de los nodos queda entonces: 
 
template <class TElem> 
TNodoPila<TElem>::TNodoPila( const TElem& elem, 
TNodoPila<TElem>* sig ) : 
 _elem(elem), _sig(sig) { 
}; 
 
template <class TElem> 
const TElem& TNodoPila<TElem>::elem() const { 
 return _elem; 
} 
 
template <class TElem> 
TNodoPila<TElem>* TNodoPila<TElem>::sig() const { 
 return _sig; 
} 
 
Y la constructora de las pilas: 
 
template <class TElem> 
TPilaDinamica<TElem>::TPilaDinamica( ) : 
 _cima(0) { 
}; 
 
Tipos abstractos de datos 85 
 
Copias, destrucciones y asignaciones 
 
 Dado que el uso de estructuras dinámicas puede dar lugar a los problemas 
mencionados en un apartado anterior, adoptaremos una estrategia conservado-
ra que evite la compartición de estructura y equiparemos a las implementacio-
nes de los TADs con: 
— constructora de copia, 
— destructora, y 
— operador de asignación. 
Dado que en C++ no puede existir un objeto que no haya sido inicializado 
mediante la invocación a una constructora, en una asignación entre objetos de 
la forma 
a = b 
a siempre contendrá un objeto válido del tipo en cuestión. Es por ello, que la 
asignación consistirá en la anulación del valor existente seguida de una copia 
del nuevo valor. 
Definimos entonces dos operaciones privadas auxiliares
libera y copia de forma 
que, salvo por el tipo de los parámetros, la implementación de la constructora 
de copia, la destructora y el operador de asignación será siempre la misma. Por 
ejemplo, en el caso de las pilas: 
 
 template <class TElem> 
 TPilaDinamica<TElem>::TPilaDinamica( const 
TPilaDinamica<TElem>& pila ) { 
 copia(pila); 
 }; 
 
 template <class TElem> 
 TPilaDinamica<TElem>::~TPilaDinamica( ) { 
 libera(); 
 }; 
 
 template <class TElem> 
 TPilaDinamica<TElem>& 
 TPilaDinamica<TElem>::operator=( const TPilaDinamica<TElem>& 
pila ) { 
 if( this != &pila ) { 
 libera(); 
 copia(pila); 
 } 
 return *this; 
 }; 
Tipos abstractos de datos 86 
 
 En el caso de las pilas copia y libera se implementan de la siguiente forma: 
 
 template <class TElem> 
 void TPilaDinamica<TElem>::libera() { 
 while (_cima != 0) { 
 TNodoPila<TElem>* tmp = _cima; 
 _cima = _cima->_sig; 
 delete tmp; 
 } 
 }; 
 
 template <class TElem> 
 void TPilaDinamica<TElem>::copia(const 
TPilaDinamica<TElem>& pila) { 
 if ( pila.esVacio() ) 
 _cima = 0; 
 else { 
 TNodoPila<TElem> *antCopia, *actCopia, *act; 
 act = pila._cima; 
 _cima = new TNodoPila<TElem>( act->_elem, 0 ); 
 actCopia = _cima; 
 while ( act->_sig != 0 ) { 
 act = act->_sig; 
 antCopia = actCopia; 
 actCopia = new TNodoPila<TElem>( act->_elem, 0 ); 
 antCopia->_sig = actCopia; 
 } 
 } 
 }; 
 
 
Tipos abstractos de datos 87 
 
 Sin embargo esta política conservadora que evita la compartición de estructura, 
penaliza el coste temporal de las operaciones. 
Si implementamos la constructora de los nodos y la operación apila de la si-
guiente forma: 
 
template <class TElem> 
void TPilaDinamica<TElem>::apila( TElem elem ) { 
 _cima = new TNodoPila<TElem> ( elem, _cima ); 
}; 
 
template <class TElem> 
TNodoPila<TElem>::TNodoPila( TElem elem, TNodoPila<TElem>* 
sig ) : 
 _elem(elem), _sig(sig) { 
}; 
 
¿cuál es el coste temporal de la siguiente invocación a apila? 
TPilaDinamica< TPilaDinamica<int> > pilaDePilas; 
TPilaDinamica<int> pila; 
... 
 
pilaDePilas.apila( pila ); 
 
considerando que 
n1 = número de elementos de pilaDePilas 
n2 = número de elementos de pila 
 
se tiene 
 
t( n1, n2 ) > n2 + // copia de pila a elem en la invocación 
de apila 
 n2 + // copia de elem a elem en la invocación 
de la 
 // constructora de TNodoPila 
 n2 + // copia de elem al dato _elem en la 
construcción 
 // del nuevo nodo 
 n2 + // destrucción del parámetro elem en la 
 // constructora de TNodoPila 
 n2 + // destrucción del parámetro elem en 
apila 
 = 5 n2 
 
Tipos abstractos de datos 88 
 
Una copia es inevitable para evitar la compartición de estructura, pero no así 
las dos copias adicionales con sus consiguientes anulaciones. La solución po-
dría ser pasar los parámetros siempre por referencia, evitándose así las copias, 
sin embargo esto iría contra la lógica de los algoritmos y pondría en peligro la 
integridad de los datos. C++ ofrece un tercer tipo de parámetros: las referen-
cias constantes. Un parámetro de tipo referencia constante se pasa por variable 
pero sin permitir que el código de la función modifique el valor. 
template <class TElem> 
void TPilaDinamica<TElem>::apila(const TElem& elem) { 
 _cima = new TNodoPila<TElem> ( elem, _cima ); 
}; 
 
template <class TElem> 
TNodoPila<TElem>::TNodoPila( const TElem& elem, 
TNodoPila<TElem>* sig ) : 
 _elem(elem), _sig(sig) { 
}; 
que sólo realiza una copia y ninguna destrucción. 
 
 Así pues, para ser estrictos, en la complejidad de las implementaciones de los 
TAD deberemos incluir la complejidad de las copias y destrucciones de los 
elementos, con la siguiente notación 
— T(TElem::TElem( )) 
coste de la constructora de los elementos 
— T(TElem::TElem(TElem&)) 
coste de la constructora de copia de los elementos 
— T(TElem::~TElem( )) 
coste de la destructora de los elementos 
— T(TElem::operator=(TElem& )) 
coste de la asignación entre elementos 
 
Así, por ejemplo, el coste de la última versión de apila queda 
T(TElem::TElem(TElem&)) + O(1) 
siendo O(1) sobre un tipo predefinido. 
 
Tipos abstractos de datos 89 
 
 Y lo mismo ocurre con los resultados de las funciones. 
Dada esta implementación de la operación cima 
template <class TElem> 
TElem TPilaDinamica<TElem>::cima( ) throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido (); 
 else 
 return _cima->_elem; 
}; 
¿cuál es el coste temporal de la siguiente invocación a apila? 
TPilaDinamica< TPilaDinamica<int> > pilaDePilas; 
TPilaDinamica<int> pila; 
... 
 
pila = pilaDePilas.cima(); 
considerando que 
n1 = número de elementos de la pila que está en la cima de 
pilaDePilas 
n2 = número de elementos de pila 
se tiene 
t( n1, n2 ) > n1 + // copia de _cima->_elem al devolver el 
resultado 
 n2 + // destrucción del valor antiguo de pila 
 n1 + // copia de la copia de _cima->_elem 
sobre pila 
 n1 // destrucción de la copia de _cima-
>_elem 
 = 3 n1 + n2 
 
La solución de nuevo es utilizar referencias constantes 
template <class TElem> 
const TElem& TPilaDinamica<TElem>::cima( ) throw 
(EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido (); 
 else 
 return _cima->_elem; 
}; 
de forma que en el ejemplo anterior sólo realiza una copia de _cima->_elem 
además de la destrucción del valor antiguo de pila. 
 
Tipos abstractos de datos 90 
 
 Es importante que estas referencias también sean constantes porque si no lo 
fueran podríamos modificar los valores almacenados en el TAD encadenando 
mensajes: 
pilaDePilas.cima().desapila(); 
aunque esta decisión también nos prohíbe encadenamientos válidos de mensa-
jes que no producen efectos colaterales, como por ejemplo 
cout << pilaDePilas.cima().cima(); 
porque a un referencia constante no se le pueden pasar mensajes que la pue-
dan modificar … 
Pero ¡ si cima no modifica la pila !, pero el compilador no lo sabe, para indicár-
selo es necesario marcar el método cima como constante 
template <class TElem> 
const TElem& TPilaDinamica<TElem>::cima( ) const throw 
(EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido (); 
 else 
 return _cima->_elem; 
}; 
 
Tipos abstractos de datos 91 
 
 Con todo esto, las operaciones propias de las pilas quedan: 
 
template <class TElem> 
void TPilaDinamica<TElem>::apila(const TElem& elem) { 
 _cima = new TNodoPila<TElem> ( elem, _cima ); 
}; 
// Complejidad: T(TElem::TElem(TElem&)) + O(1) 
 
template <class TElem> 
const TElem& TPilaDinamica<TElem>::cima( ) const throw 
(EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido(); 
 else 
 return _cima->_elem; 
}; 
// Complejidad: O(1) 
 
template <class TElem> 
void TPilaDinamica<TElem>::desapila( ) throw (EAccesoIndebido) 
{ 
 if( esVacio() ) 
 throw EAccesoIndebido(); 
 else { 
 TNodoPila<TElem>* tmp = _cima; 
 _cima = _cima->_sig; 
 delete tmp; 
 } 
}; 
// Complejidad: T(TElem::~TElem()) + O(1) 
 
template <class TElem> 
bool TPilaDinamica<TElem>::esVacio( ) const { 
 return _cima == 0; 
}; 
// Complejidad: O(1) 
 
 
Tipos abstractos de datos 92 
 
 ¿Con esta política de copias estamos condenados a manejar estructuras de da-
tos seguras pero ineficientes? 
No, en primer lugar, la compartición o no de las estructuras depende de la im-
plementación del constructor de copia de los elementos, y, por lo tanto, es po-
sible instanciar las plantillas sobre elementos que no realicen copias. 
En segundo lugar, si queremos evitar por completo las copias no tenemos más 
que instanciar las plantillas con punteros a elementos. Aunque los punteros se 
copien, las estructuras a las que apunten serán compartidas.
Sin embargo, de nuevo, aunque mejoremos la eficiencia de los programas 
habremos de ser más cuidadosos y ser conscientes de los posibles efectos cola-
terales, las referencias perdidas y la generación de basura. 
Por ejemplo, si utilizamos una variable auxiliar local a una función que conten-
ga elementos de tipo puntero, nos deberemos ocupar de anular los elementos 
explícitamente antes de salir de la función; a no ser que esos elementos formen 
parte también de alguno de los resultados, en cuyo caso, si los anulásemos es-
taríamos generando referencias perdidas. 
Por ejemplo, en esta función es necesario anular todos los elementos de pilas 
excepto la cima, que se devuelve como resultado. 
typedef TPilaDinamica< TPilaDinamica<int>* > TPuntPilas; 
 
const TPilaDinamica<int>& pilaN ( int num ) { 
 TPuntPilas pilas; 
 TPilaDinamica<int>* pila; 
 pila = new TPilaDinamica<int>(); 
 pila->apila(0); 
 pilas.apila(pila); 
 for ( int i = 1; i < num; i++ ) { 
 pila = new TPilaDinamica<int>( * pilas.cima() ); 
 pila->apila( i ); 
 pilas.apila(pila); 
 } 
 pila = pilas.cima(); 
 pilas.desapila(); 
 while ( ! pilas.esVacio() ) { 
 delete pilas.cima(); 
 pilas.desapila(); 
 } 
 return *pila; 
} 
 
Tipos abstractos de datos 93 
 
 Aunque, en realidad, la anterior implementación es intencionadamente inefi-
ciente, ya que las pilas pueden compartir estructura entre sí, no siendo necesa-
ria la construcción de copias. 
 
const TPilaDinamica<int>& pilaN2 ( int num ) { 
 TPuntPilas pilas; 
 TPilaDinamica<int>* pila; 
 
 pila = new TPilaDinamica<int>(); 
 pila->apila(0); 
 pilas.apila(pila); 
 for ( int i = 1; i < num; i++ ) { 
 pila = pilas.cima(); 
 pila->apila( i ); 
 pilas.apila(pila); 
 } 
 
 return * pilas.cima(); 
} 
 
¿Podrías dibujar cómo evoluciona la memoria cuando se realizan las invoca-
ciones pilaN(4) y pilaN2(4)? 
 
Tipos abstractos de datos 94 
 
Almacenamiento de los datos en disco 
 
 Los datos representados por estructuras dinámicas no pueden escribirse y leer-
se en archivos directamente, porque los valores de los punteros –direcciones 
de memoria–serán diferentes en cada ejecución particular. Es por ello que 
— la escritura de estructuras dinámicas se limitará a escribir la información –
los elementos– sin guardar los valores de los punteros, y 
— la lectura deberá reconstruir las estructuras dinámicas, a partir de los datos 
leídos del archivo, solicitando nueva memoria dinámica donde almacenar 
los datos leídos. 
 
 Normalmente incluiremos en los TAD una operación de escritura y redefini-
remos el operador de inserción para el tipo en cuestión. 
 
template <class TElem> 
void TPilaDinamica<TElem>::escribe( ostream& salida ) const { 
 TNodoPila<TElem>* tmp = _cima; 
 while ( tmp != 0 ) { 
 salida << tmp->elem() << endl; 
 tmp = tmp->sig(); 
 } 
}; 
 
template <class TElem> 
ostream& operator<<( ostream& salida, const 
TPilaDinamica<TElem>& pila ){ 
 pila.escribe(salida); 
 return salida; 
}; 
 
La razón de no declarar directamente el operador de inserción como amigo de 
la clase TPilaDinamica, evitando así la necesidad de definir la operación escribe, 
radica en que el mecanismo de plantillas interfiere con la resolución de identi-
ficadores y obliga a que la implementación del operador de inserción aparezca 
directamente en la definición de clase. 
Nótese que la escritura de las pilas se basa en la escritura de sus elementos, y 
que, por lo tanto, ahora sólo podremos instanciar la plantilla TPilaDinami-
ca<TElem> con tipos sobre los que esté definido operador<<. 
 
Tipos abstractos de datos 95 
 
Igualdad 
 
 El tipo representante de un tipo abstracto puede no admitir el uso de la com-
paración de igualdad. Y aunque la admitiese, la identidad entre valores del tipo 
representante en general no corresponderá a la igualdad entre los valores abs-
tractos representados. Este problema se presenta por igual en las implementa-
ciones estáticas como en las dinámicas; aunque en las estáticas puede o no 
ocurrir mientras que en las dinámicas sucede siempre3. 
 int *p1, *p2; 
 p1 = new int; 
 p2 = new int; 
 *p1 = 1; 
 *p2 = 1; 
 cout << ( p1 == p2 ? “iguales” : “diferentes” ) << endl; 
 
Aunque dos estructuras dinámicas almacenen exactamente los mismos valores, 
lo harán en posiciones de memoria diferentes, es decir, apuntados por punte-
ros diferentes. La solución radica en redefinir los operadores correspondientes 
a las comparaciones que se deseen permitir sobre los valores del TAD, hacien-
do que dichas implementaciones recorran las estructuras comparando los valo-
res e ignorando el valor de los punteros. 
template <class TElem> 
bool TPilaDinamica<TElem>::operator==( const TPilaDinamica& 
pila ) const { 
 bool iguales; 
 TNodoPila<TElem> *p, *q; 
 iguales = true; 
 p = _cima; 
 q = pila._cima; 
 while ( ( p != 0 ) && (q != 0) && iguales ) { 
 iguales = p->elem() == q->elem(); 
 p = p->sig(); 
 q = q->sig(); 
 } 
 return iguales && ( p == 0 ) && ( q == 0 ); 
} 
Nótese que la comparación entre pilas se basa en la comparación entre sus 
elementos, y que, por lo tanto, ahora sólo podremos instanciar la plantilla TPi-
laDinamica<TElem> con tipos sobre los que esté definido operador==. 
 
3 Por otra parte, C++ no permite realizar comparaciones entre objetos a no ser que se haya redefinido el operador correspon-
diente. 
Tipos abstractos de datos 96 
 
La clase TPilaDinamica 
 
template <class TElem> 
class TPilaDinamica { 
 public: 
 // Constructoras, destructora y operador de asignación 
 TPilaDinamica( ); 
 TPilaDinamica( const TPilaDinamica<TElem>& ); 
 ~TPilaDinamica( ); 
 TPilaDinamica<TElem>& operator=( const 
TPilaDinamica<TElem>& ); 
 
 // Operaciones de las pilas 
 void apila(const TElem&); 
 // Pre: true 
 // Post: Se añade 'elem' a la cima de la pila 
 const TElem& cima( ) const throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Devuelve el elemento que está en la cima 
 // Lanza la excepción TPilaArray<TElem>::Vacio si la pila 
está vacía 
 void desapila( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Elimina el elemento que está en la cima 
 // Lanza la excepción TPilaArray::Vacio si la pila está 
vacía 
 bool esVacio( ) const; 
 // Pre: true 
 // Post: Devuelve true | false según si la pila está o no 
vacía 
 
 // Comparación 
 bool operator==( const TPilaDinamica& ) const; 
 // Pre: true 
 // Post: Determina si dos pilas coinciden 
 
 // Escritura 
 void escribe( ostream& ) const; 
 
 private: 
 // Variables privadas 
 TNodoPila<TElem>* _cima; 
 
 // Operaciones privadas 
 void libera(); 
 void copia(const TPilaDinamica<TElem>& pila); 
}; 
Tipos abstractos de datos 97 
 
3.5.5 Prueba de los TADs 
 Ampliamos la aplicación de prueba para que también se ocupe de las nuevas 
operaciones. Además, utilizaremos como tipo de los elementos la clase TEntero 
que encapsula al tipo int y que nos permite comprobar el funcionamiento de 
las constructoras, la asignación y la destructora. 
 
class TEntero { 
 public: 
 TEntero( int valor = 0 ): _valor(new int(valor)) { 
// ShowMessage("TEntero: constructora por defecto"); 
 }; 
 TEntero( const TEntero& entero ) : _valor(new 
int(*entero._valor)) { 
// ShowMessage("TEntero: constructora de copia"); 
 }; 
 ~TEntero( ) { 
// ShowMessage("TEntero: destructora"); 
 delete _valor; 
 }; 
 TEntero& operator=( const TEntero& entero ) { 
// ShowMessage("TEntero: asignación"); 
 if( this != &entero ) 
 *_valor = *entero._valor; 
 return *this; 
 }; 
 bool operator==( const TEntero& entero ) { 
 return *_valor == *entero._valor; }; 
 bool operator<( const TEntero& entero ) { 
 return *_valor < *entero._valor; };
bool operator>( const TEntero& entero ) { 
 return *_valor > *entero._valor; }; 
 int valor( ) const { return *_valor; }; 
 TEntero& operator++ ( ) { 
 (*_valor)++; 
 return *this; 
 }; 
 friend std::ostream& operator<<( std::ostream& salida, 
 const TEntero& entero ) { 
 salida << *entero._valor; 
 return salida; 
 }; 
 private: 
 int* _valor; 
}; 
Tipos abstractos de datos 98 
 
 El formulario maneja dos punteros a valores del tipo en cuestión 
 
typedef TPilaDinamica<TEntero> TPilaEnt; 
 
TPilaEnt *p1, *p2; 
 
 
 
 
 
y si la función GetHeapStatus funcionase, también se mostraría la memoria di-
námica utilizada en cada instante … 
EDI todo/Tema4-TAD?s con estruc lineal.pdf
 
TEMA 4 
TIPOS ABSTRACTOS DE DATOS CON 
ESTRUCTURA LINEAL 
 
1. Pilas 
2. Colas 
3. Colas dobles 
4. Listas 
5. Secuencias 
 
 
 
 
 
 
 
 
 
 
 
 
 
Bibliografía: Fundamentals of Data Structures in C++ 
E. Horowitz, S. Sahni, D. Mehta 
Computer Science Press, 1995 
Data Abstraction and Problem Solving with C++, Second 
Edition 
Carrano, Helman y Veroff 
 
Tipos abstractos de datos con estructura lineal 1 
 
4.1 Pilas 
 En el tema anterior ya hemos estudiado las dos implementaciones más habi-
tuales de las pilas. En este aparato simplemente veremos una de sus aplicacio-
nes: la transformación a iterativo de algoritmos recursivos lineales. 
 
4.1.1 Eliminación de la recursión lineal no final 
 
 El esquema de la recursión simple 
void nombreProc ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
// declaración de constantes 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 δ1 y1’ ; ... ; δm ym’ ; // yr ’ 
 
 if ( d(xr ) ) 
 yr = g(xr ); 
 else if ( ¬d(xr ) ) { 
 xr ‘ = s(xr ); 
 nombreProc(xr ‘, yr ‘); 
 yr = c(xr , yr ‘); 
 } 
// Postcondición 
} 
 
La ejecución de nombreProc(xr , yr ) se puede ver como un “bucle descendente” 
seguido de un “bucle ascendente” 
xr → nombreProc(xr , yr ) c(xr , . ) → yr 
 ↓ 
 nombreProc (s(xr ), yr ) c(s(xr ), . ) 
 ↓ 
 nombreProc (s2(xr ), yr ) c(s2(xr ), . ) 
 ↓ 
 ... 
 nombreProc (sn–1(xr ), yr ) c(sn-1(xr ), . ) 
 ↓ 
 nombreProc (sn(xr ), yr ) → g(sn(xr )) 
 
Tipos abstractos de datos con estructura lineal 2 
 
 La transformación a iterativo se basa en ese esquema, se utilizan dos bucles 
compuestos secuencialmente, de forma que 
— en el primero se van obteniendo las descomposiciones recursivas hasta llegar 
al caso base, y 
— en el segundo se aplica sucesivamente la función de combinación. 
Nótese que en el segundo bucle, para ir aplicando sucesivamente la función de 
combinación se debe disponer de los parámetros correspondientes a esa lla-
mada: sn–i(xr ). 
 
 Según la forma de obtener los valores de sn–i(xr ) en el bucle ascendente distin-
guimos tres casos, que vienen dados por la forma de la función de descompo-
sición recursiva: 
— Caso especial. La función s de descomposición recursiva, posee una función 
inversa calculable s–1. 
Los datos sn–i(xr ) pueden calcularse sobre la marcha, usando s–1. 
— Caso general. La función s no posee inversa –o, aunque la posea, ésta no es 
calculable, o su cálculo resulta demasiado costoso–. 
Los datos sn–i(xr ) se irán almacenando en una pila en el curso del bucle des-
cendente y se irán recuperando de ésta en el curso del bucle ascendente. 
— Combinación de los casos especial y general. 
— En cada iteración del bucle descendente no es necesario apilar sn–i(xr ) 
sino que es suficiente con apilar un dato más simple ur n–i , de forma que 
— en el bucle ascendente es fácil calcular sn–i(xr ) a partir de ur n–i, obtenido 
de la pila, y sn–i+1(xr ) obtenido en la iteración anterior. 
Tipos abstractos de datos con estructura lineal 3 
 
Caso especial 
 
 El esquema de la transformación 
 
void nombreProcit ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 
 xr ’ = xr ; 
 while ( ¬d(xr ’) ) 
 xr ’ = s(xr ’); 
 yr = g(xr ’); 
 while ( xr ’ != xr ) { 
 xr ’ = s-1(xr ’); 
 yr = c(xr ’, yr ); 
 } 
// Postcondición 
} 
 
 
 Transformación de la versión recursiva no final de la función factorial 
s(n) = n–1 s–1(n) = n+1 
 
int fact ( int n ) { 
// Pre : n >= 0 } 
 
 int r, nAux; 
 
 nAux = n; 
 while ( nAux != 0 ) 
 nAux = nAux - 1; 
 r = 1; 
 while ( nAux != n ) { 
 nAux = nAux + 1; 
 r = r * nAux; 
 } 
 return r; 
// Post: devuelve n! 
} 
Es obvio que en este caso no se necesita el bucle descendente. 
Tipos abstractos de datos con estructura lineal 4 
 
Caso general 
 Este es el caso donde nos ayudamos de una pila para almacenar los sucesivos 
valores del parámetro xr 
 
void nombreProcit ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 TPila< TTupla<τ1, ..., τn> > xs; 
 
 xr ’ = xr ; 
 while ( ¬d(xr ’) ) { 
 xs.apila(xr ’); 
 xr ’ = s(xr ’); 
 } 
 yr = g(xr ’); 
 while ( ! xs.esVacio() ) { 
 xr ’ = xs.cima(); 
 yr = c(xr ’, yr ); 
 xs.desapila(); 
 } 
// Postcondición 
} 
 
 
Para utilizar la implementación genérica de las pilas en este algoritmo, es nece-
sario implementar una clase que represente a las tuplas de n elementos 
TTupla<τ1, ..., τn> 
excepto cuando n = 1 en cuyo caso los elementos de la pila serán directamente 
de tipo τ1. 
 
Tipos abstractos de datos con estructura lineal 5 
 
 Como ejemplo, veamos cómo se aplica este esquema a la función bin que ob-
tiene la representación binaria de un número decimal 
 
int bin( int n ) { 
// Pre: n >= 0 
 
 int r; 
 
 if ( n < 2 ) 
 r = n; 
 else if ( n >= 2 ) 
 r = 10 * bin(n / 2) + (n % 2); 
 
 return r; 
 
// Post: devuelve la representación binaria de n 
} 
 
La transformación a iterativo 
 
int binIt( int n ) { 
// Pre: n >= 0 
 
 int r, nAux; 
 TPilaDinamica<int> ns; 
 
 nAux = n; 
 while ( nAux >= 2 ) { 
 ns.apila(nAux); 
 nAux = nAux / 2; // n' = s(n') 
 } 
 r = nAux; 
 while ( ! ns.esVacio() ) { 
 nAux = ns.cima(); 
 r = 10 * r + nAux % 2; // r = c(n',r) 
 ns.desapila(); 
 } 
 
 return r; 
 
// Post: devuelve la representación binaria de n 
} 
Tipos abstractos de datos con estructura lineal 6 
 
Combinación de los casos especial y general 
 En este caso sólo es necesario apilar una parte de la información que contie-
nen los parámetros xr de forma que luego sea posible reconstruir dicho pará-
metro a partir de la información apilada y s(xr ). 
Formalmente, hemos de encontrar dos funciones f y h que verifiquen: 
¬d(x) ⇒ ur = f(xr ) está definido y cumple que xr = h(ur , s(xr )) 
La versión iterativa queda entonces 
 
void nombreProcit ( τ1 x1 , … , τn xn , δ1 & y1 , … , δm & ym ) { 
// Precondición 
 τ1 x1’ ; ... ; τn xn’ ; // xr ’ 
 σ1 u1 ; ... ; σp up ; 
 TPila< TTupla<σ1, ..., σp> > us; 
 
 xr ’ = xr ; 
 while ( ¬d(xr ’) ) { 
 ur = f(xr ’); 
 us.apila(ur ); 
 xr ’ = s(xr ’); 
 } 
 yr = g(xr ’); 
 while ( ! us.esVacio() ) { 
 ur = us.cima(); 
 xr ’ = h(ur , xr ’); 
 yr = c(xr ’, yr ); 
 us.desapila(); 
 } 
// Postcondición 
} 
 
Para utilizar la implementación genérica de las pilas en este algoritmo, es nece-
sario implementar una clase que represente a las tuplas de p elementos 
TTupla<σ1, ..., σp> 
excepto cuando p = 1 en cuyo caso los elementos de la pila serán directamente 
de tipo σ1. 
 
Tipos abstractos de datos con estructura lineal 7 
 
 En la función bin la descomposición recursiva es: 
s(n) = n / 2; 
Aquí no se puede aplicar la técnica del caso especial porque no existe la inversa 
de esta función: para obtener n a partir de n /2 tenemos que saber si n es par o 
impar. Sin embargo, en realidad no hace falta apilar n, basta con apilar su
pari-
dad, pues a partir de n / 2 y la paridad de n podemos obtener n. 
 
f(n) = par(n) = u 
 
 2 * s(n) si u 
h(u, s(n)) = 
 2 * s(n) + 1 si NOT u 
 
Con lo que la versión iterativa aplicando este nuevo esquema quedará: 
 
int binIt2( int n ) { 
// Pre: n >= 0 
 
 int r, nAux; 
 bool u; 
 TPilaDinamica<bool> us; 
 
 nAux = n; 
 while ( nAux >= 2 ) { 
 u = (nAux % 2) == 0; // u = f(n') 
 us.apila(u); 
 nAux = nAux / 2; // n' = s(n') 
 } 
 r = nAux; 
 while ( ! us.esVacio() ) { 
 u = us.cima(); 
 if ( u ) 
 nAux = 2 * nAux; 
 else // n' = h(u, n') 
 nAux = 2 * nAux + 1; 
 r = 10 * r + nAux % 2; // r = c(n',r) 
 us.desapila(); 
 } 
 return r; 
// Post: devuelve la representación binaria de n 
} 
Tipos abstractos de datos con estructura lineal 8 
 
4.2 Colas 
 Este TAD representa una colección de datos del mismo tipo donde las opera-
ciones de acceso hacen que su comportamiento se asemeje al de una cola de 
personas esperando a ser atendidas: los elementos se añaden por el final y se 
eliminan por el principio, o dicho de otra forma, el primero en llegar es el pri-
mero en salir –en ser atendido–: FIFO –first in first out–. 
Especificación 
 
tad COLA [E :: ANY] 
 usa 
 BOOL 
 tipo 
 Cola[Elem] 
 operaciones 
 Nuevo: → Cola[Elem] /* gen */ 
 PonDetras: (Elem, Cola[Elem]) → Cola[Elem] /* gen */ 
 quitaPrim: Cola[Elem] – → Cola[Elem] /* mod */ 
 primero: Cola[Elem] – → Elem /* obs */ 
 esVacio: Cola[Elem] → Bool /* obs */ 
 ecuaciones 
 ∀ x : Elem : ∀ xs : Cola[Elem] : 
 def quitaPrim(PonDetras(x, xs)) 
 quitaPrim(PonDetras(x, xs)) = Nuevo si esVacio(xs) 
 quitaPrim(PonDetras(x, xs)) = 
 PonDetras(x, quitaPrim(xs)) si NOT esVacio(xs) 
 def primero(PonDetras(x, xs)) 
 primero(PonDetras(x, xs)) = x si esVacio(xs) 
 primero(PonDetras(x, xs)) = primero(xs) si NOT esVacio(xs) 
 esVacio(Nuevo) = Cierto 
 esVacio(PonDetras(x, xs)) = Falso 
 errores 
 avanza(Nuevo) 
 primero(Nuevo) 
ftad 
 
 
 
Tipos abstractos de datos con estructura lineal 9 
 
Implementación estática basada en un vector 
Tipo representante 
 La idea de la representación es almacenar los datos en posiciones consecutivas 
de un array, indicando con dos índices el principio, ini, y el final, fin, de la zona 
utilizada. Inicialmente ini y fin están al principio del vector, pero tras un cierto 
número de invocaciones a Añade y avanza llegaremos a un estado como este: 
 
0 longMax-1
_ini _fin 
 
 La estructura de datos: 
template <class TElem> 
class TColaEstatica { 
 public: 
 // Tamaño máximo 
 static const int longMax = 100; 
 ... 
 private: 
 // Variables privadas 
 int _ini, _fin; 
 TElem _espacio[longMax]; 
 ... 
}; 
 
 Invariante de la representación 
Dada xs : TColaEstatica 
 R(xs) 
⇔def 
 0 ≤ xs._ini ≤ longMax ∧ -1 ≤ xs._fin ≤ longMax-1 ∧ 
 xs._ini ≤ xs._fin+1 ∧ 
 ∀ i : xs._ini ≤ i ≤ xs._fin : R(xs._espacio[i]) 
Nótese que una cola vacía se caracteriza por xs._ini = xs._fin+1. 
 
Tipos abstractos de datos con estructura lineal 10 
 
Interfaz de la implementación 
 
template <class TElem> 
class TColaEstatica { 
 public: 
 
 // tamaño máximo 
 static const int longMax = 5; 
 
 // Constructoras y operador de asignación 
 TColaEstatica( ); 
 TColaEstatica( const TColaEstatica<TElem>& ); 
 TColaEstatica<TElem>& operator=( const TColaEstatica<TElem>& ); 
 
 // Operaciones de las colas 
 void ponDetras(const TElem&) throw (EDesbordamiento); 
 // Pre: ! esLleno( ) 
 // Post: Se añade 'elem' al final de la cola 
 // Lanza la excepción EDesbordamiento si la cola está llena 
 const TElem& primero( ) const throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Devuelve el primer elemento de la cola 
 // Lanza la excepción EAccesoIndebido si la cola está vacía 
 void quitaPrim( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Elimina el primer elemento de la cola 
 // Lanza la excepción EAccesoIndebido si la cola está vacía 
 bool esVacio( ) const; 
 // Pre: true 
 // Post: Devuelve true | false según si la cola está o no vacía 
 
 // Limitaciones de la implementación 
 bool esLleno( ) const; 
 // Pre: true 
 // Post: determina si es posible añadir más elementos a la cola 
 
 private: 
 // Variables privadas 
 int _ini, _fin; 
 TElem _espacio[longMax]; 
 // Operaciones privadas 
 void copia(const TColaEstatica<TElem>&); 
}; 
Tipos abstractos de datos con estructura lineal 11 
 
Implementación de las operaciones 
template <class TElem> 
TColaEstatica<TElem>::TColaEstatica( ) : 
 _ini(0), _fin(-1) { }; 
// O( longMax * TElem::TElem() ), O(1) sobre tipos predefinidos 
 
template <class TElem> 
void TColaEstatica<TElem>::ponDetras(const TElem& elem) 
 throw (EDesbordamiento){ 
 if ( esLleno() ) 
 throw EDesbordamiento( "Cola llena" ); 
 _fin++; 
 _espacio[_fin] = elem; 
}; 
// O( TElem::operator=(TElem&) ), O(1) sobre tipos predefinidos 
 
template <class TElem> 
const TElem& TColaEstatica<TElem>::primero( ) const 
 throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía"); 
 return _espacio[_ini]; 
}; 
// O(1) 
 
template <class TElem> 
void TColaEstatica<TElem>::quitaPrim( ) throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía" ); 
 _ini++; 
}; 
// O(1) 
 
template <class TElem> 
bool TColaEstatica<TElem>::esVacio( ) const { 
 return _ini == _fin+1; 
}; 
// O(1) 
template <class TElem> 
bool TColaEstatica<TElem>::esLleno( ) const { 
 return _fin == longMax-1; 
}; 
// O(1) 
Tipos abstractos de datos con estructura lineal 12 
 
 Aunque todos los datos de TColaEstatica son estáticos, redefinimos el cons-
tructor de copia y el operador de asignación para copiar/asignar tan solo los 
elementos almacenados y no el array entero. 
 
template <class TElem> 
void TColaEstatica<TElem>::copia(const TColaEstatica<TElem>& cola) { 
 int numElem = cola._fin - cola._ini + 1; 
 for ( int i = 0; i < numElem ; i++ ) 
 _espacio[i] = cola._espacio[cola._ini + i]; 
 _ini = 0; 
 _fin = -1 + numElem; 
}; 
// O( n’ * TElem::operator=(TElem&) ) 
// O(n’) sobre tipos predefinidos 
// siendo n’ el número de elementos de ‘cola’ 
 
template <class TElem> 
TColaEstatica<TElem>::TColaEstatica( const TColaEstatica<TElem>& cola ) { 
 copia(cola); 
}; 
// O( longMax ⋅ TElem::TElem() + n’ * TElem::operator=(TElem&) ) 
// O(n’) sobre tipos predefinidos 
// siendo n’ el número de elementos de ‘cola’ 
 
template <class TElem> 
TColaEstatica<TElem>& 
TColaEstatica<TElem>::operator=( const TColaEstatica<TElem>& cola ) { 
 if( this != &cola ) { 
 copia(cola); 
 } 
 return *this; 
}; 
// O( n’ * TElem::operator=(TElem&) ) 
// O(n’) sobre tipos predefinidos 
// siendo n’ el número de elementos de ‘cola’ 
 
El coste de la destructora, que no implementamos, será 
O( longMax * TElem::~TElem() ) 
o 
O(1) 
sobre tipos predefinidos. 
Tipos abstractos de datos con estructura lineal 13 
 
Desventaja de esta implementación 
 
 El espacio ocupado por la representación de la cola se va desplazando hacia la 
derecha al ejecutar ponDetras y quitaPrim. 
Cuando _fin alcanzar el valor longMax-1 no se pueden añadir nuevos elemen-
tos, aunque quede sitio en _espacio[0.._ini–1]. 
 
0 longMax-1
_ini _fin 
 
Una solución sería que cuando llegásemos a esta situación se desplazasen to-
dos los elementos de la pila hacia la izquierda haciendo _ini=0. Sin embargo 
esto penalizaría en esos casos la eficiencia de la operación ponDetras –O(n)– 
 
 
 
Tipos abstractos de datos con estructura lineal 14 
 
Implementación basada en un vector circular 
 
 Esta implementación está pensada para resolver el problema de la anterior; 
cuando _fin alcanza el valor longMax-1 y queda espacio
libre entre 0 e _ini–1 se 
utiliza ese espacio para seguir añadiendo elementos. 
La idea es considerar que los extremos del array está unidos 
 
0
longMax-1
_ini
_fin 
 
Hay que tener cuidado porque ahora puede suceder que _ini > _fin, y en parti-
cular _ini=_fin+1 puede implicar que la cola esté llena o vacía. 
 
 Además nos aprovecharemos de la posibilidad de ubicar arrays dinámicamente 
para aumentar el tamaño del array cuando éste se llene. 
Para ello utilizamos el puntero _espacio que apuntará a un array ubicado diná-
micamente y la variable _capacidad que almacena en cada instante el tamaño del 
array ubicado. 
int _capacidad; // equivalente a longMax 
TElem *_espacio; 
 
Cuando estando todas las posiciones del array ocupadas se intenta insertar un 
nuevo elemento 
— se ubica un array del doble de capacidad, 
— se copia el array antiguo sobre el recién ubicado, y 
— se anula el array antiguo. 
 
Tipos abstractos de datos con estructura lineal 15 
 
Tipo representante 
 La estructura de datos 
template <class TElem> 
class TColaEstatica { 
 ... 
 
 private: 
 // Variables privadas 
 int _ini, _fin; 
 int _longitud; // número de elementos de la cola 
 int _capacidad; 
 TElem *_espacio; // _espacio[_capacidad] 
 ... 
}; 
 
El campo _longitud permite distinguir en la situación _ini=_fin+1 si tenemos 
una cola vacía o llena. 
 
 Invariante de la representación 
Dado xs : TColaEstatica 
 
 R(xs) 
⇔def 
 (xs._ini = 0 ∧ xs._longitud = 0 ∧ xs._fin = xs._capacidad-1) ∨ 
 (0 ≤ xs._longitud ≤ xs._capacidad ∧ 
 0 ≤ xs._ini ≤ xs.capacidad-1 ∧ 
 xs._fin = (xs._ini + xs._longitud-1) mod xs._capacidad ∧ 
 ∀ i : 0 ≤ i ≤ xs._longitud–1 : 
 R(xs._espacio[xs._ini+i mod xs._capacidad]) ) 
 
 
 
 
Tipos abstractos de datos con estructura lineal 16 
 
Interfaz de la implementación 
 
template <class TElem> 
class TColaEstatica { 
 public: 
 
 // Constructoras, destructora y operador de asignación 
 TColaEstatica( int = 5 ); 
 // Recibe el tamaño inicial de la cola 
 
 TColaEstatica( const TColaEstatica<TElem>& ); 
 ~TColaEstatica( ); 
 TColaEstatica<TElem>& operator=( const TColaEstatica<TElem>& ); 
 
 // Operaciones de las colas 
 void ponDetras(const TElem&); 
 // Pre: true 
 // Post: Se añade 'elem' al final de la cola 
 
 const TElem& primero( ) const throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Devuelve el primer elemento de la cola 
 
 void quitaPrim( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Elimina el primer elemento de la cola 
 
 bool esVacio( ) const; 
 // Pre: true 
 // Post: Devuelve true | false según si la cola está o no vacía 
 
 private: 
 // Variables privadas 
 int _longitud; // número de elementos de la cola 
 int _capacidad; 
 TElem *_espacio; // _espacio[_capacidad] 
 int _ini, _fin; 
 
 // Operaciones privadas 
 void copia(const TColaEstatica<TElem>&); 
 void libera(); 
}; 
Tipos abstractos de datos con estructura lineal 17 
 
Implementación de las operaciones 
 
// Constructoras, destructora y operador de asignación 
 
template <class TElem> 
TColaEstatica<TElem>::TColaEstatica( int capacidad ) : 
 _capacidad(capacidad), _ini(0), _fin(-1), _longitud(0), 
 _espacio(new TElem[_capacidad]) { 
}; 
// O( _capacidad * TElem::TElem() ), O(1) sobre tipos predefinidos 
 
 
// Operaciones auxiliares de copia y anulación 
 
template <class TElem> 
void TColaEstatica<TElem>::libera() { 
 delete [] _espacio; 
}; 
// O( _capacidad * TElem::~TElem() ), O(1) sobre tipos predefinidos 
 
 
template <class TElem> 
void TColaEstatica<TElem>::copia(const TColaEstatica<TElem>& cola) { 
 _capacidad = cola._capacidad; 
 _longitud = cola._longitud; 
 _espacio = new TElem[ _capacidad ]; 
 for ( int i = 0; i < _longitud; i++ ) 
 _espacio[i] = cola._espacio[(cola._ini+i) % _capacidad]; 
 _ini = 0; 
 _fin = _longitud-1; 
}; 
// O( cola._capacidad * TElem::TElem() + n’ * TElem::operator=(TElem&) ), 
// O(n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
Tipos abstractos de datos con estructura lineal 18 
 
template <class TElem> 
TColaEstatica<TElem>::TColaEstatica( const TColaEstatica<TElem>& cola ) { 
 copia(cola); 
}; 
// O( cola._capacidad * TElem::TElem() + n’ * TElem::operator=(TElem&) ), 
// O(n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
 
template <class TElem> 
TColaEstatica<TElem>::~TColaEstatica( ) { 
 libera(); 
}; 
// O( _capacidad * TElem::~TElem() ), O(1) sobre tipos predefinidos 
 
template <class TElem> 
TColaEstatica<TElem>& 
TColaEstatica<TElem>::operator=( const TColaEstatica<TElem>& cola ) { 
 if( this != &cola ) { 
 libera(); 
 copia(cola); 
 } 
 return *this; 
}; 
// O( _capacidad * TElem::~TElem() + 
// cola._capacidad * TElem::TElem() + n’ * TElem::operator=(TElem&) ), 
// O(n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
En el operador de asignación se podría distinguir el caso en el que las dos colas 
tienen la misma _capacidad, donde no es necesario liberar y copiar sino que bas-
ta con realizar asignaciones. 
Tipos abstractos de datos con estructura lineal 19 
 
template <class TElem> 
void TColaEstatica<TElem>::ponDetras(const TElem& elem) { 
 if ( _longitud == _capacidad ){ 
 _capacidad *= 2; 
 TElem* nuevo = new TElem[_capacidad]; 
 for( int i = 0; i < _longitud; i++ ) 
 nuevo[i] = _espacio[(_ini+i) % (_capacidad/2)]; 
 delete [] _espacio; _espacio = nuevo; 
 _ini = 0; _fin = _longitud-1; 
 } 
 _fin = (_fin + 1) % _capacidad ; 
 _longitud++; 
 _espacio[_fin] = elem; 
}; 
// Si _longitud < _capacidad 
// O( TElem::operator=(TElem&), O(1) sobre tipos predefinidos 
// Si _longitud == _capacidad 
// O( 2 * _capacidad * TElem::TElem() + 
// n * TElem::operator=(TElem&) + _capacidad * TElem::~TElem() ), 
// O(n) sobre tipos predefinidos 
template <class TElem> 
const TElem& TColaEstatica<TElem>::primero( ) const 
 throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía"); 
 return _espacio[_ini]; 
}; 
// O(1) 
template <class TElem> 
void TColaEstatica<TElem>::quitaPrim( ) throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía" ); 
 _ini = (_ini + 1) % _capacidad; 
 _longitud--; 
}; 
// O(1) 
 
template <class TElem> 
bool TColaEstatica<TElem>::esVacio( ) const { 
 return _longitud == 0; 
}; 
// O(1) 
Tipos abstractos de datos con estructura lineal 20 
 
Implementación dinámica 
Tipo representante 
 La idea es tener acceso directo al principio y al final de la cola para así poder 
realizar las operaciones de manera eficiente. 
xnx2x1 …
prim ult
xs
 
 
 
 La estructura de datos 
template <class TElem> 
class TNodoCola { 
 private: 
 TElem _elem; 
 TNodoCola<TElem>* _sig; 
 
 ... 
 
}; 
 
template <class TElem> 
class TColaDinamica { 
 
 ... 
 
 private: 
 TNodoCola<TElem> *_prim, *_ult; 
 ... 
 
}; 
 
Tipos abstractos de datos con estructura lineal 21 
 
 Invariante de la representación 
Dada xs : TCola 
R(xs) ⇔def RCV(xs) ∨ RCNV(xs) 
donde RCV se refieren a las condiciones de una cola vacía y RCNV a las de una 
que no está vacía 
 RCV(xs) 
⇔def 
 xs._prim = 0 ∧ xs._ult = 0 
 
 RCNV(xs) 
⇔def 
 xs._prim ≠ 0 ∧ xs._ult ≠ 0 ∧ 
 buenaCola(xs._prim, xs._ult) 
 
y el predicado buenaCola por su parte 
 buenaCola(p,q) 
⇔def 
 ( p = q ∧ ubicado(p) ∧ R(p->_elem) ∧ p->_sig = 0 ) ∨ 
 ( p ≠ q ∧ ubicado(p) ∧ R(p->_elem) ∧ 
 p ∉ cadena(p->_sig) ∧ buenaCola(p->_sig, q) ) 
 
cadena(p) =def ∅ si p = 0 
cadena(p) =def {p} ∪ cadena(p->_sig) si p ≠ 0 
 
donde se indica que: 
— desde p es posible alcanzar q 
— todos los nodos entre p y q están ubicados 
— todos los nodos contienen representantes
válidos del tipo de los elementos 
— no hay ciclos en la lista enlazada 
Tipos abstractos de datos con estructura lineal 22 
 
Interfaz de la implementación 
 Es igual que en la implementación estática 
 
template <class TElem> 
class TColaDinamica { 
 public: 
 
 // Constructoras, destructora y operador de asignación 
 TColaDinamica( ); 
 TColaDinamica( const TColaDinamica<TElem>& ); 
 ~TColaDinamica( ); 
 TColaDinamica<TElem>& operator=( const TColaDinamica<TElem>& ); 
 
 // Operaciones de las colas 
 void ponDetras(const TElem&); 
 // Pre: true 
 // Post: Se añade 'elem' al final de la cola 
 
 const TElem& primero( ) const throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Devuelve el primer elemento de la cola 
 // Lanza la excepción EAccesoIndebido si la cola está vacía 
 
 void quitaPrim( ) throw (EAccesoIndebido); 
 // Pre: ! esVacio( ) 
 // Post: Elimina el primer elemento de la cima 
 // Lanza la excepción EAccesoIndebido si la cola está vacía 
 
 bool esVacio( ) const; 
 // Pre: true 
 // Post: Devuelve true | false según si la pila está o no vacía 
 
 private: 
 // Variables privadas 
 TNodoCola<TElem> *_prim, *_ult; 
 
 // Operaciones privadas 
 void libera(); 
 void copia(const TColaDinamica<TElem>& pila); 
}; 
 
 
 
Tipos abstractos de datos con estructura lineal 23 
 
Implementación de las operaciones 
 
 La clase de los nodos 
 
template <class TElem> 
class TNodoCola { 
 private: 
 TElem _elem; 
 TNodoCola<TElem>* _sig; 
 TNodoCola( const TElem&, TNodoCola<TElem>* = 0 ); 
 public: 
 const TElem& elem() const; 
 TNodoCola<TElem> * sig() const; 
 friend TColaDinamica<TElem>; 
}; 
 
 
template <class TElem> 
TNodoCola<TElem>::TNodoCola( const TElem& elem, TNodoCola<TElem>* sig ) : 
 _elem(elem), _sig(sig) { 
}; 
// O(TElem::TElem(TElem&)), O(1) sobre tipos predefinidos 
 
template <class TElem> 
const TElem& TNodoCola<TElem>::elem() const { 
 return _elem; 
} 
// O(1) 
 
template <class TElem> 
TNodoCola<TElem>* TNodoCola<TElem>::sig() const { 
 return _sig; 
} 
// O(1) 
 
 
 
Tipos abstractos de datos con estructura lineal 24 
 
// Constructoras, destructora y operador de asignación 
 
template <class TElem> 
TColaDinamica<TElem>::TColaDinamica( ) : 
 _prim(0), _ult(0) { 
}; 
// O(1) 
 
 
template <class TElem> 
void TColaDinamica<TElem>::libera() { 
 while (_prim != 0) { 
 TNodoCola<TElem>* tmp = _prim; 
 _prim = _prim->sig(); 
 delete tmp; 
 } 
}; 
// O(n * TElem::~TElem()), O(n) si sobre tipos predefinidos 
 
 
template <class TElem> 
void TColaDinamica<TElem>::copia(const TColaDinamica<TElem>& cola) { 
 if ( cola.esVacio() ) 
 _prim = _ult = 0; 
 else { 
 TNodoCola<TElem> *antCopia, *actCopia, *act; 
 act = cola._prim; 
 _prim = new TNodoCola<TElem>( act->elem(), 0 ); 
 actCopia = _prim; 
 while ( act->sig() != 0 ) { 
 act = act->sig(); 
 antCopia = actCopia; 
 actCopia = new TNodoCola<TElem>( act->elem(), 0 ); 
 antCopia->_sig = actCopia; 
 } 
 _ult = actCopia; 
 } 
}; 
// O(n’ * TElem::TElem(TElem&)), O(n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
Tipos abstractos de datos con estructura lineal 25 
 
template <class TElem> 
TColaDinamica<TElem>::TColaDinamica( const TColaDinamica<TElem>& cola ) { 
 copia(cola); 
}; 
// O(n’ * TElem::TElem(TElem&)), O(n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
template <class TElem> 
TColaDinamica<TElem>::~TColaDinamica( ) { 
 libera(); 
}; 
// O(n * TElem::~TElem()), O(n) sobre tipos predefinidos 
 
template <class TElem> 
TColaDinamica<TElem>& 
TColaDinamica<TElem>::operator=( const TColaDinamica<TElem>& cola ) { 
 if( this != &cola ) { 
 libera(); 
 copia(cola); 
 } 
 return *this; 
}; 
// O(n * TElem::~TElem() + n’ * TElem::TElem(TElem&) ), 
// O(n+n’) sobre tipos predefinidos 
// donde n’ es el número de elementos de ‘cola’ 
 
 
Tipos abstractos de datos con estructura lineal 26 
 
// operaciones de las colas 
 
template <class TElem> 
void TColaDinamica<TElem>::ponDetras(const TElem& elem) { 
 TNodoCola<TElem>* p = new TNodoCola<TElem>(elem); 
 if( _ult != 0 ) 
 _ult->_sig = p; 
 else 
 _prim = p; 
 _ult = p; 
}; 
// O(TElem::TElem(TElem&)), O(1) sobre tipos predefinidos 
 
template <class TElem> 
const TElem& TColaDinamica<TElem>::primero( ) const 
 throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el primero de la cola vacía"); 
 else 
 return _prim->elem(); 
}; 
// O(1) 
 
template <class TElem> 
void TColaDinamica<TElem>::quitaPrim( ) throw (EAccesoIndebido) { 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el primero de la cola vacía"); 
 else { 
 TNodoCola<TElem>* tmp = _prim; 
 _prim = _prim->sig(); 
 if( _prim == 0 ) 
 _ult = 0; 
 delete tmp; 
 } 
}; 
// O(TElem::~TElem()), O(1) sobre tipos predefinidos 
 
template <class TElem> 
bool TColaDinamica<TElem>::esVacio( ) const { 
 return _prim == 0; 
}; 
// O(1) 
 
Tipos abstractos de datos con estructura lineal 27 
 
4.3 Colas dobles 
 Son una generalización de las colas y las pilas, donde es posible insertar, con-
sultar y eliminar tanto por el principio como por el final. 
Especificación 
tad DCOLA [E :: ANY] 
 usa 
 BOOL 
 tipo 
 DCola[Elem] 
 operaciones 
 Nuevo: → DCola[Elem] /* gen */ 
 PonDetras: (Elem, DCola[Elem]) → DCola[Elem] /* gen */ 
 ponDelante: (Elem, DCola[Elem]) → DCola[Elem] /* mod */ 
 quitaUlt: DCola[Elem] – → DCola[Elem] /* mod */ 
 ultimo: DCola[Elem] – → Elem /* obs */ 
 quitaPrim: DCola[Elem] – → DCola[Elem] /* mod */ 
 primero: DCola[Elem] – → Elem /* obs */ 
 esVacio: DCola[Elem] → Bool /* obs */ 
 ecuaciones 
 ∀ x, y : Elem : ∀ xs : DCola[Elem] : 
 ponDelante(y, Nuevo) = PonDetras(y, Nuevo) 
 ponDelante( y, PonDetras(x, xs)) = PonDetras(x, ponDelante(y, xs)) 
 def quitaUlt(xs) si NOT esVacio(xs) 
 quitaUlt(PonDetras(x,xs)) = xs 
 def ultimo(xs) si NOT esVacio(xs) 
 ultimo(PonDetras(x, xs)) = x 
 def quitaPrim(xs) si NOT esVacio(xs) 
 quitaPrim(PonDetras(x, xs)) = Nuevo si esVacio(xs) 
 quitaPrim(PonDetras(x,xs)) = PonDetras(x,quitaPrim(xs)) si NOT esVacio(xs) 
 def primero(xs) si NOT esVacio(xs) 
 primero(PonDetras(x, xs)) = x si esVacio(xs) 
 primero(PonDetras(x, xs)) = primero(xs) si NOT esVacio(xs) 
 esVacio(Nuevo) = Cierto 
 esVacio(PonDetras(x, xs)) = Falso 
 errores 
 quitaUlt(Nuevo) 
 ultimo(Nuevo) 
 quitaPrim(Nuevo) 
 primero(Nuevo) 
ftad 
Tipos abstractos de datos con estructura lineal 28 
 
Implementación basada en un vector circular 
Tipo representante 
 Se utiliza la misma estructura de datos y el mismo invariante de la representa-
ción que en la implementación de las colas 
Implementación de las operaciones 
 Para implementar las operaciones de inserción y supresión debemos decidir 
cómo evolucionan los índices ini y fin. 
 
ini
ponDelante quitaUlt PonDetrasquitaPrim
fin 
 
 Observamos que las que tienen una operación análoga en las colas, se imple-
menta de la misma forma. 
 
COLA DCOLA 
Nuevo Nuevo 
PonDetras PonDetras 
quitaPrim quitaPrim 
primero primero 
esVacio esVacio 
 ponDelante 
 quitaUlt 
 ultimo 
 
Tipos abstractos de datos con estructura lineal 29 
 
 Es necesario modificar la constructora porque ahora la primera inserción pue-
de realizarse con ponDelante y no es válido el valor inicial _fin=–1. 
template <class TElem> 
TDColaEstatica<TElem>::TDColaEstatica( int capacidad ) : 
 _capacidad(capacidad), _ini(0), _fin(_capacidad-1), _longitud(0), 
 _espacio(new TElem[_capacidad]) { 
}; 
 
 El resto, se implementan de forma similar 
template <class TElem> 
void TDColaEstatica<TElem>::ponDelante(const TElem& elem) { 
// Pre: true 
 if( _longitud ==
_capacidad ) 
 amplia(); 
 if ( _ini == 0 ) 
 _ini = _capacidad - 1; 
 else 
 _ini--; 
 _longitud++; 
 _espacio[_ini] = elem; 
// Post: Se añade 'elem' al principio de la cola 
// Si _longitud < _capacidad 
// O( TElem::operator=(TElem&), O(1) sobre tipos predefinidos 
// Si _longitud == _capacidad 
// O( 2 * _capacidad * TElem::TElem() + 
// n * TElem::operator=(TElem&) + _capacidad * TElem::~TElem() ), 
// O(n) sobre tipos predefinidos 
}; 
 
template <class TElem> 
void TDColaEstatica<TElem>::amplia( ) { 
 _capacidad *= 2; 
 TElem* nuevo = new TElem[_capacidad]; 
 for( int i = 0; i < _longitud; i++ ) 
 nuevo[i] = _espacio[(_ini+i) % (_capacidad/2)]; 
 delete [] _espacio; 
 _espacio = nuevo; 
 _ini = 0; 
 _fin = _longitud-1; 
}; 
 
Tipos abstractos de datos con estructura lineal 30 
 
template <class TElem> 
const TElem& TDColaEstatica<TElem>::ultimo( ) const 
 throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía"); 
 return _espacio[_fin]; 
// Post: Devuelve el último elemento de la cola 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(1) 
}; 
 
template <class TElem> 
void TDColaEstatica<TElem>::quitaUlt( ) throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido( "Cola vacía" ); 
 if ( _fin == 0 ) 
 _fin = _capacidad - 1; 
 else 
 _fin--; 
 _longitud--; 
// Post: Elimina el último elemento de la cola 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(1) 
}; 
 
Tipos abstractos de datos con estructura lineal 31 
 
Implementación dinámica 
Tipo representante 
 Se utiliza la misma estructura de datos y el mismo invariante de la representa-
ción que en la implementación de las colas 
Implementación de las operaciones 
 Las que tienen una operación análoga en las colas, se implementa de la misma 
forma. ponDelante y ultimo se implementan de la forma obvia con complejidad 
O(1), pero no así quitaUlt. 
 Al eliminar el último nodo, es necesario hacer que ult pase a apuntar al penúl-
timo, pero para acceder a éste es necesario recorrer la lista enlazada, con lo que 
se obtiene una complejidad O(n). 
 
template <class TElem> 
void TDColaDinamica<TElem>::quitaUlt( ) throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido("Error al quitar el último de la cola vacía"); 
 else { 
 TNodoCola<TElem>* p = _prim; 
 if ( p == _ult ) { 
 _prim = _ult = 0; 
 delete p; 
 } 
 else { 
 while ( p->sig() != _ult ) 
 p = p->sig(); 
 p->_sig = 0; 
 delete _ult; 
 _ult = p; 
 } 
 } 
// Post: Elimina el último elemento de la cima 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(TElem::~TElem()), O(1) sobre tipos predefinidos 
}; 
Tipos abstractos de datos con estructura lineal 32 
 
Implementación dinámica con encadenamiento doble 
Tipo representante 
 La idea de la representación 
 
……
prim ult
xs
x2x1 xn-1 xn 
 
 
 Estructura de datos 
 
template <class TElem> 
class TNodoCola { 
 public: 
 friend TDColaDinamica<TElem>; 
 ... 
 
 private: 
 TElem _elem; 
 TNodoCola<TElem> *_sig, *_ant; 
 
 ... 
}; 
 
template <class TElem> 
class TDColaDinamica { 
 
 ... 
 
 private: 
 TNodoCola<TElem> *_prim, *_ult; 
 
 ... 
}; 
 
Tipos abstractos de datos con estructura lineal 33 
 
 Invariante de la representación 
Dada xs : TDColaDinamica 
 
R(xs) ⇔def RCDV(xs) ∨ RCDNV(xs) 
 
donde RCDV se refieren a las condiciones de una cola doble vacía y RCDNV a las 
de una que no está vacía 
 
 RCDV(xs) 
⇔def 
 xs._prim = 0 ∧ xs._ult = 0 
 
 
 
 RCDNV(xs) 
⇔def 
 xs._prim ≠ 0 ∧ xs._ult ≠ 0 ∧ 
 xs._prim->ant = 0 ∧ 
 buenaColaDoble(xs._prim, xs._ult) 
 
y el predicado buenaColaDoble por su parte 
 
 buenaColaDoble(p,q) 
⇔def 
 (p = q ∧ ubicado(p) ∧ R(p->_elem) ∧ p->_sig = 0) ∨ 
 (p ≠ q ∧ ubicado(p) ∧ R(p->_elem) ∧ 
 p->_sig->_ant = p ∧ 
 p ∉ cadena(p->_sig) ∧ buenaColaDoble(p->_sig, q)) 
 
cadena(p) =def ∅ si p = nil 
cadena(p) =def {p} ∪ cadena(p->_sig) si p ≠ nil 
 
Tipos abstractos de datos con estructura lineal 34 
 
Implementación de las operaciones 
 
 La clase de los nodos 
 
template <class TElem> 
TNodoCola<TElem>::TNodoCola( const TElem& elem, TNodoCola<TElem>* sig, 
 TNodoCola<TElem>* ant ) : 
 _elem(elem), _sig(sig), _ant(ant) { 
// O(TElem::TElem(TElem&), O(1) sobre tipos predefinidos 
}; 
 
template <class TElem> 
const TElem& TNodoCola<TElem>::elem() const { 
 return _elem; 
// O(1) 
} 
 
template <class TElem> 
TNodoCola<TElem>* TNodoCola<TElem>::sig() const { 
 return _sig; 
//O(1) 
} 
 
template <class TElem> 
TNodoCola<TElem>* TNodoCola<TElem>::ant() const { 
 return _ant; 
//O(1) 
} 
 
 
Tipos abstractos de datos con estructura lineal 35 
 
 Las operaciones de las colas dobles 
 
template <class TElem> 
TDColaDinamica<TElem>::TDColaDinamica( ) : 
 _prim(0), _ult(0) { 
}; 
 
 
// constructora de copia, destructora y operador de asignación 
// igual que siempre 
 
// operaciones privadas de copia y anulación 
 
template <class TElem> 
void TDColaDinamica<TElem>::libera() { 
 while (_prim != 0) { 
 TNodoCola<TElem>* tmp = _prim; 
 _prim = _prim->sig(); 
 delete tmp; 
 } 
// O(n * TElem::~TElem()), O(n) sobre tipos predefinidos 
}; 
 
template <class TElem> 
void TDColaDinamica<TElem>::copia(const TDColaDinamica<TElem>& cola) { 
 if ( cola.esVacio() ) 
 _prim = _ult = 0; 
 else { 
 TNodoCola<TElem> *antCopia, *actCopia, *act; 
 act = cola._prim; 
 _prim = new TNodoCola<TElem>( act->elem(), 0 ); 
 actCopia = _prim; 
 while ( act->sig() != 0 ) { 
 act = act->sig(); 
 antCopia = actCopia; 
 actCopia = new TNodoCola<TElem>( act->elem(), 0, antCopia ); 
 antCopia->_sig = actCopia; 
 } 
 _ult = actCopia; 
 } 
// O(n' * TElem::TElem(TElem&)), O(n') sobre tipos predefinidos 
// donde n' es el número de elementos de la cola a copiar 
}; 
Tipos abstractos de datos con estructura lineal 36 
 
template <class TElem> 
void TDColaDinamica<TElem>::ponDetras(const TElem& elem) { 
// Pre: true 
 TNodoCola<TElem>* p = new TNodoCola<TElem>(elem); 
 if( esVacio() ) 
 _prim = _ult = p; 
 else { 
 _ult->_sig = p; 
 p->_ant = _ult; 
 _ult = p; 
 } 
// Post: Se añade 'elem' al final de la cola 
// O(TElem::TElem(TElem&)), O(1) sobre tipos predefinidos 
}; 
 
template <class TElem> 
void TDColaDinamica<TElem>::ponDelante(const TElem& elem) { 
// Pre: true 
 TNodoCola<TElem>* p = new TNodoCola<TElem>(elem); 
 if( esVacio() ) 
 _prim = _ult = p; 
 else { 
 _prim->_ant = p; 
 p->_sig = _prim; 
 _prim = p; 
 } 
// Post: Se añade 'elem' al principio de la cola 
// O(TElem::TElem(TElem&)), O(1) sobre tipos predefinidos 
}; 
 
template <class TElem> 
const TElem& TDColaDinamica<TElem>::primero( ) const 
 throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el primero de la cola vacía"); 
 else 
 return _prim->elem(); 
// Post: Devuelve el primer elemento de la cola 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(1) 
}; 
 
Tipos abstractos de datos con estructura lineal 37 
 
template <class TElem> 
const TElem& TDColaDinamica<TElem>::ultimo( ) const 
 throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el último de la cola vacía"); 
 else 
 return _ult->elem(); 
// Post: Devuelve el último elemento de la cola 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(1) 
}; 
 
 
template <class TElem> 
void TDColaDinamica<TElem>::quitaPrim( ) throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el primero de la cola vacía"); 
 else {
TNodoCola<TElem>* tmp = _prim; 
 _prim = _prim->sig(); 
 if( _prim == 0 ) 
 _ult = 0; 
 else 
 _prim->_ant = 0; 
 delete tmp; 
 } 
// Post: Elimina el primer elemento de la cima 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(TElem::~TElem()), O(1) sobre tipos predefinidos 
}; 
 
Tipos abstractos de datos con estructura lineal 38 
 
template <class TElem> 
void TDColaDinamica<TElem>::quitaUlt( ) throw (EAccesoIndebido) { 
// Pre: ! esVacio( ) 
 if( esVacio() ) 
 throw EAccesoIndebido("Error: no existe el último de la cola vacía"); 
 else { 
 TNodoCola<TElem>* tmp = _ult; 
 _ult = _ult->ant(); 
 if ( _ult == 0 ) 
 _prim = 0; 
 else 
 _ult->_sig = 0; 
 delete tmp; 
 } 
// Post: Elimina el último elemento de la cima 
// Lanza la excepción EAccesoIndebido si la cola está vacía 
// O(TElem::~TElem()), O(1) sobre tipos predefinidos 
}; 
 
template <class TElem> 
bool TDColaDinamica<TElem>::esVacio( ) const { 
// Pre: true 
 return _prim == 0; 
// Post: Devuelve true | false según si la pila está o no vacía 
// O(1) 
}; 
 
 
 
 
 
 
Tipos abstractos de datos con estructura lineal 39 
 
4.4 Listas 
 
 Es un TAD que representa a una colección de elementos de un mismo tipo 
que generaliza a las colas dobles incluyendo algunas operaciones adicionales 
 
Operaciones de LISTA Operaciones de DCOLA 
Nuevo Nuevo 
PonDelante ponDelante 
ponDetras PonDetras 
primero primero 
quitaPrim quitaPrim 
último último 
quitaUlt quitaUlt 
concatena –– 
esVacio esVacio 
numElem –– 
elemEn –– 
 
 
— concatena construye una lista a partir de dos 
— numElem devuelve el número de elementos de la lista 
— elemEn permite acceder al elemento i-ésimo de una lista. 
 
 Debido a estas similitudes, las implementaciones que consideraremos serán 
similares a las estudiadas para las colas dobles. 
Tipos abstractos de datos con estructura lineal 40 
 
Especificación 
 
tad LISTA[E :: ANY] 
 usa 
 BOOL, NAT 
 tipo 
 Lista[Elem] 
 operaciones 
 Nuevo: → Lista[Elem] /* gen */ 
 PonDelante: (Elem, Lista[Elem]) → Lista[Elem] /* gen */ 
 ponDetras: (Lista[Elem], Elem) → Lista[Elem] /* mod */ 
 primero: Lista[Elem] – → Elem /* obs */ 
 quitaPrim: Lista[Elem] – → Lista[Elem] /* mod */ 
 último: Lista[Elem] – → Elem /* obs */ 
 quitaUlt: Lista[Elem] – → Lista[Elem] /* mod */ 
 concatena: (Lista[Elem], Lista[Elem]) → Lista[Elem] /* mod */ 
 esVacio: Lista[Elem] → Bool /* obs */ 
 numElem: Lista[Elem] → Nat /* obs */ 
 elemEn: (Lista[Elem], Nat) – → Elem /* obs */ 
 ecuaciones 
 ∀ x, y: Elem : ∀ xs, ys : Lista[Elem] : ∀ n : Nat : 
 concatena(Nuevo, ys) = ys 
 concatena(PonDelante(x, xs), ys) = PonDelante(x, concatena(xs, ys)) 
 ponDetras(xs, x) = concatena(xs, PonDelante(x, Nuevo)) 
 esVacio(Nuevo) = Cierto 
 esVacio(PonDelante(x, xs)) = Falso 
 def primero(xs) si NOT esVacio(xs) 
 primero(PonDelante(x, xs)) = x 
 def quitaPrim(xs) si NOT esVacio(xs) 
 quitaPrim(PonDelante(x, xs)) = xs 
 def último(xs) si NOT esVacio(xs) 
 último(PonDelante(x, xs)) = x si esVacio(xs) 
 último(PonDelante(x, xs)) = último(xs) si NOT esVacio(xs) 
 def quitaUlt(xs) si NOT esVacio(xs) 
 quitaUlt(PonDelante(x, xs)) = Nuevo si esVacio(xs) 
 quitaUlt(PonDelante(x, xs)) = PonDelante(x, quitaUlt(xs)) 
 si NOT esVacio(xs) 
 numElem(Nuevo) = Cero 
 numElem(PonDelante(x, xs)) = Suc(numElem(xs)) 
 def elemEn(xs, n) si Suc(Cero) ≤ n ≤ numElem(xs) 
 elemEn(PonDelante(x, xs), Suc(Cero)) = x 
 elemEn(PonDelante(x, PonDelante(y, ys)), Suc(Suc(n)) 
 =f elemEn(PonDelante(y,ys), Suc(n)) 
Tipos abstractos de datos con estructura lineal 41 
 
 errores 
 primero(Nuevo) 
 quitaPrim(Nuevo) 
 último(Nuevo) 
 quitaUlt(Nuevo) 
 elemEn(xs, n) si (n == Cero) OR (n > (numElem xs)) 
ftad 
Implementación estática basada en un vector circular 
Tipo representante 
 Se utiliza la misma estructura de datos y el mismo invariante de la representa-
ción que en la implementación de las colas dobles. 
Implementación de las operaciones 
 Las que tienen un equivalente en las colas dobles se implementan exactamente 
de la misma forma, de forma que sólo resta implementar concatena, numElem y 
elemEn. 
 
template <class TElem> 
void TListaEstatica<TElem>::concatena( const TListaEstatica<TElem>& lista ) 
{ 
// Pre: true 
 if( _longitud + lista._longitud > _capacidad ) 
 amplia( _longitud + lista._longitud ); 
 for ( int i = 0; i < lista._longitud; i++ ) { 
 _fin = ( _fin + 1 ) % _capacidad; 
 _espacio[_fin] = lista._espacio[ (lista._ini + i) % lista._capacidad ]; 
 } 
 _longitud += lista._longitud; 
// Post: concatena la lista pasada como parámetro, dejándola vacía 
// Si _longitud + lista._longitud <= _capacidad 
// O( n'*TElem::operator=(TElem&) ), O(n') sobre tipos predefinidos 
// Si _longitud + lista._longitud > _capacidad 
// O( _longitud + lista._longitud * TElem::TElem() + 
// n * TElem::operator=(TElem&) + _capacidad * TElem::~TElem() + 
// n'*TElem::operator=(TElem&) ), 
// O(n+n') sobre tipos predefinidos 
// siendo n' el número de elementos de la lista pasada como parámetro 
} 
Tipos abstractos de datos con estructura lineal 42 
 
Es necesario cambiar la implementación de amplia para que reciba la nueva 
longitud como parámetro. 
 
template <class TElem> 
void TListaEstatica<TElem>::amplia( int longitud ) { 
// Pre: longitud >= _capacidad 
 TElem* nuevo = new TElem[longitud]; 
 for( int i = 0; i < _longitud; i++ ) 
 nuevo[i] = _espacio[(_ini+i) % (_capacidad)]; 
 delete [] _espacio; 
 _capacidad = longitud; 
 _espacio = nuevo; 
 _ini = 0; 
 _fin = _longitud-1; 
// Post: _espacio es una array de 'longitud' elementos 
// los elementos del valor inicial de _espacio ocupan 
// las posiciones 0 .. _ini + _longitud - 1 
// O( longitud * TElem::TElem() + 
// n * TElem::operator=(TElem&) + 
// _capacidad * TElem::~TElem() ), 
// O(n) sobre tipos predefinidos 
}; 
 
 Las operaciones numElem y elemEn se implementan de forma trivial. 
 
template <class TElem> 
int TListaEstatica<TElem>::numElem( ) const { 
// Pre: true 
 return _longitud; 
// Post: Devuelve el número de elementos de la lista 
// O(1) 
} 
 
template <class TElem> 
const TElem& TListaEstatica<TElem>::elemEn( int n ) const 
 throw (EAccesoIndebido) { 
// Pre: 1 <= n <= numElem() 
 if ( ( n < 1 ) || ( n > numElem() ) ) 
 throw EAccesoIndebido("Error: posición inexistente"); 
 return _espacio[ (_ini + n – 1) % _capacidad ]; 
// Post: Devuelve el elemento en la posición n 
// O(1) 
} 
Tipos abstractos de datos con estructura lineal 43 
 
Implementación dinámica con encadenamiento doble 
Tipo representante 
 
 La estructura de datos es la misma que se utiliza en las colas dobles excepto 
porque se guarda la longitud, para así poder implementar numElem con com-
plejidad O(1). 
 
template <class TElem> 
class TNodoLista { 
 public: 
 friend TListaDinamica<TElem>; 
 ... 
 
 private: 
 TElem _elem; 
 TNodoCola<TElem> *_sig, *_ant; 
 
 ... 
}; 
 
template <class TElem> 
class TListaDinamica { 
 
 ... 
 
 private: 
 TNodoLista<TElem> *_prim, *_ult; 
 int _longitud; 
 
 ... 
}; 
 
 El invariante de la representación es el mismo que en las colas dobles, aña-
diendo la condición referente al campo longitud: 
 
R(xs) ⇔def (RCDV(xs) ∨ RCDNV(xs)) ∧ xs._longitud = longitud(xs._prim) 
 
longitud(p) =def 0 si p = 0 
longitud(p) =def 1 + longitud(p->_sig) si p ≠ 0 
 
Tipos abstractos de datos con estructura lineal 44 
 
Implementación de las operaciones 
 
 Para las operaciones que tienen una operación análoga en las colas dobles, po-
demos utilizar la implementación correspondiente, añadiendo las asignaciones 
necesarias para que se cumpla la parte del invariante de la representación