CIMEC Algoritmos y Estructuras de Datos
CIMEC Algoritmos y Estructuras de Datos
CIMEC Algoritmos y Estructuras de Datos
Pucheta (Contents-prev-up-next)
Contents
Dictado
Diseño y análisis de
algoritmos
Estrategias
• Existe una estrategia (trivial) que consiste en evaluar todos los caminos
posibles. Pero esta estrategia de “búsqueda exhaustiva” tiene un gran
defecto, el costo computacional crece de tal manera con el número de
ciudades que deja de ser aplicable a partir de una cantidad relativamente
pequeña.
• Otra estrategia “heurı́stica” se basa en buscar un camino que, si bien no
es el óptimo (el de menor recorrido sobre todos los posibles) puede ser
relativamente bueno en la mayorı́a de los casos prácticos. Por ejemplo,
empezar en una ciudad e ir a la más cercana que no haya sido aún
visitada hasta recorrerlas todas.
Algoritmo
Consideremos un sistema de
procesamiento con varios T0 T4 T6 T9 T2
procesadores que acceden a
un área de memoria O0 O1 O2 O3 O4
compartida. En memoria hay
una serie de objetos O0 , O1 , ... T1 T3 T5
On−1 , con n = 10 y una serie
de tareas a realizar T0 , T1 , ... O5 O6 O7 O8 O9
Tm−1 con m = 12. Cada tarea
debe modificar un cierto
subconjunto de los objetos, T7 T8 T10 T11
según la figura
Las tareas pueden realizarse en cualquier orden, pero dos tareas no pueden
ejecutarse al mismo tiempo si acceden al mismo objeto, ya que los cambios
hechos por una de ellas puede interferir con los cambios hechos por la otra.
Debe entonces desarrollarse un sistema que sincronice entre sı́ la ejecución
de las diferentes tareas.
Una forma trivial de sincronización es ejecutar cada una de las tareas en
forma secuencial. Primero la tarea T0 luego la T1 y ası́ siguiendo hasta la T11 ,
de esta forma nos aseguramos que no hay conflictos en el acceso a los
objetos.
Sin embargo, esta solución puede estar muy lejos de ser óptima en cuanto al
tiempo de ejecución ya que por ejemplo T0 y T1 pueden ejecutarse al mismo
tiempo en diferentes procesadores ya que modifican diferentes objetos. Si
asumimos, para simplificar, que todas las tareas llevan el mismo tiempo de
ejecución τ , entonces la versión trivial consume una cantidad de tiempo mτ ,
mientras que ejecutando las tareas T0 y T1 al mismo tiempo reducimos el
tiempo de ejecución a (m − 1)τ .
• Cada tarea debe estar en una y sólo una etapa. (De lo contrario la tarea no
se realizarı́a o se realizarı́a más de una vez, lo cual es redundante. En el
lenguaje de la teorı́a de conjuntos, estamos diciendo que debemos
particionar el conjunto de etapas en un cierto número de subconjuntos
“disjuntos”.)
f c 1 0 0 1 1 1
c d 1 1 1 0 1 1
e e 1 0 1 1 0 0
d f 0 0 1 1 0 0
f c 1 0 0 1 1 1
c d 1 1 1 0 1 1
e e 1 0 1 1 0 0
d f 0 0 1 1 0 0
T0 T1 T2
T3 T4 T5
R R G
T0 T1 T2
La buena noticia es que nuestro problema de
particionar el grafo ha sido muy estudiado en B
R Y
la teorı́a de grafos y se llama el problema de T3 T4 T5
“colorear” el grafo, es decir se representan
gráficamente las etapas asignándole colores a
G G
los vértices del grafo. La mala noticia es que B
T6 T7 T8
se ha encontrado que obtener el coloreado
óptimo (es decir el coloreado admisible con la
B Y
menor cantidad de colores posibles) resulta R
ser un problema extremadamente costoso en T9 T10 T11
cuanto a tiempo de cálculo.
Probar si el grafo se puede colorear con 1 solo color (esto sólo es posible si
no hay ninguna arista en el grafo). Si esto es posible el problema está
resuelto (no puede haber coloraciones con menos de un color). Si no es
posible entonces generamos todas las coloraciones con 2 colores, para cada
una de ellas verificamos si satisface las restricciones o no, es decir si es
admisible. Si lo es, el problema está resuelto: encontramos una coloración
admisible con dos colores y ya verificamos que con 1 solo color no es
posible. Si no encontramos ninguna coloración admisible de 2 colores
entonces probamos con las de 3 colores y ası́ sucesivamente. Si
encontramos una coloración de nc colores entonces será óptima, ya que
previamente verificamos para cada número de colores entre 1 y nc − 1 que
no habı́a ninguna coloración admisible.
• Da la solución óptima.
• Termina en un número finito de pasos ya que a lo sumo puede haber
nc = nv colores, es decir la coloración que consiste en asignar a cada
vértice un color diferente es siempre admisible.
Para nc = 1 es trivial, hay una sola coloración donde todos los vértices
tienen el mismo color, es decir N (nc = 1, nv ) = 1 para cualquier nv .
nv=3
N=8
nc=2
R R R
nv=2 a b c
N=4 a
R G
c
R
nc=2 b
R R R
R R +c aG b c
a b G G R
R G a b c
a b
R G
a G
b +c
R R G
Coloraciones de 3 aG b G a b c
objetos con 2 colores R G G
a b c
N (2, 3) = 2 N (2, 2) a G b R cG
N (nc , nv ) = nc N (nc , nv − 1) G G G
a b c
= nnc v −1 N (nc , 1)
Esto cierra con la última pregunta, ya que vemos que el número de pasos
para cada uno de los colores es finito, y hay a lo sumo nv colores de manera
que el número total de posibles coloraciones a verificar es finito. Notar de
paso que esta forma de contar las coloraciones es también “constructiva”, da
un procedimiento para generar todas las coloraciones, si uno estuviera
decidido a implementar la estrategia de búsqueda exhaustiva.
coloraciones en R R G
realidad tienen a b c
menos de 3 R G R
colores. a b c
• Otras en realidad R G G
son esencialmente a b c =
la misma un solo color G R R = =
coloración, ya que a b c
corresponden a G R G
intercambiar a b c
colores entre sı́ G G R
(por ejemplo rojo a b c
con verde). G G G
a b c
El crecimiento de la función nn v
v con el número de vértices es tan rápido que
hasta puede generar asombro. Consideremos el tiempo que tarda una
computadora personal tı́pica en evaluar todas las posibilidades para nv = 20
vértices. Tomando un procesador de 2.4 GHz (un procesador tı́pico al
momento de escribir este apunte) y asumiendo que podemos escribir un
programa tan eficiente que puede evaluar una arista por cada ciclo del
procesador (en la práctica esto es imposible y al menos necesitaremos unas
decenas de ciclos para evaluar una coloración) el tiempo en años necesario
para evaluar todas las coloraciones es de
2022 11
T = 9
= 5.54×10 años
2.4×10 . 3600 . 24 . 365
Esto es unas 40 veces la edad del universo (estimada en 15.000.000.000 de
años).
coloraciones
Eliminando las coloraciones nv coloraciones
redundantes se obtiene una diferentes
gran reducción en el número
de coloraciones a evaluar. 1 1 1
n /2+2
Nbem ≈ nv v . Esto 2 4 2
significa una gran mejora con
respecto a nn v
v , para 3 27 5
20 vértices pasamos de
4.2×1028 coloraciones a 4 256 15
5.2×1013 coloraciones y el 5 3125 52
tiempo de cómputo se reduce
a sólo 99 dı́as. Está claro que 6 46656 203
todavı́a resulta ser excesivo
para un uso práctico. 7 823543 877
Esta aproximación es llamada ávida ya que asigna colores tan rápido como lo
puede hacer, sin tener en cuenta las posibles consecuencias negativas de tal
acción. Si estuviéramos escribiendo un programa para jugar al ajedrez,
entonces una estrategia ávida, serı́a evaluar todas las posibles jugadas y
elegir la que da la mejor ventaja material. En realidad no se puede catalogar a
los algoritmos como ávidos en forma absoluta, sino que se debe hacer en
forma comparativa: hay algoritmos más ávidos que otros. En general cuanto
más ávido es un algoritmo más simple es y más rápido es en cuanto a
avanzar para resolver el problema, pero por otra parte explora en menor
medida el espacio de búsqueda y por lo tanto puede dar una solución peor
que otro menos ávido. Volviendo al ejemplo del ajedrez, un programa que,
además de evaluar la ganancia material de la jugada a realizar, evalúe las
posibles consecuencias de la siguiente jugada del oponente requerirá mayor
tiempo pero a largo plazo producirá mejores resultados.
El algoritmo encuentra una solución con tres colores, sin embargo se puede
encontrar una solución con dos colores. Esta última es óptima ya que una
mejor deberı́a tener sólo un color, pero esto es imposible ya que entonces no
podrı́a haber ninguna arista en el grafo. Este ejemplo ilustra perfectamente
que si bien el algoritmo ávido da una solución razonable, ésta puede no ser la
óptima.
Notemos también que la coloración
producida por el algoritmo ávido c
depende del orden en el que se
recorren los vértices. En el caso
previo, si recorriéramos los nodos en a e b
el orden {a, e, c, d, b}, obtendrı́amos
la coloración óptima.
d
18 if (!adyacente) {
19 // marcar a ‘*q’ como coloreado
20 tabla-color[*q] = color;
21 // agregar ‘*q’ a ‘nuevo-color’
22 nuevo-color.insert(*q);
23 }
24 }
25 }
22 }
21 }
22 }
Tiempos de ejecución de
un programa
• En tal módulo tal vez sea mejor preocuparse por la robustez y sencillez de
programación que por la eficiencia.
Para fijar ideas, pensemos en un programa que ordena de menor a mayor una
serie de números enteros. El tiempo de ejecución de un programa depende
de:
T (n) = cn (3)
Notación asintótica
La idea es que no nos interesa cmo se comporta la función T (n) para valores
de n pequeños sino sólo la tendencia para n → ∞.
180
T(n)
160
140
120 2n2
c
100
80
n0 (n+1) 2
60
40
20
0
1 2 3 4 5 6 7 8 9 10
n
Para ver que esta relación es válida para todos los valores de n tales que
n ≥ 3, entonces debemos recurrir a un poco de álgebra.
n ≥ 3,
n − 1 ≥ 2,
(n − 1)2 ≥ 4,
n2 − 2n + 1 ≥ 4, (6)
n2 ≥ 3 + 2n,
2n2 ≥ 3 + 2n + n2
2n2 ≥ (n + 1)2 + 2 ≥ (n + 1)2
La notación O(...) puede usarse con otras funciones, es decir O(n3 ), O(2n ),
O(log n). En general decimos que T (n) = O(f (n)) (se lee “T (n) es orden
f (n)”) si existen constantes c, n0 > 0 tales que
T (n) ≤ c f (n), para n ≥ n0 (7)
Es decir si
100 ; para n < 10,
T1 (n) = (9)
(n + 1)2 ; para n ≥ 10,
700
600
500
T (n) y T1 (n) sólo 2n 2
400 n0
difieren en un número
300 T1 (n)
finito de puntos (los
200
valores de n < 10)
100
0
0 2 4 6 8 10 12 14 16 18 20
n
T(n)=(n+1)2
Demostración: Esto puede verse ya que si T (n) < 2n2 para n ≥ 3 (como se
vio en el ejemplo citado), entonces T1 (n) < 2n2 para n > n00 = 10.
Transitividad
Regla de la suma
Cualquier función puede ser utilizada como tasa de crecimiento, pero las más
usuales son, en orden de crecimiento
√
1 < log n < n < n < n2 < ... < np < 2n < 3n < · · · < n! < nn (10)
√
• Aquı́ “<” representa O(...). Es decir, 1 = O(log n), log n = O( n), ...
• La función logaritmo crece menos que cualquier potencia nα con α > 1.
• Los logaritmos en diferente base son equivalentes entre sı́ por la bien
conocida relación
logb n = logb a loga n, (11)
de manera que en muchas expresiones con logaritmos no es importante
la base utilizada. En computación cientı́fica es muy común que aparezcan
expresiones con logaritmos en base 2.
La función factorial
Una función que aparece muy comúnmente en problemas combinatorios es la
función factorial n!. Para poder comparar esta función con otras para
grandes valores de n es conveniente usar la “aproximación de Stirling”
√ 1
n! ∼ 2π nn+ /2 e−n (12)
n! = O(nn ) (13)
y
an = O(n!), (14)
Para T (n) = (n + 1)2 podemos decir que T (n) = O(n2 ) ó T (n) = O(n3 ).
n2 < n3 . En general
debemos tomar la
“menor” de todas las
cotas posibles. n
O(n8 )
z }| {
T (n) = (3n3 + 2n2 + 6) n5 + 2n + 16n!
El término que gobierna es el último de manera que
T (n) = O(n!)
Muchas veces los diferentes términos que aparecen en la expresión para el
tiempo de ejecución corresponde a diferentes partes del programa, de
manera que, como ganancia adicional, la notación asintótica nos indica
cuáles son las partes del programa que requieren más tiempo de cálculo y,
por lo tanto, deben ser eventualmente optimizadas.
multiplicativa resultan
ser simplemente
0.1
desplazadas según la 100 1000
n 10000
Algoritmos P y NP
Si bien para ciertos problemas (como el de colorear grafos) no se conocen
algoritmos con tiempo de ejecución polinomial, es muy difı́cil demostrar que
realmente es ası́, es decir que para ese problema no existe ningún algoritmo
de complejidad polinomial.
Para ser más precisos hay que introducir una serie de conceptos nuevos. Por
empezar, cuando se habla de tiempos de ejecución se refiere a instrucciones
realizadas en una “máquina de Turing”, que es una abstracción de la
computadora más simple posible, con un juego de instrucciones reducido.
Una “máquina de Turing no determinı́stica” es una máquina de Turing que en
cada paso puede invocar un cierto número de instrucciones, y no una sola
instruccion como es el caso de la máquina de Turing determinı́stica. En cierta
forma, es como si una máquina de Turing no-determinı́stica pudiera invocar
otras series de máquinas de Turing, de manera que en vez de tener un
“camino de cómputo”, como es usual en una computadora secuencial,
tenemos un “árbol de cómputo”.
Conteo de operaciones
Bloques if
if(<cond>) {
<body>
}
podemos o bien considerar el peor caso, asumiendo que <body> se ejecuta
siempre
Tpeor = Tcond + Tbody
o, en el caso promedio, calcular la probabilidad P de que <cond> de
verdadero. En ese caso
Bloques if (cont.)
if(<cond>) {
<body-true>
} else {
<body-false>
}
podemos considerar,
Lazos
Lazos (cont.)
N
X −1
T = Tini + (Tbody,i + Tinc + Tstop ).
i=0
Algunas veces es difı́cil calcular una expresión analı́tica para tales sumas. Si
podemos determinar una cierta tasa de crecimiento para todos los términos
entonces,
N −1
T ≤ N max(Tbody,i + Tinc + Tstop ) = O(N f (n))
i=1
Lazos (cont.)
Más difı́cil aún es el caso en que el número de veces que se ejecuta el lazo no
se conoce a priori, por ejemplo un lazo while como el siguiente
while (<cond>) {
<body>
}
En este caso debemos determinar también el número de veces que se
ejecutará el lazo.
Ej: Bubble-sort
13 1 9 3 2 5 18
13 1 9 3 2 5 18
=burbuja
13 1 9 3 2 5 18 j=0
=ya ordenado 13 1 9 2 3 5 18
13 1 2 9 3 5 18
13 1 2 9 3 5 18
1 13 2 9 3 5 18
1 13 2 9 3 5 18
1 13 2 9 3 5 18 j=1
...
...
...
1 2 13 3 9 5 18
1 2 3 13 5 9 18 j=2
1 2 3 5 13 9 18
...
1 2 3 5 9 13 18
1 2 3 5 9 13 18 j=5
T = c3 + (n − j − 1) c2
n−2
X
T = c4 + (n − 1)(c3 + c5 + c6 ) + c2 (n − j − 1)
j=0
n−2
X n
X
(n − j − 1) = (n − 1) + (n − 2) + ... + 1 = j − n.
j=0 1
0 1 1
111
000 0 j=1
11
0 0
1
0
1 0
1
01
1
111
00000
1 1 0000000000
1111111111000
111
000000
111111 1
00
000
111 0000000000
1111111111000
111
000000
111111 0
1
0
1
000
111 0000000000
1111111111000
111
000000
111111
Consideremos:
0001
111
000
111
0000000000
1111111111
000
111
0000000000
1111111111
000
111
n/2 111
000 000 1
111 0
0
1
000
111000
111 1
0 0000000000
1111111111
000
111
0
1 0000
1111 0
1
0
1
000
111000
111 0000000000
1111111111
000
111
0
1 0000
1111
0000000000
1111111111 0
1
n 000
111000 =
111 000
111
0
1 0000
1111
0000000000
1111111111 0
1
000
111000
111 000
111
0
1 0000
1111
000
111
0000000000
1111111111 0
1
000
111000
111000
111 0
1 000
111
X
j= 000
111000
111000
111 0000000000
1111111111
0
1 000
111 0
1
0
1
n
000
111000
111000
111 0000000000
1111111111
0
1 000
111 0
1
j=1 000
111 000
111000
111000
111 11
00 0000000000
1111111111
0000
1111
0
1 000
111
0000000000
1111111111 0
1
000
111 000
111000
111000
111 0
1
0000
1111
0
10000000000
1111111111 0
1
000
111 000
111000
111000
111 0000
1111
0
10000000000
1111111111
11111 0
1
000
111 000
111000
111000 00
111 0000
01111111111
10000000000
0000
1111 0
1
000
111 000
111000
111000 11
111 0
1
01111111111
10000000000 1
00
1
j=1 2 3 4 0
1
0
1
2 n /2
n2 n n(n + 1)
= + =
2 2 2
O(n2 )
O(1) O(n)
}| { z
z}|{ z }| { n(n − 1)
T (n) = c4 + (n − 1)(c3 + c5 + c6 ) + c2
2
n2
= O( )
2
Llamadas a rutinas
• Calculados los tiempos de
rutinas que no llaman a
otras rutinas (S0 ),
calculamos el de aquellas
rutinas que sólo llaman a
rutinas de S0 (llamémoslas
S1 ).
S3
• Se asigna a las lı́neas con main( )
llamadas a rutinas de S0
de acuerdo con el tiempo S2
de ejecución previamente sub1( ) sub2( ) sub3( )
calculado, como si fuera
una instrucción más del
lenguaje. Por ejemplo S1
sub4( ) sub5( ) sub1( )
podemos usar a bubble
como una instrucción cuyo
S0
costo es O(n2 ). sub4( )
Llamadas recursivas
a[0] j1 p j2 a[n]
c ; si m = 1;
T (m) =
d + T (m/2) ; si m > 1;
T (2) = d + T (1) = d + c
T (4) = d + T (2) = 2d + c
T (8) = d + T (4) = 3d + c
..
.
T (2p ) = d + T (2p−1 ) = pd + c
como p = log2 m, vemos que
T (m) = d log2 m + c = O(log2 m)
T (2) = d + 2T (1) = d + 2c
T (4) = d + 2T (2) = d + 2d + 4c = 3d + 4c
T (8) = d + 2T (4) = d + 6d + 8c = 7d + 8c
..
.
Notar entonces que si bien las dos cotas (por el máximo y por la suma) son
válidas, la del máximo da una mejor estimación (más baja) en este caso.
Tipos de datos
abstractos
fundamentales
operaciones abstractas
del TAD
abstracción
interfase
implementación
1 template<class T>
2 class set {
3 public:
4 class iterator { /* . . . */ };
5 void insert(T x);
6 void erase(iterator p);
7 void erase(T x);
8 iterator find(T x);
9 iterator begin();
10 iterator end();
11 };
Descripción de la interfase
2 // Pone elementos en A y B
3 // . . .
4 C.insert(A.begin(),A.end());
5 C.insert(B.begin(),B.end());
En este capı́tulo se estudiarán varios tipos de datos básicos. Para cada uno
de estos TAD se discutirán en el siguiente orden
El TAD lista
L=( 1 3 2 5 6 3 8 2 6 3 )
p q
eliminar
L=( 1 3 2 5 6 8 2 6 )
q
p
L = (1, 3, 5, 7)
suprime elemento en la posición 2 (17)
→ L = (1, 3, 7)
Notar que esta operación es, en cierta forma, la inversa de insertar. Si,
como en el ejemplo anterior, insertamos un elemento en la posición i y
después suprimimos en esa misma posición, entonces la lista queda
inalterada. Notar que sólo es válido suprimir en las posiciones
dereferenciables.
Posiciones abstractas
1 class iterator-t { /* . . . */ };
2
3 class list {
4 private:
5 // . . .
6 public:
7 // . . .
8 iterator-t insert(iterator-t p,elem-t x);
9 iterator-t erase(iterator-t p);
10 elem-t & retrieve(iterator-t p);
11 iterator-t next(iterator-t p);
12 iterator-t begin();
13 iterator-t end();
14 }
Lo correcto es
Lo correcto es
Por ejemplo,
1 iterator-t p,q,r;
2 list L;
3 elem-t x,y,z;
4 //. . .
5 // p es una posicion dereferenciable
6 q = L.next(p);
7 r = L.end();
8 L.erase(p);
9 x = L.retrieve(p); // incorrecto
10 y = L.retrieve(q); // incorrecto
11 L.insert(r,z); // incorrecto
ya que p,q,r ya no son válidos (están después de la posición borrada p).
. Copiar:
q = p;
. Comparar: Notar que sólo se puede comparar por igualdad o desigualdad,
no por operadores de comparación, como < ó >.
q == p
r != L.end();
Usando punteros:
1 int *min(int *v,int n) {
2 int x = v[0];
3 int jmin = 0;
4 for (int k=1; k<n; k++) {
5 if (v[k]<x) {
6 jmin = k;
7 x = v[jmin];
8 }
9 }
10 return &v[jmin];
11 }
12
13 void print(int *v,int n) {
14 cout << "Vector: (";
15 for (int j=0; j<n; j++) cout << v[j] << " ";
16 cout << "), valor minimo: " << *min(v,n) << endl;
17 }
18
19 int main() {
20 int v[ ] = {6,5,1,4,2,3};
21 int n = 6;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 131
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
22
23 print(v,n);
24 for (int j=0; j<6; j++) {
25 *min(v,n) = 2* (*min(v,n));
26 print(v,n);
27 }
28 }
Salida:
1 [mstorti@spider aedsrc]$ ptrexa
2 Vector: (6 5 1 4 2 3 ), valor minimo: 1
3 Vector: (6 5 2 4 2 3 ), valor minimo: 2
4 Vector: (6 5 4 4 2 3 ), valor minimo: 2
5 Vector: (6 5 4 4 4 3 ), valor minimo: 3
6 Vector: (6 5 4 4 4 6 ), valor minimo: 4
7 Vector: (6 5 8 4 4 6 ), valor minimo: 4
8 Vector: (6 5 8 8 4 6 ), valor minimo: 4
9 [mstorti@spider aedsrc]$
Usando referencias:
1 int &min(int *v,int n) {
2 int x = v[0];
3 int jmin = 0;
4 for (int k=1; k<n; k++) {
5 if (v[k]<x) {
6 jmin = k;
7 x = v[jmin];
8 }
9 }
10 return v[jmin];
11 }
12
13 void print(int *v,int n) {
14 cout << "Vector: (";
15 for (int j=0; j<n; j++) cout << v[j] << " ";
16 cout << "), valor minimo: " << min(v,n) << endl;
17 }
18
19 int main() {
20 int v[ ] = {6,5,1,4,2,3};
21 int n = 6;
22
23 print(v,n);
24 for (int j=0; j<6; j++) {
25 min(v,n) = 2*min(v,n);
26 print(v,n);
27 }
28 }
23 print(L);
24 cout << "Purga lista. . . " << endl;
25 purge(L);
26 cout << "Lista despues de purgar: " << endl;
27 print(L);
28 }
Implementaciones de
lista
elemento 0
elemento 1
elemento 2
.
.
.
.
MAX_SIZE
elemento n−2
elemento n−1
1111
0000
0000
1111
end() n 1111
0000
0000
1111
.
.
.
.
L.insert(p,x)
a a
. .
. .
. .
. .
p−1 d p−1 d
p e p x
p+1 f p+1 e
p+2 g p+2 f
p+3 g
11111
00000
z
00000
11111
00000
11111
end()
1111
0000
z
end() 1111
0000
0000
1111
21 };
1 #include <iostream>
2 #include <aedsrc/lista.h>
3 #include <cstdlib>
4
5 using namespace std;
6 using namespace aed;
7
8 int list::MAX-SIZE=100;
9
10 list::list() : elems(new elem-t[MAX-SIZE]),
11 size(0) { }
12
13 list::˜list() { delete[ ] elems; }
14
15 elem-t &list::retrieve(iterator-t p) {
16 if (p<0 | | p>=size) {
17 cout << "p: mala posicion.\n";
18 abort();
19 }
20 return elems[p];
21 }
1 iterator-t list::erase(iterator-t p) {
2 if (p<0 | | p>=size) {
3 cout << "p: posicion invalida.\n";
4 abort();
5 }
6 for (int j=p; j<size-1; j++) elems[j] = elems[j+1];
7 size--;
8 return p;
9 }
• Almacenamiento rı́gido
• Tiempo de ejecución de insert(p,x) y erase(p) son O(n) en promedio.
1 class cell {
2 friend class list;
3 elem-t elem;
4 cell *next;
5 cell() : next(NULL) {}
6 };
El tipo posición
q r
x y z
w
s
q r
x y z
w
s
x y z
w
s
Celda de encabezamiento
111
000
q0=begin() q1 q2 q3 q4=end()
1111
0000
000
111 0000
1111
000
111
celda de
1 3 2 5
0000
1111
encabezamiento
• L.retrieve(q0) retornará 1,
• L.retrieve(q1) retornará 3...
• L.retrieve(q4) dará error ya que corresponde a la posición
no-dereferenciable L.end()
L
first
last
111
000
q0=begin() q1 q2 q3 q4=end()
1111
0000
0000
1111
000
111
000
111 1 3 2 5
0000
1111
celda de
encabezamiento
1 iterator-t list::prev(iterator-t p) {
2 iterator-t q = first;
3 while (q->next != p) q = q->next;
4 return q;
5 }
1 iterator-t
2 list::insert(iterator-t p,elem-t k) {
3 iterator-t q = p->next;
4 iterator-t c = new cell;
5 p->next = c;
6 c->next = q;
7 c->elem = k;
8 if (q==NULL) last = c;
9 return p;
10 }
p q
x y
w
c
x w y
x w y z
eliminar
111
000
q0=begin() q1 q2 q3 q4=end()
1111
0000
0000
1111
000
111
000
111 1 3 2 5
0000
1111
celda de
encabezamiento
Lista vacı́a
L
first
last
111
000
q0=begin()=end()
000
111
000
111
celda de
encabezamiento
• Las celdas son como en el caso de los punteros, pero ahora el campo
next es de tipo entero, ası́ como las posiciones (iterator_t).
= NULL_CELL cell_space
= cualquier valor elem next
* nro. de celda 0
4
1
*
2 10
2
5
*
CELL_SPACE_SIZE
L1 3
first=2 4 *
last=8 5
* 3
6 11
6
5 8
top_free_cell 7
0
8
*
3
L2 9
* 1
first=9 10
last=10 5
11
9 6
Gestión de celdas
• Es como una lista normal enlazada pero sin posiciones. (En realidad es
una pila).
Gestión de celdas
• Antes de hacer cualquier operación sobre una lista hay que asegurarse
que el espacio de celdas este inicializado.
• Esto se hace en cell_space_init() que aloca cell_space y crea la lista
de celdas libres.
• cell_space_init() lo incluimos en el constructor de la clase lista.
1 list::list() {
2 if (!cell-space) cell-space-init();
3 first = last = new-cell();
4 cell-space[first].next = NULL-CELL;
5 }
6
7 void list::cell space init() {
- -
8 cell-space = new cell[CELL-SPACE-SIZE];
9 for (int j=0; j<CELL-SPACE-SIZE-1; j++)
10 cell-space[j].next = j+1;
11 cell-space[CELL-SPACE-SIZE-1].next = NULL-CELL;
12 top-free-cell = 0;
13 }
Punteros Cursores
Area de heap
almacenamiento cell space
Tipo para direcciones
de las celdas cell* c int c
Dereferenciación de
direcciones
(dirección → celda) *c cell space[c]
Dato de una celda
dada su dirección c c->elem cell space[c].elem
Enlace de una celda
(campo next) dada
su dirección c
c->next cell space[c].next
Dirección inválida
NULL NULL CELL
Tiempos de ejecución
Interfase STL
Sobrecarga de operadores
p = next(p); → p++
p = prev(p); → p--
x = L.retrieve(p); → x = *p
1 public:
2 class iterator {
3 private:
4 friend class list;
5 cell* ptr;
6 public:
7 T & operator*() { return ptr->next->t; }
8 T *operator->() { return &ptr->next->t; }
9 bool operator!=(iterator q) { return ptr!=q.ptr; }
10 bool operator==(iterator q) { return ptr==q.ptr; }
11 iterator(cell *p=NULL) : ptr(p) {}
12 // Prefix:
13 iterator operator++() {
14 ptr = ptr->next;
15 return *this;
16 }
17 // Postfix:
18 iterator operator++(int) {
19 iterator q = *this;
20 ptr = ptr->next;
21 return q;
22 }
23 };
• Hemos incluido un namespace aed. Por lo tanto las clases deben ser
referenciadas como aed::list<int> y aed::list<int>::iterator. O
usar: using namespace aed;.
1 class cell {
2 elem-t elem;
3 cell *next, *prev;
4 cell() : next(NULL), prev(NULL) {}
5 };
El TAD pila
El TAD pila
1 elem-t top();
2 void pop();
3 void push(elem-t x);
4 void clear();
5 int size();
6 bool empty();
• x = top() devuelve el elemento en el tope de la pila (sin modificarla).
• pop() remueve el elemento del tope (sin retornar su valor!).
• push(x) inserta el elemento x en el tope de la pila.
• Hemos agregado
. void clear() remueve todos los elementos de la pila.
. int size() devuelve el número de elementos en la pila.
. bool empty() retorna verdadero si la pila esta vacı́a, verdadero en
caso contrario. (Notar que empty() no modifica la pila, mucha gente
tiende a confundirla con clear().)
P Q P Q P Q P Q
2 2 2
9 3 9
1 7 7 1
7 1 1 7
3 3 9 9 3
2 <vacia> 2 2 <vacia> 2 2 <vacia>
Versión no destructiva:
1 int sumstack2(stack<int> &P) {
2 stack<int> Q;
3 int sum=0;
4 while (!P.empty()) {
5 int w = P.top();
6 P.pop();
7 sum += w;
8 Q.push(w);
9 }
10
11 while (!Q.empty()) {
12 P.push(Q.top());
13 Q.pop();
14 }
15 return sum;
16 }
push
pop x y z
top
1 stack::stack() : size-m(0) { }
2
3 elem-t& stack::top() {
4 return retrieve(begin());
5 }
6
7 void stack::pop() {
8 erase(begin()); size-m--;
9 }
10
11 void stack::push(elem-t x) {
12 insert(begin(),x); size-m++;
13 }
1 void stack::clear() {
2 erase(begin(),end()); size-m = 0;
3 }
4
5 bool stack::empty() {
6 return begin()==end();
7 }
8
9 int stack::size() {
10 return size-m;
11 }
a
.
.
.
.
d
e
f
g
• Tanto la inserción como la supresión en el
primer elemento son O(n).
Tamaño de la pila
• Notar que la pila deriva directamente de la lista, pero con una declaración
private. De esta forma el usuario de la clase stack no puede usar
métodos de la clase lista.
• El hecho de que la pila sea tan simple permite que pueda ser
implementada en términos de otros contenedores también, como por
ejemplo el contenedor vector de STL.
• De esta forma, podemos pensar a la pila como un “adaptador” (“container
adaptor”).
vectores
Interfase STL
El TAD cola
El TAD cola
1 template<class T>
2 class queue : private list<T> {
3 private:
4 int size-m;
5 public:
6 queue() : size-m(0) { }
7 void clear() { erase(begin(),end()); size-m = 0; }
8 T front() { return *begin(); }
9 void pop() { erase(begin()); size-m--; }
10 void push(T x) { insert(end(),x); size-m++; }
11 int size() { return size-m; }
12 bool empty() { return size-m==0; }
13 };
• Inicialmente:
Q = (2, 3, 5, 4, 3, 1), Q2 = ().
• Después de haber pasado algunos elementos de Q a Q2:
Q = (4, 3, 1), Q2 = (2, 3, 5).
• Después de haber pasado todos los elementos de Q a Q2:
Q = (), Q2 = (2, 3, 5, 4, 3, 1).
• Después de volver todos los elementos de Q2 a Q:
Q = (2, 3, 5, 4, 3, 1), Q2 = ().
pop
x y z
front push
Tiempos de ejecución
Correspondencias
El TAD correspondencia
teros a enteros.
−1 1
• La correspondencia debe 0
ser “unı́voca”. 1 0 4
3
• Al elemento del dominio se 9
2
le llama a veces “clave” y al
−3
del contradominio “valor”
1 map sueldo;
2 while(1) {
3 cout << "Ingrese nro. documento > ";
4 int doc;
5 double salario;
6 cin >> doc;
7 if(!doc) break;
8 iterator-t q = sueldo.find(doc);
9 if (q==sueldo.end()) {
10 cout << "Ingrese salario mensual: ";
11 cin >> salario;
12 sueldo.insert(doc,salario);
13 } else {
14 cout << "Doc: " << doc << ", salario: "
15 << sueldo.retrieve(doc) << endl;
16 }
17 }
18 cout << "No se ingresan mas sueldos. . ." << endl;
M
• Guardar en un contenedor todas las asignaciones.
C
D
• Definimos una clase elem t 4
que simplemente contiene dos −1
campos first y second con 3
la clave y el valor de la 1
• Para otros tipos compuestos, para los cuales no existe una relación de
orden se pueden definir relaciones de orden ad-hoc. En algunos casos
estas relaciones de orden no tienen ningún otro interés que ser usadas en
este tipo de representaciones o en otros algoritmos relacionados.
D
M C
−1 4
3
1
2 9
−3
• Sin embargo, al insertar nuevas asignaciones hay que tener en cuenta que
no se debe insertar en cualquier posición sino que hay que mantener la
lista ordenada. Por ejemplo, si queremos asignar a la clave 0 el valor 7,
entonces el par (0, 7) debe insertarse entre los pares (−1, 4) y (2, 1),
para mantener el orden entre las claves.
• Declaración de las clases. map contiene una lista l que contiene las
asignaciones
1 class elem-t {
2 private:
3 friend class map;
4 domain-t first;
5 range-t second;
6 };
7 // iterator para map va a ser el mismo que para listas.
8 class map {
9 private:
10 list l;
1 public:
2 map() { }
3 iterator-t find(domain-t key) {
4 iterator-t p = lower-bound(key);
5 if (p!=l.end() && l.retrieve(p).first == key)
6 return p;
7 else return l.end();
8 }
• Si p es end() o la asignación correspondiente a p contiene exactamente
la clave key, entonces debe retornar p. Caso contrario, p debe
corresponder a una posición dereferenciacible, pero cuya clave no es key
de manera que en este caso no debe retornar p sino end().
1 map<int,double> sueldo;
2 while(1) {
3 cout << "Ingrese nro. documento > ";
4 int doc;
5 double salario;
6 cin >> doc;
7 if (!doc) break;
8 map<int,double>::iterator q = sueldo.find(doc);
9 if (q==sueldo.end()) {
10 cout << "Ingrese salario mensual: ";
11 cin >> salario;
12 sueldo[doc]=salario;
13 } else {
14 cout << "Doc: " << doc << ", salario: "
15 << sueldo[doc] << endl;
16 }
17 }
18 cout << "No se ingresan mas sueldos. . ." << endl;
(Notación: mejor/promedio/peor)
• Una vez que tenemos un rango válido [p, q) podemos refinarlo haciendo
r = floor((p + q)/2) (20)
(Notación: mejor/promedio/peor)
Arboles
Arboles
• Ejemplos tı́picos:
. Diagrama de organización de las empresas o instituciones
. La estructura de un sistema de archivos en una computadora.
. Representar fórmulas
• En general para representar grandes sistemas en sistemas más pequeños
en forma recursiva
Arboles (cont.)
b c d
e f g
anuser/
m1.txt p1.h g1 g2
m2.txt p1.cpp
p2.cpp
Camino en un árbol
camino a
b c d
e f g
h camino
Descendientes y antecesores.
b c d
e f g
Hojas
b c d
e f g
h
hojas
• Un nodo que no tiene hijos es una “hoja” del árbol. (Recordemos que, por
contraposición el nodo que no tiene padre es único y es la raı́z.) En el
ejemplo, los nodos e, f , h y d son hojas.
Altura de un nodo.
h=3=altura del arbol
a
h=1 h=0
b c h=2 d
h=0
h=altura h
a nivel p=0
nivel p=1
b c d
nivel p=2
e f g
Nodos hermanos
a
hermanos
b c d
e f g
• Se dice que los nodos que tienen un mismo padre son “hermanos” entre
sı́.
• Notar que no basta con que dos nodos estén en el mismo nivel para que
sean hermanos.
Arboles ordenados
orientados
a a
b c c b
• En este capı́tulo, estudiamos árboles para los cuales el orden entre los
hermanos es relevante.
• Es decir, los árboles de la figura son diferentes ya que si bien a tiene los
mismos hijos, están en diferente orden.
b c d
e f g
a a
b c d Λ b c d
e f g e f g
h Λ h
Podemos pensar al árbol como una lista bidimensional. Ası́ como en las listas
se puede avanzar linealmente desde el comienzo hacia el fin, en cada nodo
del árbol podemos avanzar en dos direcciones
Posiciones no dereferenciables
a
• Avanzando por el hermano
derecho el recorrido termina en el
b c d Λ8
último hermano a la derecha. Por
analogı́a con la posición end() en Λ7
e f Λ3 g Λ6
las listas, asumiremos que
después del último hermano existe Λ1 Λ2 Λ5
h
un nodo ficticio no
dereferenciable. Λ4
e f g descendientes e f g derecha
h h
Dicho de otra manera, dado un nodo n el conjunto N de todos los nodos del
árbol se puede dividir en 5 conjuntos disjuntos a saber
Orden previo
n1 n2 nk
T1 T2 Tk
e f g
1
oprev(a) = (a, b, e, f, c, g, h, d) b 3 c d
• Recorremos el borde del árbol en 2
el sentido contrario a las agujas del e g
f
reloj
• Dado un nodo como el b el camino
h
pasa cerca de él en varios puntos.
• El orden previo consiste en listar los nodos una sola vez, la primera vez
que el camino pasa cerca del árbol.
Orden posterior
n1 n2 nk
T1 T2 Tk
e f g
1
b 3 c d
2
e f g
opost(a) = (e, f, b, h, g, c, d, a)
• Recorriendo el borde del árbol igual que antes (esto es en sentido
contrario a las agujas del reloj), listando el nodo la última vez que el
recorrido pasa por al lado del mismo.
3
b 1 c d
2
e f g
Orden simétrico
Existe otro orden que se llama “simétrico”, pero este sólo tiene sentido en el
caso de árboles binarios, ası́ que no será explicado aquı́.
+ −
2 3 4 5
Las expresiones matemáticas como (2 + 3) ∗ (4 − 5) se pueden poner en
forma de árbol como se muestra en la figura.
3 * 20 −
sin − 10 7
+ 5 exp
4 20 3
+ −
2 3 4 5
El listado en orden posterior de árboles de expresiones coincide con la
notación polaca invertida (RPN).
rpn = (2, 3, +, 4, 5, −, ∗)
2 operando 2
3 operando 3,2
+ operador 5
4 operando 4,5
5 operando 5,4,5
- operador -1,5
* operador -5
g h q
a b t u v r s
n1 n2 nk
T1 T2 Tk
Notemos que el orden de los nodos es igual al del orden previo. Se puede dar
una definición precisa de la notación Lisp como para el caso de los órdenes
previo y posterior:
si n es una hoja: n
lisp(n) =
caso contrario: (n lisp(n1 ) lisp(n2 ) . . . lisp(nm ))
Para expresiones más complejas, la forma Lisp para el árbol da el código Lisp
correspondiente
*
+ +
3 * 20 −
sin − 10 7
+ 5 exp
4 20 3
1 (* (+ 3 (* (sin (+ 4 20)) (- 5 (exp 3)))) (+ 20 (- 10 7)))
a a a a
b c d b d b b
c c c d
a a a a
b c d b d d d
c c b c
Lo mismo ocurre con el orden posterior. Todos los árboles previos tienen
orden posterior (b, c, d, a)
opost(n) = (descendientes(n1 ), n1 ,
descendientes(n2 ), n2 , . . . , descendientes(nm ), nm , n1 ).
w a c
x m
y t
u
v
oprev(c) = (c, m, t, u, v)
opost(c) = (t, u, v, m, c)
w a c
x y m
t u v
Orden posterior
Notación Lisp
si n es una hoja: n
lisp(n) =
caso contrario: (n lisp(n1 ) lisp(n2 ) . . . lisp(nm ))
Inserción en árboles
Ej: Inserta z en Λ3 y en g
a a a
b c d Λ8
b c d b c d
e f Λ3 g Λ6 Λ7
e f z g e f z g
Λ1 Λ2 Λ5
h
Λ4 h h
nt nq
b c b Λ1 b c b c
ct ct cq cq
e f r g w e f e f Λ2 e f r Λ3
s t h s t
Supresión en árboles
“Poda” un árbol, eliminando todos los nodos de un árbol que son impares
incluyendo sus subárboles. Por ejemplo, si T=(6 (2 3 4) (5 8 10)).
Entonces después de aplicar prune_odd tenemos T=(6 (2 4)). Notar que
los nodos 8 y 10 se eliminan porque son hijos de 5 que es impar.
1 iterator-t prune-odd(tree &T,iterator-t n) {
2 if (/* el valor de ‘n’ es impar. . . */ % 2)
3 /* elimina el nodo ‘n’ . . . */;
4 else {
5 iterator-t c =
6 /* hijo mas izquierdo de ‘n’ . . . */;
7 while (/*‘c’ no es ‘Lambda’ . . . */)
8 c = prune-odd(T,c);
9 c = /* hermano derecho de ‘c’ . . . */;
10 }
11 return n;
12 }
Implementación de
árboles
1 class iterator-t {
2 /* . . . . */
3 public:
4 iterator-t lchild();
5 iterator-t right();
6 };
7
8 class tree {
9 /* . . . . */
10 public:
11 iterator-t begin();
12 iterator-t end();
13 elem-t &retrieve(iterator-t p);
14 iterator-t insert(iterator-t p,elem-t t);
15 iterator-t erase(iterator-t p);
16 void clear();
17 iterator-t splice(iterator-t to,iterator-t from);
18 };
Como con las listas y correspondencias tenemos una clase iterator_t que
nos permite iterar sobre los nodos del árbol, tanto dereferenciables como no
dereferenciables. En lo que sigue T es un árbol, p, q y r son nodos (iterators)
y x es un elemento de tipo elem_t.
Q T Q T
a u a u
b c v b c r v
e f r g w x y e f g w s t x y
s t h h
Notación Lisp
Copia
1 iterator-t tree-copy(tree &T,iterator-t nt,
2 tree &Q,iterator-t nq) {
3 nq = Q.insert(nq,T.retrieve(nt));
4 iterator-t
5 ct = nt.lchild(),
6 cq = nq.lchild();
7 while (ct!=T.end()) {
8 cq = tree-copy(T,ct,Q,cq);
9 ct = ct.right();
10 cq = cq.right();
11 }
12 return nq;
13 }
14
15 void tree-copy(tree &T,tree &Q) {
16 if (T.begin() != T.end())
17 tree-copy(T,T.begin(),Q,Q.begin());
18 }
19
20 //---:---<*>---:---<*>---:---<*>---:---<*>
Copia espejo
Poda impares
x hermano derecho
• Las celdas contienen, además del dato elem, un puntero right a la celda
que corresponde al hermano derecho y otro left_child al hijo más
izquierdo .
celda de encabezamiento
*
c c
r g w r g w
s t h s t h
El tipo iterator
b c d Λ8
• Podrı́amos definir iterator t
como un typedef a cell . Λ6 Λ7
e f Λ3 g
• OK para posiciones dereferencia-
Λ1 Λ2 Λ5
bles y posiciones no dereferencia- h
bles Λ3 .
Λ4
a
• nodo e: ptr=e, prev=NULL, father=b.
• nodo f : ptr=f , prev=e, father=b. b c d Λ8
• nodo Λ1 : ptr=NULL, prev=NULL,
father=e. e f Λ3 g Λ6 Λ7
• nodo Λ2 : ptr=NULL, prev=NULL, Λ1 Λ2
h Λ5
father=f .
• nodo Λ3 : ptr=NULL, prev=f , father=b. Λ4
• nodo g : ptr=g , prev=NULL, father=c.
• nodo Λ6 : ptr=NULL, prev=g , father=c.
1 class tree;
2 class iterator-t;
3
4 //---:---<*>---:---<*>---:---<*>---:---<*>
5 class cell {
6 friend class tree;
7 friend class iterator-t;
8 elem-t elem;
9 cell *right, *left-child;
10 cell() : right(NULL), left-child(NULL) {}
11 };
12
13 //---:---<*>---:---<*>---:---<*>---:---<*>
1 class iterator-t {
2 private:
3 friend class tree;
4 cell *ptr,*prev,*father;
5 iterator-t(cell *p,cell *prev-a, cell *f-a)
6 : ptr(p), prev(prev-a), father(f-a) { }
7 public:
8 iterator-t(const iterator-t &q) {
9 ptr = q.ptr;
10 prev = q.prev;
11 father = q.father;
12 }
• Constructor privado iterator_t(cell *p,cell *pv, cell *f)
• Constructor público iterator_t(const iterator_t &q). Llamado
“constructor por copia” es utilizado cuando hacemos asignaciones de
nodos, por ejemplo iterator_t p,q; ...; p=q;.
1 iterator-t lchild() {
2 return iterator-t(ptr->left-child,NULL,ptr);
3 }
4 iterator-t right() {
5 return iterator-t(ptr->right,ptr,father);
6 }
7 };
8
9 //---:---<*>---:---<*>---:---<*>---:---<*>
• lchild() y right() permiten “movernos” dentro del árbol.
• Sólo pueden aplicarse a posiciones dereferenciables y pueden retornar
posiciones dereferenciables o no.
m.father=n.ptr
n
m.prev=NULL m.ptr=n.ptr−>left_child
m
• m = n.lchild()
• m.ptr = n.ptr->left_child
• m.prev = NULL (ya que m es un hijo más izquierdo)
• m.father = n.ptr
q.father=n.father
y
q.prev=n.ptr q.ptr=n.right
n q
• q = n.right()
• q.ptr = n.ptr->right
• q.prev = n.ptr
• q.father = n.father
1 class tree {
2 private:
3 cell *header;
4 tree(const tree &T) {}
5 public:
6
7 tree() {
8 header = new cell;
9 header->right = NULL;
10 header->left-child = NULL;
11 }
12 ˜tree() { clear(); delete header; }
1 elem-t &retrieve(iterator-t p) {
2 return p.ptr->elem;
3 }
4
5 iterator-t insert(iterator-t p,elem-t elem) {
6 assert(!(p.father==header && p.ptr));
7 cell *c = new cell;
8 c->right = p.ptr;
9 c->elem = elem;
10 p.ptr = c;
11 if (p.prev) p.prev->right = c;
12 else p.father->left-child = c;
13 return p;
14 }
1 iterator-t erase(iterator-t p) {
2 if(p==end()) return p;
3 iterator-t c = p.lchild();
4 while (c!=end()) c = erase(c);
5 cell *q = p.ptr;
6 p.ptr = p.ptr->right;
7 if (p.prev) p.prev->right = p.ptr;
8 else p.father->left-child = p.ptr;
9 delete q;
10 return p;
11 }
Interfase avanzada
Tiempos de ejecución
Operación T (n)
• Todas las funciones básicas
tienen costo O(1). (incluso
O(1)
begin(), end(),
splice()!!) n.right(), n++,
n.lchild(), *n,
• Esto se debe a que la operación insert(),
splice(to,from)
de mover todo el árbol de una
posición a otra se realiza con O(n)
erase(), find(),
una operación de punteros. clear(), T1=T2
• Las operaciones que no son O(1) son erase(p) que debe eliminar
todos los nodos del subárbol del nodo p, clear() que equivale a
erase(begin()), find(x) y el constructor por copia (T1=T2). En
todos los casos n es o bien el número de nodos del subárbol (erase(p)
y find(x,p)) o bien el número total de nodos del árbol (clear(),
find(x) y el constructor por copia T1=T2).
Arboles binarios
Arboles binarios
a a a
b b b
e d
Notación Lisp
a
lisp(a) = (a (b e ·) (c (d · f ) ·))
b c
e d
Interfase básica
1 class iterator-t {
2 /* . . . */
3 public:
4 iterator-t left();
5 iterator-t right();
6 };
7
8 class btree {
9 /* . . . */
10 public:
11 iterator-t begin();
12 iterator-t end();
13 elem-t & retrieve(iterator-t p);
14 iterator-t insert(iterator-t p,elem-t t);
15 iterator-t erase(iterator-t p);
16 void clear();
17 iterator-t splice(iterator-t to,iterator-t from);
18 };
Semejante
7 9 6 4
6 0 7 8
2 3
Semejante (cont.)
x
hijo izquierdo hijo derecho
celda de encabezamiento
*
3 3
7 9 7 9
6 0 6 0
2 2
La clase cell
1 class cell {
2 friend class btree;
3 friend class iterator-t;
4 elem-t t;
5 cell *right,*left;
6 cell() : right(NULL), left(NULL) {}
7 };
left elem right
x
hijo izquierdo hijo derecho
La clase iterator
1 class iterator-t {
2 private:
3 friend class btree;
4 cell *ptr,*father;
5 enum side-t {NONE,R,L};
6 side-t side;
7 iterator-t(cell *p,side-t side-a,cell *f-a)
8 : ptr(p), side(side-a), father(f-a) { }
• Contiene un puntero a la celda, y otro al padre, como en el caso del AOO.
• El puntero prev que apunta al hermano a la izquierda, aquı́ ya no tiene
sentido. Recordemos que el iterator nos debe permitir ubicar a las
posiciones, incluso aquellas que son Λ. Para ello incluimos el iterator un
miembro side de tipo enum side_t, que puede tomar los valores R (right)
y L (left).
b c
d Λ1 Λ2 Λ3
Λ4 Λ5
1 public:
2 iterator-t(const iterator-t &q) {
3 ptr = q.ptr;
4 side = q.side;
5 father = q.father;
6 }
7 bool operator!=(iterator-t q) { return ptr!=q.ptr; }
8 bool operator==(iterator-t q) { return ptr==q.ptr; }
• La comparación de iterators compara sólo los campos ptr, de manera
que todos los iterators Λ resultan iguales entre sı́ (ya que tienen
ptr=NULL). Como end() retorna un iterator Λ (ver más abajo), entonces
esto habilita a usar los lazos tı́picos
1 while (c!=T.end()) {
2 // . . . .
3 c = c.right();
4 }
La clase btree
1 iterator-t left() {
2 return iterator-t(ptr->left,L,ptr);
3 }
4 iterator-t right() {
5 return iterator-t(ptr->right,R,ptr);
6 }
7 };
1 class btree {
2 private:
3 cell *header;
4 iterator-t tree-copy-aux(iterator-t nq,
5 btree &TT,iterator-t nt) {
6 nq = insert(nq,TT.retrieve(nt));
7 iterator-t m = nt.left();
8 if (m != TT.end()) tree-copy-aux(nq.left(),TT,m);
9 m = nt.right();
10 if (m != TT.end()) tree-copy-aux(nq.right(),TT,m);
11 return nq;
12 }
1 public:
2 static int cell-count-m;
3 static int cell-count() { return cell-count-m; }
4 btree() {
5 header = new cell;
6 cell-count-m++;
7 header->right = NULL;
8 header->left = NULL;
9 }
• La clase btree incluye un contador de celdas cell_count() y
constructor por copia btree(const btree &), como para AOO.
Interfase avanzada
Programación funcional
en árboles binarios
Arboles de Huffman
l
hli = = 8 bits/caracter
N
hli = ceil(log2 nc )
Si consideramos texto común, el número de caracteres puede oscilar entre
unos 90 y 128 caracteres, con lo cual la tasa será de 7 bits por caracter, lo
cual representa una ganancia relativamente pequeña del 12.5%.
d 11 11 101
70 × 1 + 10 × 3 + 10 × 3 + 10 × 2 = 150 bits,
resultando en una longitud promedio de
hli =1.5 bit/caracter.
En general
150 bits
hli =
100 caracteres
= 0.70 × 1 + 0.1 × 3 + 0.1 × 3 + 0.1 × 2
X
= P (c)l(c)
c
Esta longitud media representa una compresión de 25% con respecto al C1.
Por supuesto, la ventaja del código C2 se debe a la gran diferencia en
probabilidades entre a y los otros caracteres. Si la diferencia fuera menor,
digamos P (a) = 0.4, P (b) = P (c) = P (d) = 0.2, entonces la longitud de
un mensaje tı́pico de 100 caracteres serı́a
40 × 1 + 20 × 3 + 20 × 3 + 20 × 2 = 200 bits, o sea una tasa de
2 bits/caracter, igual a la de C1.
Condición de prefijos
En el caso de que un código de longitud variable
como el C2 sea más conveniente, debemos poder
asegurarnos poder desencodar los mensajes, es
C1 C2 C3 decir que la relación entre mensaje y mensaje
encodado sea unı́voca y que exista un algoritmo
a 00 0 0 para encodar y desencodar mensajes en un
tiempo razonable. Por empezar los códigos de los
b 01 100 01
diferentes caracteres deben ser diferentes entre
c 10 101 10 sı́, pero esto no es suficiente. Por el ejemplo el
código C3 tiene un código diferente para todos
d 11 11 101
los caracteres, pero los mensaje dba y ccc se
encodan ambos como 101010.
C1 C2 C3
<nulo> <nulo> <nulo>
0 1 0 1 0 1
a a
00 01 10 11 10 11 01 10
a b c d d b c
100 101 101
b c d
C1 C2 C3
<nulo> <nulo> <nulo>
0 1 0 1 0 1
a a
00 01 10 11 10 11 01 10
a b c d d b c
100 101 101
b c d
Códigos redundantes
C4
Letra Código C2 Código C4
<nulo>
a 0 0 0 1
a
b 100 10100 10 11
10100 10101
b c
Notemos que el árbol tiene 3 nodos interiores 10, 101 y 11 que tienen un sólo
hijo. Entonces podemos eliminar tales nodos interiores, “subiendo” todo el
subárbol de 1010 a la posición del 10 y la del 111 a la posición del 11. El
código resultante resulta ser igual al C2 que ya hemos visto.
Como todos los nodos suben y la profundidad de los nodos da la longitud del
código, es obvio que la longitud del código medio será siempre menor para el
código C2 que para el C4, independientemente de las probabilidades de los
códigos de cada caracter. Decimos entonces que un código como el C4 que
tiene nodos interiores con un sólo hijo es “redundante”, ya que puede ser
trivialmente optimizado eliminando tales nodos y subiendo sus subárboles.
Si un AB es tal que no contiene nodos interiores con un solo hijo se le llama
“árbol binario completo” (ABC). Es decir, en un árbol binario completo los
nodos son o bien hojas, o bien nodos interiores con sus dos hijos.
Todas las combinaciones se pueden hallar tomando cada uno de los posibles
pares de árboles (Ti , Tj ) de la lista, combinándolo e insertando
comb(Ti , Tj ) en la lista, después de eliminar los árboles originales.
comb(T0 , T1 , T2 , T3 ) = (comb(comb(T0 , T1 ), T2 , T3 ),
comb(comb(T0 , T2 ), T1 , T3 ),
comb(comb(T0 , T3 ), T1 , T2 ),
comb(comb(T1 , T2 ), T0 , T3 ),
comb(comb(T1 , T3 ), T0 , T2 ),
comb(comb(T2 , T3 ), T0 , T1 ))
T3 T2
T2 T3 T2 T3 T0 T1
T0 T1 T0 T1
Ahora, recursivamente, cada uno de las sublistas se expande a su vez en 3
árboles, por ejemplo
comb(comb(T0 , T1 ), T2 , T3 ) = (comb(comb(comb(T0 , T1 ), T2 ), T3 ),
comb(comb(comb(T0 , T1 ), T3 ), T2 ),
comb(comb(T2 , T3 ), comb(T0 , T1 )))
El algoritmo de Huffman
T1 T2 T3 T4 T5 T6 T1 T7 T8
0.4 0.15 0.15 0.1 0.1 0.1 0.4 0.25 0.35
j=0
a b c d e f a j=3
c d b
T1 T2 T3 T4 T7
e f
0.4 0.15 0.15 0.1 0.2
j=1 T1 T9
a b c d
0.4 0.60
e f
a j=4
T1 T2 T8 T6
0.4 0.15 0.25 0.2 c d b
j=2
e f
a b
c d e f T10
1.0
a j=5
c d b
e f
<nulo> <nulo>
0 1 0 1
a a
10 11 10 11
Notemos que al combinar dos árboles entre sı́ no estamos especificando cuál
queda como hijo derecho y cuál como izquierdo. Por ejemplo, si al combinar
T7 con T8 para formar T9 dejamos a T7 a la derecha, entonces el árbol
resultante será el de la drecha. Si bien el árbol resultante (y por lo tanto la
tabla de códigos) es diferente, la longitud media del código será la misma, ya
que la profundidad de cada caracter, que es su longitud de código, es la
misma en los dos árboles. Por ejemplo, e y f tienen longitud 4 en los dos
códigos.
A diferencia de los algoritmos heurı́sticos, la tabla de códigos ası́ generada es
“óptima” (la mejor de todas), y la longitud media por caracter coincide con la
que se obtendrı́a aplicando el algoritmo de búsqueda exhaustiva pero a un
costo mucho menor.
23 bosque-t::iterator htree =
24 bosque.insert(bosque.begin(),huffman-tree());
25 htree->p = prob[j];
26 htree->T.insert(htree->T.begin(),j);
27 }
28
29 // Aqui empieza el algoritmo de Huffman.
30 // Tmp va a contener el arbol combinado
31 btree<int> Tmp;
32 for (int j=0; j<N-1; j++) {
33 // En la raiz de Tmp (que es un nodo interior)
34 // ponemos un -1 (esto es solo para chequear).
35 Tmp.insert(Tmp.begin(),-1);
36 // Tmp-p es la probabilidad del arbol combinado
37 // (la suma de las probabilidades de los dos subarboles)
38 double Tmp-p = 0.0;
39 // Para ‘k=0’ toma el menor y lo pone en el
40 // hijo izquierdo de la raiz de Tmp. Para ‘k=1’ en el
41 // hijo derecho.
42 for (int k=0; k<2; k++) {
43 // recorre el ‘bosque’ (la lista de arboles)
44 // busca el menor. ‘qmin’ es un iterator al menor
45 bosque-t::iterator q = bosque.begin(), qmin=q;
46 while (q != bosque.end()) {
47 if (q->p < qmin->p) qmin = q;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 404
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
48 q++;
49 }
50 // Asigna a ‘node’ el hijo derecho o izquierdo
51 // de la raiz de ‘Tmp’ dependiendo de ‘k’
52 btree<int>::iterator node = Tmp.begin();
53 node = (k==0 ? node.left() : node.right());
54 // Mueve todo el nodo que esta en ‘qmin’
55 // al nodo correspondiente de ‘Tmp’
56 Tmp.splice(node,qmin->T.begin());
57 // Acumula las probabilidades
58 Tmp-p += qmin->p;
59 // Elimina el arbol correspondiente del bosque.
60 bosque.erase(qmin);
61 }
62 // Inserta el arbol combinado en el bosque
63 bosque-t::iterator r =
64 bosque.insert(bosque.begin(),huffman-tree());
65 // Mueve todo el arbol de ‘Tmp’ al nodo
66 // recien insertado
67 r->T.splice(r->T.begin(),Tmp.begin());
68 // Pone la probabilidad en el elemento de la
69 // lista
70 r->p = Tmp-p;
71 }
72 // Debe haber quedado 1 solo elemento en la lista
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 405
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
73 assert(bosque.size()==1);
74 // Mueve todo el arbol que quedo a ‘T’
75 T.clear();
76 T.splice(T.begin(),bosque.begin()->T.begin());
77 }
• El código se basa en usar una lista de estructuras de tipo huffman_tree
que contienen un doble (la probabilidad) y un árbol.
• Los árboles son de tipo btree<int>. En los valores nodales
almacenaremos para las hojas un ı́ndice que identifica al caracter
correspondiente. Este ı́ndice va entre 0 y N-1 donde N es el número de
caracteres. prob es un vector de dobles de longitud N. prob[j] es la
probabilidad del caracter j. En los valores nodales de los nodos
interiores del árbol almacenaremos un valor -1. Este valor no es usado
normalmente, sólo sirve como un chequeo adicional.
• El tipo bosque_t es un alias para una lista de tales estructuras.
• La función huffman(prob,T) toma un vector de dobles (las
probabilidades) prob y calcula el árbol de Huffman correspondiente.
• En el primer lazo inicial sobre j los elementos del bosque son
inicializados, insertando el único nodo
• En el segundo lazo sobre j se van tomando los dos árboles de bosque
con probabilidad menor. Se combinan usando splice en un árbol auxiliar
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 406
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
Conjuntos
Conjuntos
Relaciones de orden
Una posibilidad serı́a en comparar ciertas funciones escalares del par, como
la suma, o la suma de los cuadrados. Por ejemplo definir que (a, b) < (c, d)
si y sólo sı́ (a + b) < (c + d). Una tal definición satisface transitividad, pero
no la otra condición, ya que por ejemplo los pares (2, 3) y (1, 4) no satisfacen
ninguna de las tres condiciones.
Notar que una vez que se define un operador <, los operadores ≤, > y ≥, e
incluso = se pueden definir fácilmente en términos de <.
Notación de conjuntos
Notación de conjuntos
A = {x entero /x es par }
De esta forma se pueden definir conjuntos con un número infinito de
miembros.
A ∪ B = (A ∩ B) ∪ (A − B) ∪ (B − A)
siendo los tres conjuntos del miembro derecho disjuntos.
1 class iterator-t {
2 private:
3 /* . . . */;
4 public:
5 bool operator!=(iterator-t q);
6 bool operator==(iterator-t q);
7 };
• Como en los otros contenedores STL vistos, una clase iterator permite
recorrer el contenedor. Los iterators soportan los operadores de
comparación == y !=.
1 class set {
2 private:
3 /* . . . */;
4 public:
5 set();
6 set(const set &);
7 ˜set();
8 elem-t retrieve(iterator-t p);
9 pair<iterator-t,bool> insert(elem-t t);
• Sin embargo, en el conjunto no se puede insertar un elemento en una
posición determinada, por lo tanto la función insert no tiene un
argumento posición como en listas o árboles. Sin embargo insert
retorna un iterator al elemento insertado.
1 void clear();
2 iterator-t next(iterator-t p);
3 iterator-t find(elem-t x);
4 iterator-t begin();
5 iterator-t end();
6 };
• Como es usual begin(), end() y next() permiten iterar sobre el
conjunto.
1: t=?
gen[0]={1,2,3}
B0 2: p=?
kill[0]={4,5,6,7,8,9}
3: q=?
B2 q<=p? gen[2]=kill[2]={}
no gen[3]={6}
B3 6: t=p; kill[3]={1,9}
si
B4 7: p=q; gen[4]={7,8}
8: q=t; kill[4]={2,3,4,5}
si
B5 p%q==0? gen[5]=kill[5]={}
no gen[6]=kill[6]={}
B6 cout << q;
B7 9: t=p%q; gen[7]={9}
kill[7]={1,6}
Las asignaciones que salen del bloque son aquellas que llegan, más las
generadas en el bloque menos las que son eliminadas en el mismo.
defin[j]
gen[j] kill[j]
Bj
defout[j]
defout[j]0 ⊆ defout[j]1
defin[j]k ⊆ defin[j]k+1
defout[j]k ⊆ defout[j]k+1
25 if (defout[j].size()!=out-prev) cambio=true;
26 }
27 }
28 }
Implementación por
vectores de bits
Funciones para
U = {enteros pares entre 100 y 198}
Funciones auxiliares para definir conjuntos dentro de las letras a-z y A-Z.
Descripción de la implementación
1 typedef int iterator-t;
2
3 class set {
4 private:
5 vector<bool> v;
6 iterator-t next-aux(iterator-t p) {
7 while (p<N && !v[p]) p++;
8 return p;
9 }
10 typedef pair<iterator-t,bool> pair-t;
11 public:
12 set() : v(N,0) { }
13 set(const set &A) : v(A.v) {}
14 ˜set() {}
15 iterator-t lower-bound(elem-t x) {
16 return next-aux(indx(x));
17 }
18 pair-t insert(elem-t x) {
19 iterator-t k = indx(x);
20 bool inserted = !v[k];
21 v[k] = true;
22 return pair-t(k,inserted);
23 }
24 elem-t retrieve(iterator-t p) { return element(p); }
25 void erase(iterator-t p) { v[p]=false; }
26 int erase(elem-t x) {
27 iterator-t p = indx(x);
28 int r = (v[p] ? 1 : 0);
29 v[p] = false;
30 return r;
31 }
32 void clear() { for(int j=0; j<N; j++) v[j]=false; }
33 iterator-t find(elem-t x) {
34 int k = indx(x);
35 return (v[k] ? k : N);
36 }
37 iterator-t begin() { return next-aux(0); }
38 iterator-t end() { return N; }
39 iterator-t next(iterator-t p) { next-aux(++p); }
40 int size() {
41 int count=0;
42 for (int j=0; j<N; j++) if (v[j]) count++;
43 return count;
44 }
45 friend void set-union(set &A,set &B,set &C);
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 451
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
Implementación con
listas
x = retrieve(p); x = retrieve(p).first;
p=insert(x) p=insert(x,w)
erase(p) erase(p)
erase(x) erase(x)
clear() clear()
p = find(x) p = find(x)
begin() begin()
end() end()
Operaciones binarias
• xa =1, xb =3,
• xa =3, xb =3, inserta 3
• xa =5, xb =5, inserta 5
• xa =7, xb =7, inserta 7
• xa =10, xb =9,
• xa =10, xb =10, inserta 10
• pa llega a A.end()
Descripción de la implementación
1 typedef int elem-t;
2
3 typedef list<elem-t>::iterator iterator-t;
4
5 class set {
6 private:
7 list<elem-t> L;
8 public:
9 set() {}
10 set(const set &A) : L(A.L) {}
11 ˜set() {}
12 elem-t retrieve(iterator-t p) { return *p; }
13 iterator-t lower-bound(elem-t t) {
14 list<elem-t>::iterator p = L.begin();
15 while (p!=L.end() && t>*p) p++;
16 return p;
17 }
18 iterator-t next(iterator-t p) { return ++p; }
19 iterator-t insert(elem-t x) {
20 list<elem-t>::iterator
21 p = lower-bound(x);
45
46 void set-union(set &A,set &B,set &C) {
47 C.clear();
48 list<elem-t>::iterator pa = A.L.begin(),
49 pb = B.L.begin(), pc = C.L.begin();
50 while (pa!=A.L.end() && pb!=B.L.end()) {
51 if (*pa<*pb) { pc = C.L.insert(pc,*pa); pa++; }
52 else if (*pa>*pb) {pc = C.L.insert(pc,*pb); pb++; }
53 else {pc = C.L.insert(pc,*pa); pa++; pb++; }
54 pc++;
55 }
56 while (pa!=A.L.end()) {
57 pc = C.L.insert(pc,*pa);
58 pa++; pc++;
59 }
60 while (pb!=B.L.end()) {
61 pc = C.L.insert(pc,*pb);
62 pb++; pc++;
63 }
64 }
65 void set-intersection(set &A,set &B,set &C) {
66 C.clear();
67 list<elem-t>::iterator pa = A.L.begin(),
68 pb = B.L.begin(), pc = C.L.begin();
69 while (pa!=A.L.end() && pb!=B.L.end()) {
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 470
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
70 if (*pa<*pb) pa++;
71 else if (*pa>*pb) pb++;
72 else { pc=C.L.insert(pc,*pa); pa++; pb++; pc++; }
73 }
74 }
75 // C = A - B
76 void set-difference(set &A,set &B,set &C) {
77 C.clear();
78 list<elem-t>::iterator pa = A.L.begin(),
79 pb = B.L.begin(), pc = C.L.begin();
80 while (pa!=A.L.end() && pb!=B.L.end()) {
81 if (*pa<*pb) { pc=C.L.insert(pc,*pa); pa++; pc++; }
82 else if (*pa>*pb) pb++;
83 else { pa++; pb++; }
84 }
85 while (pa!=A.L.end()) {
86 pc = C.L.insert(pc,*pa);
87 pa++; pc++;
88 }
89 }
Tiempos de ejecución
Método T (N )
O(n)
retrieve(p), insert(x), erase(x),
clear(), find(x), lower bound(x),
set union(A,B,C),
set intersection(A,B,C),
set difference(A,B,C),
O(1)
erase(p), begin(), end(),
Interfase avanzada
Diccionarios
El diccionario
Tablas de dispersión
En este caso está garantizado que los números de cubetas devueltos por h()
están en el rango [0, B). En la práctica, el programador de la clase puede
proveer funciones de hash para los tipos más usuales (como int, double,
string...) dejando la posibilidad de que el usuario defina la función de hash
para otros tipos, o también para los tipos básicos si considera que los que el
provee son más eficientes (ya veremos cuáles son los requisitos para una
buena función de hash). Asumiremos siempre que el tiempo de ejecución de
la función de dispersión es O(1). Para mayor seguridad, asignamos al
elemento t la cubeta b=h(t)%B, de esta forma está siempre garantizado que b
está en el rango [0, B).
En esta implementación las cubetas no son elementos, sino que son listas
(simplemente enlazadas) de elementos, es decir el vector v es de tipo
vector< list<elem_t> >). De esta forma cada cubeta puede contener
(teóricamente) infinitos elementos. Los elementos pueden insertarse en las
cubetas en cualquier orden o ordenadas. La discusión de la eficiencia en este
caso es similar a la de correspondencia con contenedores lineales.
Detalles de implementación
1 typedef int key-t;
2
3 class hash-set;
4 class iterator-t {
5 friend class hash-set;
6 private:
7 int bucket;
8 std::list<key-t>::iterator p;
9 iterator-t(int b,std::list<key-t>::iterator q)
10 : bucket(b), p(q) { }
11 public:
12 bool operator==(iterator-t q) {
13 return (bucket == q.bucket && p==q.p);
14 }
15 bool operator!=(iterator-t q) {
16 return !(*this==q);
17 }
18 iterator-t() { }
19 };
20 typedef int (*hash-fun)(key-t x);
21
22 class hash-set {
23 private:
24 typedef std::list<key-t> list-t;
25 typedef list-t::iterator listit-t;
26 typedef std::pair<iterator-t,bool> pair-t;
27 hash-set(const hash-set&) {}
28 hash-set& operator=(const hash-set&) {}
29 hash-fun h;
30 int B;
31 int count;
32 std::vector<list-t> v;
33 iterator-t next-aux(iterator-t p) {
34 while (p.p==v[p.bucket].end()
35 && p.bucket<B-1) {
36 p.bucket++;
37 p.p = v[p.bucket].begin();
38 }
39 return p;
40 }
41 public:
42 hash-set(int B-a,hash-fun h-a)
43 : B(B-a), v(B), h(h-a), count(0) { }
44 iterator-t begin() {
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 488
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
45 iterator-t p = iterator-t(0,v[0].begin());
46 return next-aux(p);
47 }
48 iterator-t end() {
49 return iterator-t(B-1,v[B-1].end());
50 }
51 iterator-t next(iterator-t p) {
52 p.p++; return next-aux(p);
53 }
54 key-t retrieve(iterator-t p) { return *p.p; }
55 pair-t insert(const key-t& x) {
56 int b = h(x) % B;
57 list-t &L = v[b];
58 listit-t p = L.begin();
59 while (p!= L.end() && *p!=x) p++;
60 if (p!= L.end() && *p==x)
61 return pair-t(iterator-t(b,p),false);
62 else {
63 count++;
64 p = L.insert(p,x);
65 return pair-t(iterator-t(b,p),true);
66 }
67 }
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 489
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
68 iterator-t find(key-t& x) {
69 int b = h(x) % B;
70 list-t &L = v[b];
71 listit-t p = L.begin();
72 while (p!= L.end() && *p!=x) p++;
73 if (p!= L.end() && *p==x)
74 return iterator-t(b,p);
75 else return end();
76 }
77 int erase(const key-t& x) {
78 list-t &L = v[h(x) % B];
79 listit-t p = L.begin();
80 while (p!= L.end() && *p!=x) p++;
81 if (p!= L.end() && *p==x) {
82 L.erase(p);
83 count--;
84 return 1;
85 } else return 0;
86 }
87 void erase(iterator-t p) {
88 v[p.bucket].erase(p.p);
89 }
90 void clear() {
91 count=0;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 490
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
• Hay que tener cuidado de, al avanzar un iterator siempre llegar a otro
iterator válido. La función privada next_aux() avanza cualquier
combinación de cubeta y posición en la lista (puede no ser válida, por
ejemplo el end() de una lista que no es la última) hasta la siguiente
posición válida.
Diccionarios. Tiempos
de ejecución
Tiempos de ejecución
O(1) O(1)
retrieve(p),
erase(p), end()
O(1 + n/B) O(n)
insert(x),
erase(x)
si n = 0, O(B);
si n <= B, O(B/n); O(B)
begin(), next(p)
si n >B O(1);
O(n + B) O(n + B)
clear()
Funciones de dispersión
De los costos que hemos analizado para las tablas de dispersión abierta se
deduce que para que éstas sean efectivas los elementos deben ser
distribuidos uniformemente sobre las cubetas. El diseño de una buena
función de dispersión es precisamente ése. Pensemos por ejemplo en el caso
de una tabla de dispersión para strings con B =256 cubetas. Como función de
dispersión podemos tomar
1 int h2(string s) {
2 int v = 0;
3 for (int j=0; j<s.size(); j++) {
4 v += s[j];
5 v = v % 256;
6 }
7 return v;
8 }
Esta función calcula la suma de los códigos ASCII de todos los caracteres del
string, módulo 256. Notamos primero que basta con que dos strings tengan
un sólo caracter diferente para que sus valores de función de dispersión sean
diferentes. Por ejemplo, los strings argonauta y argonautas irán a diferentes
cubetas ya que difieren en un caracter. Sin embargo las palabras vibora y
bravio irán a la misma ya que los caracteres son los mismos, pero en
diferente orden (son anagramas la una de la otra).
Insertado={1,13,4,1,24,12,15,34,4,44,22,15,17}
Probabilidad de ocurrencia
Nro. de intentos infructuosos
P (m) = αm (1 − α), (α =
(m)
0.75)
0 0.250000
1 0.187500
2 0.140625
3 0.105469
4 0.079102
5 0.059326
6 0.044495
7 0.033371
8 0.025028
B−1
X
hmi = m P (m)
m=0
B−1
X
= m αm (1 − α)
m=0
m d m
mα =α α
dα
de manera que
∞
X d m
hmi = (1 − α) α α
m=0
dα
∞
!
d X
= α (1 − α) αm
dα
k=0
d 1 α
= α (1 − α) =
dα 1−α 1−α
Por ejemplo, en el caso de tener B = 100 cubetas y α = 0.9 (90% de cubetas
ocupadas) el número de intentos medio es de hmi = 0.9/(1 − 0.9) = 9.
1 α α0
Z
0
hmn.e. i = dα
α α0 =0 1 − α0
Z α
1 1 0
= − 1 dα
α α0 =0 1 − α0
1 α
= (− log(1 − α0 ) − α0 )|α0 =0
α
1
= − log(1 − α) − 1
α
8
1
6
4
1
log(1 ) 1
2
0
0 0.2 0.4 0.6 0.8 1
Costo de la búsqueda
α 1
hmbusq.n.e. i = , hmbusq.e. i = − log(1 − α) − 1
1−α α
Supresión de elementos
Al eliminar un elemento uno estarı́a tentado de reemplazar el elemento por un
undef. Sin embargo, esto dificultarı́a las posibles búsquedas futuras ya que
ya no serı́a posible detenerse al encontrar un undef para determinar que el
elemento no está en la tabla. La solución es introducir otro elemento deleted
(“eliminado”) que marcará posiciones donde previamente hubo alguna vez un
elemento que fue eliminado. Por ejemplo, para enteros positivos podrı́amos
usar undef=0 y deleted=-1. Ahora, al hacer p=find(x) debemos recorrer las
cubetas siguientes a h(x) hasta encontrar x o un elemento undef. Los
elementos deleted son tratados como una cubeta ocupada más. Sin
embargo, al hacer un insert(x) de un nuevo elemento, podemos insertarlo
en posiciones deleted además de undef.
n + ndel
α0 =
B
donde ahora ndel es el número de elementos deleted en la tabla.
Supongamos una tabla con B =100 cubetas en la cual se insertan 50
elementos distintos, y a partir de allı́ se ejecuta un lazo infinito en el cual se
inserta un nuevo elemento al azar y se elimina otro del conjunto, también al
azar. Después de cada ejecución del cuerpo del lazo el número de elementos
se mantiene en n = 50 ya que se inserta y elimina un elemento. La tabla
nunca se llena, pero el número de suprimidos ndel crece hasta que
eventualmente llega a ser igual B − n, es decir todas las cubetas están
ocupadas o bien tienen deleted. Cada operación recorre toda la tabla, ya que
en ningún momento encuentra un undef.
Reinserción de la tabla
Como inicialmente la nueva tabla tendrá todos los elementos undef, y las
inserciones no generan elementos deleted, la tabla reinsertada estará libre
de deleted’s. Esta tarea es O(B + n) y se ve compensada por el tiempo que
se ahorrará en unas pocas operaciones.
ndel
β=
B
Cuando β ≈ 1 − α quiere decir que de la fracción de cubetas no ocupadas
1 − α, una gran cantidad de ellas dada por la fracción β está ocupada por
suprimidos, degradando la eficiencia de la tabla. Por ejemplo, si α = 0.5 y
β = 0.45 entonces 50% de las cubetas está ocupada, y del restante 50% el
45% está ocupado por deleted. En esta situación la eficiencia de la tabla es
equivalente una con un 95% ocupado.
Reinserción continua
S={} S={24} S={24} S={24}
3 13 3 13 3 13 3 13
4 4 4 4 4 <undef> 4 24
5 24 5 <undef> 5 <undef> 5 <undef>
6 <undef> 6 <undef> 6 <undef> 6 <undef>
Estrategias de redispersión
Detalles de implementación
1 typedef int iterator-t;
2 typedef int (*hash-fun)(key-t x);
3 typedef int (*redisp-fun)(int j);
4
5 int linear-redisp-fun(int j) { return j; }
6
7 class hash-set {
8 private:
9 hash-set(const hash-set&) {}
10 hash-set& operator=(const hash-set&) {}
11 int undef, deleted;
12 hash-fun h;
13 redisp-fun rdf;
14 int B;
15 int count;
16 std::vector<key-t> v;
17 std::stack<key-t> S;
18 iterator-t locate(key-t x,iterator-t &fdel) {
19 int init = h(x)+rdf(0);
20 int bucket;
21 bool not-found = true;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 522
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
70 }
71 int erase(const key-t& x) {
72 iterator-t fdel;
73 int bucket = locate(x,fdel);
74 if (v[bucket]==x) {
75 v[bucket]=deleted;
76 count--;
77 // Trata de purgar elementos ‘deleted’
78 // Busca el siguiente elemento ‘undef’
79 int j;
80 for (j=1; j<B; j++) {
81 op-count++;
82 int b = (bucket+j) % B;
83 key-t vb = v[b];
84 if (vb==undef) break;
85 S.push(vb);
86 v[b]=undef;
87 count--;
88 }
89 v[bucket]=undef;
90 // Va haciendo erase/insert de los elementos
91 // de atras hacia adelante hasta que se llene
92 // ‘bucket’
93 while (!S.empty()) {
94 op-count++;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 525
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
95 insert(S.top());
96 S.pop();
97 }
98 return 1;
99 } else return 0;
100 }
101 iterator-t begin() {
102 return next-aux(0);
103 }
104 iterator-t end() { return B; }
105 iterator-t next(iterator-t p) {
106 return next-aux(p++);
107 }
108 void clear() {
109 count=0;
110 for (int j=0; j<B; j++) v[j]=undef;
111 }
112 int size() { return count; }
113 };
Una forma muy eficiente de representar conjuntos son los árboles binarios de
búsqueda (ABB). Un árbol binario es un ABB si es vacı́o (Λ) o:
• Todos los elementos en los nodos del subárbol izquierdo son menores
que el nodo raı́z.
• Todos los elementos en los nodos del subárbol derecho son mayores que
el nodo raı́z.
• Los subárboles del hijo derecho e izquierdo son a su vez ABB.
10
5 14
7 12 18
15
min 10 max
5 14
7 12 18
15
10 10
5 14 5 14
7 12 18 7 12 18
15 Λ 15
borrar 10
x sale
10 12
5 16 5 16
minr
7 12 18 7 14 18
14 13 15
13 15
Implementación
1 template<class T>
2 class set {
3 private:
4 typedef btree<T> tree-t;
5 typedef typename tree-t::iterator node-t;
6 tree-t bstree;
7 node-t min(node-t m) {
8 if (m == bstree.end()) return bstree.end();
9 while (true) {
10 node-t n = m.left();
11 if (n==bstree.end()) return m;
12 m = n;
13 }
14 }
15
16 void set-union-aux(tree-t &t,node-t n) {
17 if (n==t.end()) return;
18 else {
19 insert(*n);
20 set-union-aux(t,n.left());
21 set-union-aux(t,n.right());
22 }
23 }
24 void set-intersection-aux(tree-t &t,
25 node-t n, set &B) {
26 if (n==t.end()) return;
27 else {
28 if (B.find(*n)!=B.end()) insert(*n);
29 set-intersection-aux(t,n.left(),B);
30 set-intersection-aux(t,n.right(),B);
31 }
32 }
33 void set-difference-aux(tree-t &t,
34 node-t n, set &B) {
35 if (n==t.end()) return;
36 else {
37 if (B.find(*n)==B.end()) insert(*n);
38 set-difference-aux(t,n.left(),B);
39 set-difference-aux(t,n.right(),B);
40 }
41 }
42 int size-aux(tree-t t,node-t n) {
43 if (n==t.end()) return 0;
44 else return 1+size-aux(t,n.left())
45 +size-aux(t,n.right());
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 539
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
46 }
47 public:
48 class iterator {
49 private:
50 friend class set;
51 node-t node;
52 tree-t *bstree;
53 iterator(node-t m,tree-t &t)
54 : node(m), bstree(&t) {}
55 node-t next(node-t n) {
56 node-t m = n.right();
57 if (m!=bstree->end()) {
58 while (true) {
59 node-t q = m.left();
60 if (q==bstree->end()) return m;
61 m = q;
62 }
63 } else {
64 // busca el padre
65 m = bstree->begin();
66 if (n==m) return bstree->end();
67 node-t r = bstree->end();
68 while (true) {
69 node-t q;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 540
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
96 return *this;
97 }
98 // Postfix:
99 iterator operator++(int) {
100 node-t q = node;
101 node = next(node);
102 return iterator(q,*bstree);
103 }
104 };
105 private:
106 typedef pair<iterator,bool> pair-t;
107 public:
108 set() {}
109 set(const set &A) : bstree(A.bstree) {}
110 ˜set() {}
111 pair-t insert(T x) {
112 node-t q = find(x).node;
113 if (q == bstree.end()) {
114 q = bstree.insert(q,x);
115 return pair-t(iterator(q,bstree),true);
116 } else return pair-t(iterator(q,bstree),false);
117 }
118 void erase(iterator m) {
119 node-t p = m.node;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 542
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
145 else {
146 erase(q);
147 ret = 1;
148 }
149 return ret;
150 }
151 void clear() { bstree.clear(); }
152 iterator find(T x) {
153 node-t m = bstree.begin();
154 while (true) {
155 if (m == bstree.end())
156 return iterator(m,bstree);
157 if (x<*m) m = m.left();
158 else if (x>*m) m = m.right();
159 else return iterator(m,bstree);
160 }
161 }
162 iterator begin() {
163 return iterator(min(bstree.begin()),bstree);
164 }
165 iterator end() {
166 return iterator(bstree.end(),bstree);
167 }
168 int size() {
169 return size-aux(bstree,bstree.begin()); }
170 friend void
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 544
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
196 C.set-difference-aux(A.bstree,
197 A.bstree.begin(),B);
198 }
Detalles de implementación
Tiempos de ejecución
D A
A E B D
B C C E
left rotation
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 552
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
Ordenamiento
Ordenamiento
Ordenamiento (cont.)
Ordenamiento (cont.)
Estabilidad
• Recordemos que una relación de orden fuerte debe ser transitiva: a < b y
b < c =⇒ a < c y satisfacer que, dados x e y sólo una de las siguienes
es verdadera
x < y, y < x, x=y
• Una relación de orden es débil si para todo x, y nunca ocurre que
x < y, e y < x,
son verdaderas al mismo tiempo. Si ninguna de x < y y < x son falsas
entonces decimos que x e y son equivalentes (x ≡ y ).
Por ejemplo
. Ordenar enteros por su valor absoluto (−3 ≡ 3).
. Pares de enteros por la primera componente. ((2, 3) ≡ (2, 5)).
. Legajos por el nombre del empleado. (Perez, Juan,14231235) ≡
(Perez, Juan,12765987).
Estabilidad (cont.)
Métodos de
ordenamiento lentos
ordenado no ordenado
burbuja
1 33 15 31 31 36 7 13 30 11 22
2 7 33 15 31 31 36 11 13 30 22
3 7 11 33 15 31 31 36 13 22 30
4 7 11 13 33 15 31 31 36 22 30
5 7 11 13 15 33 22 31 31 36 30
6 7 11 13 15 22 33 30 31 31 36
7 7 11 13 15 22 30 33 31 31 36
8 7 11 13 15 22 30 31 33 31 36
9 7 11 13 15 22 30 31 31 33 36
10 7 11 13 15 22 30 31 31 33 36
• En el último caso usa como función de comparación la relación < del tipo
T.
1 template<class T>
2 bool less(T x,T y) {
3 return x<y;
4 }
5
6 vector<int> v;
7 // Pone elementos en v. . .
8
9 sort(v.begin(),v.end()); // Ordena por < de enteros
10
11 // Ordena por valor absoluto
12 bool less-abs(int x, int y) {
13 return abs(x)<abs(y);
14 }
15 sort(v.begin(),v.end(),less-abs);
1 33 15 31 31 36 7 13 30 11 22
2 15 33 31 31 36 7 13 30 11 22
3 15 31 33 31 36 7 13 30 11 22
4 15 31 31 33 36 7 13 30 11 22
5 15 31 31 33 36 7 13 30 11 22
6 7 15 31 31 33 36 13 30 11 22
7 7 13 15 31 31 33 36 30 11 22
8 7 13 15 30 31 31 33 36 11 22
9 7 11 13 15 30 31 31 33 36 22
10 7 11 13 15 22 30 31 31 33 36
1 33 15 31 31 36 7 13 30 11 22
2 7 15 31 31 36 33 13 30 11 22
3 7 11 31 31 36 33 13 30 15 22
4 7 11 13 31 36 33 31 30 15 22
5 7 11 13 15 36 33 31 30 31 22
6 7 11 13 15 22 33 31 30 31 36
7 7 11 13 15 22 30 31 33 31 36
8 7 11 13 15 22 30 31 33 31 36
9 7 11 13 15 22 30 31 31 33 36
10 7 11 13 15 22 30 31 31 33 36
Tiempos de ejecución
Notación: (mejor/prom/peor)
Ordenamiento rápido
(quick-sort)
w
quicksort(w,0,n)
l=particiona(w,0,n,v)
<v >=v
quicksort(w,0,l) r quicksort(w,v,l,n)
ordenado ordenado
Seudocódigo
1 void quicksort(w,j1,j2) {
2 // Ordena el rango [j1,j2) de ‘w’
3 if (n==1) return;
4 // elegir pivote v . . .
5 l = particiona(w,j1,j2,v);
6 quicksort(w,j1,l);
7 quicksort(w,l,j2);
8 }
Si n = j2 − j1 es la longitud del vector y n1 = l − j1 y n2 = j2 − l
v
3 1 4 1 5 9 2 6 5 3
particiona(v=3)
2 1 1 4 5 9 3 6 5 3
part(v=2) part(v=5)
Temporariamente 1 1 2 4 3 3 9 6 5 5
tomamos como pivote
el mayor de los dos part(v=4) part(v=9)
primeros distintos. 3 3 4 5 6 5 9
Esto garantiza que al
menos cada una de las part(v=6)
dos particiones tiene al
menos un elemento.
5 5 6
Si logramos que
T (2) = c + 2T (1) = c + 2d
T (4) = 4c + 2T (2) = 3 · 4c + 4d
T (8) = 8c + 2T (4) = 4 · 8c + 8d
T (16) = 16c + 2T (8) = 5 · 16c + 16d
.. ..
. = .
T (2p ) = (p + 1)n(c + d)
Como p = log2 n
T (n) = O(n log n)
Detalles de implementación
1 template<class T>
2 typename std::vector<T>::iterator
3 partition(typename std::vector<T>::iterator first,
4 typename std::vector<T>::iterator last,
5 bool (*comp)(T&,T&),T &pivot) {
6 typename std::vector<T>::iterator
7 l = first,
8 r = last;
9 r--;
10 while (true) {
11 T tmp = *l;
12 *l = *r;
13 *r = tmp;
14 while (comp(*l,pivot)) l++;
15 while (!comp(*r,pivot)) r--;
16 if (l>r) break;
17 }
18 return l;
19 }
23 }
24 typename std::vector<T>::iterator
25 s = dif.begin();
26 bubble-sort(s,s+ndif,comp);
27 return ndif;
28 }
22
23 template<class T> void
24 quick-sort(typename std::vector<T>::iterator first,
25 typename std::vector<T>::iterator last) {
26 quick-sort(first,last,less<T>);
27 }
Peor caso
Quick-sort. Observaciones
Ordenamiento por
montı́culos (heap-sort)
Montı́culo
23 11 32 18 23 11 32 13 23 11 Λ 18
24 25 12 22 25 12 24 Λ 12
Montı́culo (cont.)
0
5
• La condición de que sea 1 2
10 16
parcialmente ordenado
implica que el mı́nimo está 3 4 5 6
siempre en la raı́z. 23 11 32 18
• La condición de 7 8 9
parcialmente lleno permite 24 25 12
implementarlo
eficientemente en un 5 10 16 23 11 32 18 24 25 12
vector.
Inserción en montı́culo
inserta 4
5 4
10 16 5 16
23 11 32 18 23 10 32 18
24 25 12 4 24 25 12 11
sale
4 11 Re−heap 5
5 16 5 16 10 16
23 10 32 18 23 10 32 18 23 11 32 18
24 25 12 11 24 25 12 24 25 12
Make-heap.
12 5 4 8 12 23 16 8 12 23 16 10
Implementación
1 template<class T> void
2 re-heap(typename std::vector<T>::iterator first,
3 typename std::vector<T>::iterator last,
4 bool (*comp)(T&,T&),int j=0) {
5 int size = (last-first);
6 T tmp;
7 while (true) {
8 typename std::vector<T>::iterator
9 higher,
10 father = first + j,
11 l = first + 2*j+1,
12 r = l + 1;
13 if (l>=last) break;
14 if (r<last)
15 higher = (comp(*l,*r) ? r : l);
16 else higher = l;
17 if (comp(*father,*higher)) {
18 tmp = *higher;
19 *higher = *father;
20 *father = tmp;
21 }
22 j = higher - first;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 593
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
23 }
24 }
25
26 template<class T> void
27 make-heap(typename std::vector<T>::iterator first,
28 typename std::vector<T>::iterator last,
29 bool (*comp)(T&,T&)) {
30 int size = (last-first);
31 for (int j=size/2-1; j>=0; j--)
32 re-heap(first,last,comp,j);
33 }
34
35 template<class T> void
36 heap-sort(typename std::vector<T>::iterator first,
37 typename std::vector<T>::iterator last,
38 bool (*comp)(T&,T&)) {
39 make-heap(first,last,comp);
40 typename std::vector<T>::iterator
41 heap-last = last;
42 T tmp;
43 while (heap-last>first) {
44 heap-last--;
45 tmp = *first;
46 *first = *heap-last;
47 *heap-last = tmp;
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 594
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
48 re-heap(first,heap-last,comp);
49 }
50 }
51
52 template<class T> void
53 heap-sort(typename std::vector<T>::iterator first,
54 typename std::vector<T>::iterator last) {
55 heap-sort(first,last,less<T>);
4 5 8
5 8 10 8 10 16
12 23 16 10 12 23 16 12 23
4 5 8 12 23 16 10 5 10 8 12 23 16 4 8 10 16 12 23 5 4
heap heap ord heap ord
Estabilidad de los
diferentes esquemas
Estabilidad
ordenado no ordenado
burbuja
1 for (int k=size-1; k>j; k--) {
2 if (comp(*(first+k),*(first+k-1))) {
3 T tmp = *(first+k-1);
4 *(first+k-1) = *(first+k);
5 *(first+k) = tmp;
6 }
7 }
por lo tanto bubble -sort SI es estable.
Estabilidad de inserción
El elemento ordenado
*(first+j) es *(first+j)
x<=*(first+j) x>*(first+j) desordenado
intercambiado con
todo el rango
[first+k,first+j)
que son elementos
estrictamente mayores
que *(first+j).
1 for (int j=1; j<size; j++) {
2 T tmp = *(first+j);
3 int k=j-1;
4 while (true) {
5 if (!comp(tmp,*(first+k))) break;
6 *(first+k+1) = *(first+k);
7 if (--k < 0) break;
8 }
9 *(first+k+1) = tmp;
10 }
por lo tanto insertion-sort SI es estable.
Estabilidad de selección
El elemento *(first+j),
que va a ir a la posición min
ordenado desordenado
min, puede estar
cambiando de posición
relativa con elementos
equivalentes en el rango
(first+j,min), violando
la estabilidad.
1 for (int j=0; j<size-1; j++) {
2 typename std::vector<T>::iterator
3 min = first+j,
4 q = min+1;
5 while (q<last) {
6 if (comp(*q,*min)) min = q;
7 q++;
8 }
9 T tmp = *(first+j);
10 *(first+j) = *min;
11 *min = tmp;
12 }
por lo tanto selection-sort NO es estable.
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 601
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
Estabilidad de quick-sort
w
quicksort(w,0,n)
l=particiona(w,0,n,v)
<v >=v
quicksort(w,0,l) r quicksort(w,v,l,n)
ordenado ordenado
• Notar que tenemos dos lazos recursivos anidados: un lazo externo que
hace el quick-sort y uno interno para cada partición.
• Si el tiempo de swap es O(n), entonces se puede hacer un análisis
similar al de quick-sort (para el caso promedio) y se llega a la conclusión
que el tiempo de ejecución de esta versión del algoritmo de partición es
O(n log n) siempre. (Aquı́ elegimos el punto medio middle siempre en la
mitad mientras que en quick-sort las subsecuencias pueden llegar a estar
desbalanceadas).
• swap() se puede implementar en tiempo O(n) in-place (ver apuntes). (Si
relajamos la condición de ser in-place es trivial).
• Al aumentar el tiempo de ejecución del particionamiento, el tiempo de
ejecución de quick-sort pasa a ser O(n(log n)2 ) (ver apuntes).
Estabilidad de heap-sort
L= 7 3 8 1 4 0 1 3
split
L1 = 7 8 4 1 L2 = 3 1 0 3
sort(L1) sort(L2)
L1 = 1 4 7 8 L2 = 0 1 3 3
merge
L= 0 1 1 3 3 4 7 8
23 LL.erase(LL.begin());
24 }
25 while (!L1.empty()) {
26 L.insert(L.end(),*L1.begin());
27 L1.erase(L1.begin());
28 }
29 while (!L2.empty()) {
30 L.insert(L.end(),*L2.begin());
31 L2.erase(L2.begin());
32 }
33 }
34
35 template<class T>
36 void merge-sort(std::list<T> &L) {
37 merge-sort(L,less<T>);
Merge-sort estable
L1 = 7 8 4 1 L2 = 3 1 0 3
sort(L1) sort(L2)
L1 = 1 4 7 8 L2 = 0 1 3 3
merge
L= 0 1 1 3 3 4 7 8
Para que merge-sort se debe hacer el split en forma estable mandando los
primeros n1 = floor(n/2) a L1 y los demás
(n2 = n − floor(n/2) = ceil(n/2)) a L2. Después, al hacer el merge, cuando
ambos elementos son equivalentes se elige el de la lista L1.
L= 7 3 8 1 4 0 1 3
stable split
L1 = 7 3 8 1 L2 = 4 0 1 3
sort(L1) sort(L2)
L1 = 1 3 7 8 L2 = 0 1 3 4
merge
L= 0 1 1 3 3 4 7 8
Ordenamiento externo
con merge-sort
min
ya ordenado
1 // K-way merge
2 FILE *out = fopen(file-out,"w");
3 while (1) {
4 int jmin=-1;
5 for (int j=0; j<nfiles; j++) {
6 if (!files[j]) continue;
7 if (jmin<0 | | front[j]<front[jmin])
8 jmin = j;
9 }
10 if (jmin<0) break;
11 fprintf(out,"%d\n",front[jmin]);
12 int nread = fscanf(files[jmin],
13 "%d",&front[jmin]);
14 if (nread!=1) {
15 fclose(files[jmin]);
16 files[jmin]=NULL;
17 }
18
19 }
• Este es el corazón del algoritmo. En cada paso del lazo tomamos el menor
de todo el frente, lo guardamos en el vector de salida y reemplazamos el
elemento con el siguiente del archivo correspondiente.
• Solo se revisan los archivos activos (files[j]!=NULL).
• Si en uno se acaban los elementos se cierra y se desactiva.
Comparación de
métodos
Comparación de métodos
merge−sort[list]
quick−sort[st] merge−sort[ext,M=1e5]
merge−sort[ext,M=1e6]
1e−06 merge−sort[ext,M=1e7]
heap−sort
libc−sort
merge−sort[vec,st]
STL[vec]
1e−07
1 10 100 1000 10000 100000 1e+06 1e+07 1e+08 1e+09
n
Diseño de algoritmos
Algoritmos dividir-para-vencer
El problema de las
Torres de Hanoi
0
1
2
3
4
0000000000000000000000000000000000000
1111111111111111111111111111111111111
0000000000000000000000000000000000000
1111111111111111111111111111111111111
0000000000000000000000000000000000000
1111111111111111111111111111111111111
0000000000000000000000000000000000000
1111111111111111111111111111111111111
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 643
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
Para n =3
A:2 1 0 B: C:
A:2 1 B:0 C:
A:2 B:0 C:1
Para n =2 A:2 B: C:1 0
A:1 0 B: C: A: B:2 C:1 0
A:1 B:0 C: A:0 B:2 C:1
A: B:0 C:1 A:0 B:2 1 C:
A: B: C:1 0 A: B:2 1 0 C:
Para n =4
A:3 2 1 0 B: C:
A:3 2 1 B:0 C:
A:3 2 B:0 C:1
A:3 2 B: C:1 0
A:3 B:2 C:1 0
A:3 0 B:2 C:1
A:3 0 B:2 1 C:
A:3 B:2 1 0 C:
A: B:2 1 0 C:3
A: B:2 1 C:3 0
A:1 B:2 C:3 0
A:1 0 B:2 C:3
A:1 0 B: C:3 2
A:1 B:0 C:3 2
A: B:0 C:3 2 1
A: B: C:3 2 1 0
Para n =3
A:2 1 0 B: C:
A:2 1 B:0 C:
A:2 B:0 C:1
A:2 B: C:1 0
A: B:2 C:1 0
A:0 B:2 C:1
A:0 B:2 1 C:
A: B:2 1 0 C:
El código para avanzar el menor se basa en mantener una variable int minor
que indica cuál de los postes contiene el menor. Inicialmente minor=0.
1 // Avanza menor
2 int next = (minor+1) % 3;
3 vv[minor].pop-back();
4 vv[next].push-back(0);
5 minor = next;
6 hprint(n,vv); moves ++;
7 if(vv[minor].size()==n) break;
22 vv[0].push-back(j);
23 hprint(n,vv);
24
25 int minor = 0;
26 int moves = 0;
27 while (1) {
28 // Avanza menor
29 int next = (minor+1) % 3;
30 vv[minor].pop-back();
31 vv[next].push-back(0);
32 minor = next;
33 hprint(n,vv); moves ++;
34 if(vv[minor].size()==n) break;
35
36 // Unica operacion que no cambia al menor
37 int lower = (minor+1)%3;
38 int higher = (minor+2)%3;
39 if (vv[higher].empty()) { }
40 else if (vv[lower].empty() | |
41 vv[lower].back() > vv[higher].back()) {
42 int tmp = lower;
43 lower = higher;
44 higher = tmp;
45 }
46 // Pasar el tope de ‘lower’ a ‘higher’
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 655
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
47 int x = vv[lower].back();
48 vv[lower].pop-back();
49 vv[higher].push-back(x);
50 hprint(n,vv); moves ++;
51
52 }
53 printf("solved puzzle in %d moves\n",moves);
54 }
0
1 0
.... 1
n ....
111111111111111111
000000000000000000
n+1 11111111111111111
00000000000000000
n+1 n
000000000000000000
111111111111111111
000000000000000000
111111111111111111 00000000000000000
11111111111111111
00000000000000000
11111111111111111
move(1,B,A)
move(n−1,B,C)
A B C A B C
0
1 0
.... 1
n ....
111111111111111111 000000000000000000
000000000000000000 111111111111111111
n+1 n+1 n
000000000000000000
111111111111111111
000000000000000000 111111111111111111
111111111111111111 000000000000000000
000000000000000000
111111111111111111
Centro Internacional de Métodos Computacionales en Ingenierı́a slide 657
((version aed-2.0.1-21-g5f1b5d2) (date Wed Oct 10 17:32:20 2007 -0300)
(processed-date Wed Oct 10 18:55:00 2007 -0300))
Algoritmos y Estructuras de Datos, por M.Storti, J.D’Elı́a, R.Paz, L. Dalcı́n, M. Pucheta (Contents-prev-up-next)
0
1 0
.... 1
n−2 ....
111111111111111111
000000000000000000
n−1
000000000000000000
111111111111111111 11111111111111111
00000000000000000
n−1
00000000000000000
11111111111111111
n−2
000000000000000000
111111111111111111 00000000000000000
11111111111111111
move(1,to,from)
move(n−1,to,aux,from)
from to aux from to aux
0
1 0
.... 1
n−2 ....
111111111111111111
000000000000000000
n−1
000000000000000000
111111111111111111 111111111111111111
000000000000000000
n−1 n−2
000000000000000000 111111111111111111
111111111111111111 000000000000000000
000000000000000000
111111111111111111
1 int main() {
2 int n = 10;
3 vv.resize(3);
4 for (int j=0; j<n; j++)
5 vv[0].push-front(j);
6 hprint(vv);
7
8 move(n,vv[1],vv[0],vv[2]);
9 printf("Solved Hanoi puzzle in %d moves\n",moves);
10 }
M (n) = 1 + 2M (n − 1)
De manera que
M (2) = 1 + 2M (1) = 3
M (3) = 1 + 2M (2) = 7
M (4) = 1 + 2M (3) = 15
...
M (n) = 2n − 1
La solución recursiva
De hecho, las dos soluciones son equivalentes, sólo difieren en la forma del
planteo.
Fixture de torneos
todos-contra-todos
F (2) = 1
F (4) = 2 + F (2) = 3
F (8) = 4 + F (4) = 7
.. ..
. = .
F (n) = n − 1
que es la solución óptima, ya que vimos que al menos hacı́an falta n − 1
fechas. Notar que este problema es equivalente a colorear un grafo, pero en
ese caso o bien podemos aplicar un algoritmo exhaustivo (que es O(n!)
no-polinomial) o bien uno exhaustivo que no garantiza la solución óptima.
Programación dinámica
Tenemos dos equipos A y B que juegan una serie de partidos hasta que
alguno de ellos gana n partidos. Por ejemplo, si n = 4 y A gana el primer,
partido B el segundo y siguiendo A, B, A, A (denotamos la secuencia total
como A, B, A, B, A, A), entonces termina ganando la serie mundial A ya
que acumuló primero 4 partidos ganados. Supongamos que en cada partido
la probabilidad de que gane A o B sea la misma. Entonces si en cierto
momento A lleva ganados 3 partidos y B lleva ganados 0, es mucho más
probable que A gane la serie mundial, ya que le falta un solo partido por
ganar, mientras que a B le faltan 4.
(A=2,B=3)
0.5 (A=2,B=2)
gana B
Ası́ se puede construir toda la tabla, hasta que alguno de los dos equipos
gana. De esta forma se puede calcular la probabilidad de que cada uno de los
equipos gane lo cual puede ser de utilidad a la hora de apostar (,).
0.25 (A=0,B=2)
0.5 (A=1,B=3)
0.25
(A=0,B=1)
(A=2,B=3) (A=1,B=2) 0.1875
0.25
0.25
0.5 (A=2,B=2) 0.125 (A=1,B=1)
0.1875
0.25 (A=2,B=1) (A=1,B=0)
0.125
(A=2,B=0)
total prob B = 0.3125
(A=2,B=0)
Es muy sencillo implementar una función recursiva que calcular PA (p, q):
1 double p-A(int p,int q) {
2 if (!p) return 1;
3 if (!q) return 0;
4 return 0.5*(p-A(p-1,q)+p-A(p,q-1));
5 }
P A(1,3) P A(2,2)
mismo elemento
mismo elemento