Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Capítulo 16.
Cuando los ítems almacenados en un árbol de búsqueda binario cambian en el tiempo, debido a
que éstos pueden ser insertados o descartados de manera no predecible, no puede asegurarse que
todas las hojas mantengan profundidades similares. Debido a esto se han desarrollado diversos
algoritmos determinísticos para lograr mantener una estructura balanceada: Árboles AVL,
árboles 2-3, árboles coloreados, árboles AA y muchos otros; todos ellos modifican la estructura
basados en pequeñas operaciones locales denominadas rotaciones y en un análisis cuidadoso de
las diferentes y determinadas situaciones que se producen.
Antes se demostró que un árbol generado aleatoriamente tiene una altura esperada que varía
logarítmicamente con respecto al número de nodos almacenados en la estructura, y que tiende a
ser balanceado.
Se estudiará uno de estos métodos que consiste en introducir un número aleatorio en cada nodo.
El número aleatorio será la prioridad del nodo. Las claves y las prioridades deben ser diferentes
y pertenecer a un universo totalmente ordenado. En el árbol, las claves están almacenadas según
un árbol binario de búsqueda, y las prioridades según una cola de prioridad. Por esta razón a la
estructura se la denomina treaps que es un acrónimo de trees y heaps.
Si las claves y las prioridades son diferentes para un conjunto determinado de valores de claves
y prioridades el treap es único; y su forma correspondería al árbol binario de búsqueda que se
obtendría al insertar, a partir de un árbol vacío, los ítems en orden de prioridad creciente.
Lo notable de esta estructura es que está basada en dos de las fundamentales que se estudian en
un curso básico de estructuras de datos: el árbol binario de búsqueda y una cola de prioridad o
heap. Si se tuviera una función de hash que genere un valor aleatorio (la prioridad), a partir de
la clave, no sería necesario almacenar el valor de prioridad en cada nodo, reduciendo el tamaño
del almacenamiento, ya que mediante la función pueden calcularse las prioridades de los nodos
cuando sea necesario disponer de ellas. En esta forma de diseño, a partir de tres estructuras
fundamentales de datos se construye una nueva.
Si se inserta un nodo con clave determinada, de manera usual, en las hojas de un árbol binario,
se tiene que todas las claves de los nodos cumplen la propiedad de un árbol binario de
búsqueda; sin embargo puede ser que la propiedad del heap, que es tener prioridades de los hijos
mayores que el padre, no se cumpla. Para reestablecer esa propiedad, al nodo recién insertado se
lo hace ascender, mediante rotaciones, mientras el padre tenga prioridad mayor. El proceso se
detiene si se encuentra un padre con prioridad menor o si el nuevo nodo se convierte en la raíz.
El descarte de un nodo se logra invirtiendo las operaciones que se realizan para insertarlo; es
decir, una vez ubicada la clave, se hace descender al nodo, mediante rotaciones con el nodo con
menor prioridad de sus hijos, hasta que el nodo sea una hoja; instancia en que se lo puede
descartar, preservándose las propiedades de los árboles binarios de búsqueda y de los heaps.
La Figura 16.1, ilustra un treap, donde en la parte superior se muestra la clave y en la inferior de
cada nodo la prioridad. El árbol se formó ingresando las claves de acuerdo a un orden creciente
de las prioridades; es decir en orden: 10, 12, 13, 18, 22, 24, 33 y 40. El nodo con menor valor
de prioridad se ubica en la raíz.
9
10
4 15
12 13
2 5 12 20
22 24 18 40
7
33
Si se inserta un nodo con clave 8 y prioridad 11, de acuerdo al algoritmo de inserción debe
agregárselo a la derecha del nodo con clave 7. Esto se muestra a la izquierda de la Figura 16.2.
Como no se cumple la propiedad que la prioridad del nodo 8 sea mayor que la de su padre,
4 15 4 15
12 13 12 13
2 5 12 20 2 5 12 20
22 24 18 40 22 24 18 40
7 8
33 11
8 7
11 33
Del nodo con clave 8 hacia abajo se tiene un heap, pero no hacia arriba. Por esto es preciso
efectuar otra rotación a la izquierda en el nodo padre del 8, el nodo con clave 5 en la Figura 16.2
a la derecha. Luego de realizada, se obtiene el diagrama a la izquierda de la Figura 16.3.
Finalmente volviendo a rotar a la izquierda en el nodo con clave 4, padre del nodo con clave 8,
se logra el treap a la derecha de la Figura 16.3.
9 9
10 10
4 15 8 15
12 13 11 13
2 8 12 20 4 12 20
22 11 18 40 12 18 40
5 2 5
24 22 24
7 7
33 33
El descarte del nodo con clave 8, sigue el proceso inverso a su inserción, se lo hace descender,
mediante rotaciones con el nodo hijo que tenga menor prioridad, hasta llegar a ser una hoja.
La operación descarte se ve simplificada si los enlaces derecho e izquierdo de las hojas apuntan
a un nodo de fondo o centinela que tenga un valor mayor de prioridad que los valores de
prioridad que puedan tener los nodos.
Entonces los condicionales, que deberían efectuarse cerca de las hojas, para asegurar la
existencia de los valores p->left->prioridad o p->right->prioridad no son necesarios.
La operación de inserción debe descender hasta las hojas y luego ascender preservando el heap.
Esto puede lograrse de manera simple mediante un diseño recursivo, o empleando un stack, para
almacenar la ruta de descenso; una vez ubicada la posición para insertar, al retornar de las
invocaciones recursivas, se dispone del nodo padre, en el cual deben realizarse las rotaciones
que corresponda.
La operación de descarte, debe descender hasta encontrar el nodo que se busca para descartar, y
luego seguir descendiendo hasta las hojas, manteniendo el heap. Para lograrlo, de manera
iterativa, es necesario mantener en el descenso un puntero al padre del nodo que está
descendiendo para ser descartado, además de la dirección de descenso; ya que es preciso
mantener los enlaces. Lo anterior también puede simplificarse si los nodos contienen un puntero
al padre, pero esto aumenta el tamaño del almacenamiento del nodo.
Si la función que genera aleatoriamente las prioridades, produjera algunos números con iguales
valores, los algoritmos de inserción y descarte se realizan normalmente. En la inserción no se
realizan rotaciones adicionales, y quedarían elementos con prioridades iguales adyacentes,
alargando la altura del último nodo ingresado; pero si esto ocurre con baja probabilidad puede
tolerarse. En el descarte, si la prioridad del nodo es igual a la de uno de sus hijos, disminuye el
largo de secuencias de igual prioridad y mejora el balance; por otro lado si los hijos tienen
iguales prioridades, tiende a aumentar la altura del último nodo. Por lo tanto es perfectamente
tolerable aceptar un generador aleatorio simple que demande bajo número de operaciones y que
produzca un limitado número de colisiones en las prioridades.
Los autores del algoritmo diseñan recursivamente ambas operaciones de actualización. También
demuestran que el valor esperado de la altura de un treap, con n nodos, es O(log(n)) , y que el
valor esperado de las rotaciones necesarias para mantener el treap es menor que 2; es decir un
número constante.
También muestran que una función de hash adecuada, para generar las prioridades, a partir de
una clave i, es un polinomio de grado 4.
Donde las constantes deben ser números aleatorios entre 0 y (U-1). Con U un número primo
mayor que n3 , con n elementos en el árbol; lo cual logra asegurar que dos elementos pueden
tener prioridades iguales con una probabilidad menor que 1/n.
16.2. Complejidad.
Una variable aleatoria indicadora I (e) asociada con el evento e de un espacio muestreal S, está
definida según:
I (e) =1 si ocurre el evento e, y 0 si el evento e no ocurre.
Se define el valor esperado como el promedio de los valores que la variable puede tomar. En un
espacio discreto, es la suma ponderada, de acuerdo a su probabilidad, de todos los valores que
puede tomar la variable X:
E[ X ] x Pr( X x)
x
Las variables aleatorias indicadoras son útiles para analizar situaciones en las que se realizan
repetidos ensayos aleatorios.
Si se emplea la variable indicadora Vi para asociarla con la producción del evento e en el
ensayo i-ésimo. Entonces la variable aleatoria V que indica el número total de eventos e en los n
ensayos, queda descrita por:
i n
V Vi
i 1
Y el valor esperado de V, se logra tomando la expectación en ambos lados de la expresión
anterior:
i n
E[V ] E[ Vi ]
i 1
Pero el operador expectación es lineal, y se tendrá que el valor esperado de la suma de dos
variables aleatorias es la suma de sus expectaciones, esto aún si las variables no son
independientes. Para el caso de la sumatoria anterior, se tiene:
i n
E[V ] E[Vi ]
i 1
16.2.2. Altura.
Sea xk el nodo que tiene la k-ésima clave menor en el treap; y ai ,k una función que vale 1 si xi es
ancestro propio de xk , y 0 en caso contrario.
Entonces el número de nodos del trayecto de xk a la raíz, está dada por:
i n
h( xk ) ai ,k
i 1
La Figura 16.4, ilustra la situación de los nodos del conjunto. Sin perder generalidad se ha
supuesto que las claves están formadas por números entre 1 y n. El diagrama a la derecha
muestra un caso en que i < k , y el izquierdo un caso en que i > k.
i i
i+1 i+1
k k
Debido a la construcción del treap, puede asegurarse que xi es un ancestro propio de xk si y sólo
si xi tiene la menor prioridad entre todos los nodos en el subconjunto X (i, k ) .
d) Si xi y xk no son la raíz y están en el mismo subárbol, se desciende hasta que uno de ellos
sea la raíz o hasta encontrar una raíz x j que los deje en subárboles diferentes y se vuelven
(inductivamente) a aplicar las consideraciones realizadas en a), b) y c).
Como cada nodo del subconjunto X (i, k ) tiene una prioridad elegida en forma independiente,
como una variable aleatoria con una distribución continua, que i tenga la prioridad menor del
conjunto ocurre con probabilidad:
1
Pr(ai ,k 1)
|k i| 1
Es decir, es uno entre todos los nodos del conjunto.
El resultado anterior permite calcular el valor esperado para la altura, definida como el número
de nodos del trayecto de xk hasta la raíz, según:
i n i k 1 i n
1 1
h Pr(ai ,k 1)
i 1 i 1 k i 1 i k 1i k 1
j k j n k 1
1 1
h 2 H (k ) H (n k 1) 2
j 1 j j 1 j
Con H (n) la serie armónica:
i n
1
H ( n) ln(n)
i 1 i
Con =0,577. Para n >>1, se tiene:
h ln(n) O(log(n))
Debemos suponer que k >1 y n > k, es decir un treap que contenga un número mayor de nodos
que la k-ésima clave menor del treap. Con estas condiciones puede realizarse, mediante Maple,
las siguientes gráficas, para ilustrar la complejidad de la altura de un treap.
> h:=sum(1/(k-i+1),i=1..k-1)+sum(1/(i-k+1),i=k+1..n);
> k:=30;
> plot([1.1*ln(n)/ln(2),h,0.8*ln(n)/ln(2)],
n=k..100*k,color=[red,black,blue],thickness=2);
Se han elegido constantes de 1,1 y 0,8 para acotar por arriba y por abajo a la función h.
Los diagramas, para la 30ava clave menor del treap, se muestran en la Figura 16.6.
Figura 16.6. Valor esperado para la altura de un nodo con valor pequeño de clave.
Puede determinarse el largo del trayecto formado por los descendientes izquierdos de la raíz
(que se define como espina o columna vertebral izquierda). Si suponemos un treap formado por
n claves y consideramos, sin perder generalidad, que las claves almacenadas en el árbol son los
números de 1 a n, podemos definir el indicador X i ,k con valor uno, si el nodo con clave i está en
la espina izquierda del subárbol con raíz k; y cero en caso contrario.
En un treap debe cumplirse que k > i y la prioridad del nodo con clave k debe ser menor que la
prioridad del nodo con clave i. Además para cada nodo con clave z, con i < z < k, debe
cumplirse que la prioridad de z debe ser menor que la prioridad del nodo con clave i.
k
(k i 1)! 1
Pr[ X i ,k 1]
(k i 1)! (k i)(k i 1)
El numerador contabiliza el número de permutaciones que pueden escribirse con las claves
desde i hasta k, es decir con (k-i+1) claves. De todas las anteriores, aquellas que comienzan en k
y terminan en i, son las que pueden generarse con las permutaciones de las cifras contenidas
entre ambas; es decir con (k-i-1) claves.
El valor esperado para el largo de la espina izquierda, es la suma de los nodos que están
presentes en la espina:
i k 1 i k 1
1
E(I ) Pr[ X i ,k 1]
i 1 i 1 (k i)(k i 1)
j k 1
1 1
E(I ) 1
j 1 j ( j 1) k
1 1 1
P(k 1) 1 1
k k (k 1) k 1
El valor esperado del largo de la espina derecha, se realiza de igual forma, pero sumando desde
1 hasta (n-k).
j n k
1 1
E ( D) 1
j 1 j ( j 1) n k 1
Si xk es la raíz, la espina izquierda se calcula sumando Pr[ X i ,k 1] desde i igual a 1 a (k-1); para
la espina derecha pueden cambiarse los índices para sumar desde i igual 1 hasta (n-k), según se
muestra en la Figura 16.8. A la derecha se ilustra con i=5, k=8 y n=11.
k 8
El número de rotaciones necesarias para insertar una determinada clave es el mismo que el
número de rotaciones para descartarla. Para descartar un clave se realizan rotaciones que la
hacen descender; en cada rotación a la izquierda la espina derecha disminuye en uno, y en cada
rotación a la derecha la espina izquierda disminuye en uno. Al llegar la clave a la posición de
una hoja, ambas espinas son cero. Entonces el número de rotaciones para descartar una clave es
la suma de las espinas derecha e izquierda de esa clave, antes de descartarla.
Los siguientes comandos Maple, muestran las gráficas del número de rotaciones para tres
valores de k.
> rot:=2-1/k-1/(n-k+1);
> subs(k=1,rot),subs(k=n/2,rot),subs(k=n,rot);
1 2 1 1
1 ,2 ,1
n n 1 n
n 1
2
> plot([subs(k=1,rot),subs(k=n/2,rot),subs(k=n,rot)],n=1..20,
color=[red,black,blue],thickness=2);
Para k=n/2, el número de rotaciones es menor que 2, lo que se muestra en la Figura 16.9.
Se emplea un tipo especial para la clave, para facilitar los cambios en caso que la clave no sea
entera.
typedef int data;
void initglobalvariables()
{ nil=¢inela;
nil->prioridad = UINT_MAX; //
nil->left = nil; nil->right = nil;
}
Si la clave no es numérica puede tomarse q(&p), para generar la prioridad del nodo.
Las rotaciones se implementan como macros. Se agregan globales para contar las rotaciones, en
el momento de la depuración, luego pueden eliminarse los contadores.
int opl=0;
int opr=0;
#define rrotm(t) do { \
{ temp=t; \
t = t->left; \
temp->left = t->right; \
t->right = temp; \
opr++; \
} \
} while(0)
#define lrotm(t) do { \
{ temp=t; \
t = t->right; \
temp->right = t->left;\
t->left= temp; \
opl++; \
} \
} while(0)
16.7. Insertar.
#define raiz 2
#define izq 1
#define der 0
#define AjustaPadre(p) if (dir==der) pp->right=(p); else if(dir==izq) pp->left=(p);else if(dir==raiz)*t=(p);
int espina=0;
int espinas(pnodo t)
{ int n=0;
pnodo p;
if (t!=NULL)
if (t!=nil )
{ p=t->left; //cuenta descendientes de t en espina izquierda
while (p!=nil) { n++; p=p->left;}
p=t->right; //cuenta descendientes de t en espina derecha
while (p!=nil) { n++; p=p->right;}
}
return n;
}
La función check efectúa un recorrido en el árbol, verificando las propiedades del treap en cada
nodo.
void check(pnodo p)
{ int k, tipo;
if (p!= nil)
{ check(p->left);
k=0;
//Hay error si prioridad del padre mayor o igual que la del hijo izq
if (p->left->prioridad < p->prioridad ) {tipo=1; k++;}
if (p->left->prioridad == p->prioridad ) {tipo=2; k++;} //warning
//Hay error si prioridad del padre mayor que la del hijo der
if (p->right->prioridad < p->prioridad ){tipo=3;k++;}
if (p->right->prioridad == p->prioridad ){tipo=4;k++;}//warning
//Se pueden agregar algunos test para verificar la estructura de árbol de búsqueda
//hijo izquierdo debe tener clave menor que su padre
if(p->left !=nil) if (p->left->clave >= p->clave ) {tipo=5; k++;}
//hijo derecho debe tener clave mayor que su padre
if(p->right !=nil) if(p->right->clave <= p->clave ) {tipo=6; k++;}
if(k)
{ if(tipo==2 || tipo==4) printf("Warning=%d %d \n", tipo, p->clave);
else printf("Error=%d %d \n", tipo, p->clave);
// prtprioridades(tree); putchar('\n');
}
check(p->right);
}
}
#define N …
int instrucciones=0;
int main(void)
{ int i;
clock_t start, stop; //tipo definido en time.h
int totaltime = 0;
initglobalvariables();
tree=nil;
start = clock();
srand(1);
for(i=1; i<=N; i++)
{ int aleatorio=rand()%1023;
tree=insertar(aleatorio, tree);
check(tree);
espina+=espinas(Buscar(aleatorio, tree));
//prtinorder(tree); putchar('\n');
}
instrucciones+=N;
E16.1.
A veces es deseable partir un conjunto de items X en dos conjuntos. Uno X1 con las claves de X
que son menores que a; y otro X2 con las claves mayores que a. O bien es necesario unir dos
conjuntos X1 y X2, cuando se asume que las claves del conjunto X1 son menores que las claves
del conjunto X2.
Estas operaciones son fáciles de implementar con las operaciones de inserción y descarte en un
treap. Para partir (split) un treap que almacena X, basta insertar la clave a, con prioridad
mínima, lo cual lleva dicho ítem a la raíz; dejando X1 como el subárbol izquierdo y X2 como el
subárbol derecho. Para unir o mezclar (join) se forma un treap con una raíz con prioridad
máxima, y con subárbol izquierdo el treap formado por X1, y con subárbol derecho el treap
formado por X2; luego se descarta la raíz.
E16.2.
Modificar la función de inserción cuando se pasa la dirección del nodo antecesor o sucesor de la
clave que será insertada.
Modificar la función de descarte cuando se pasa la dirección del nodo será descartado.
E16.3.
E16.4.
Referencias.
Raimund Seidel, Cecilia Aragon. “Randomized search trees”. Algorithmica 16:464-497, 1996.