07 - Árboles Binarios de Búsqueda - Tema 19 - Book-Estructuras de Datos en Java 4ed Weiss (Legible) - 561-646
07 - Árboles Binarios de Búsqueda - Tema 19 - Book-Estructuras de Datos en Java 4ed Weiss (Legible) - 561-646
07 - Árboles Binarios de Búsqueda - Tema 19 - Book-Estructuras de Datos en Java 4ed Weiss (Legible) - 561-646
Rara grandes cantidades de entrada, el tiempo de acceso lineal de las listas enlazadas resulta
prohibitivo. En este capítulo vamos a analizar una alternativa a la lista enlazada: el árbol de
búsqueda binaría una estructura de datos simple que se puede contemplar como una extensión del
algoritmo de búsqueda binaria para permitir inserciones y borrados. El tiempo de ejecución para la
mayoría de las operaciones es Óflog N) como promedio. Lamentablemente, el tiempo del caso peor
es O{N) por cada operación.
Eh este capítulo veremos
■ El árbol de búsqueda binaria básico.
■ Un método para añadir estadísticas de orden (es decir, la operación findKth).
■ Tres formas distintas de eliminar el caso peor O(N)'. en concreto el árbol A VL. el árbol rojo-
negro y el árbol-AA.
■ La implementación del TreeSet y el TreeHap de la API de Colecciones.
■ La utilización del árbol-Bym buscar de forma rápida en una base de datos de gran tamaño.
19 1 Ideas básicas
Bi el caso general, buscamos un elemento utilizando su clave. Por ejemplo, podríamos buscar el
examen correspondiente a un estudiante utilizando su identiflcador ID de estudiante. En este caso,
el número ID se denomina clave del elemento.
El árbol de búsqueda binaríasatistox la propiedad de búsqueda ordenada:
es decir, para todo nodo Zdel árbol, los valores de todas las claves contenidas toa cuaiqder nodo del
en el subárbol izquierdo son menores que la clave de Xy los valores de todas ttxt tte Wsyjeda Uñaría.
todos bs nodos oon
las claves contenidas en el subárbol derecho son mayores que la clave de X. daves mas pequeñas se
El valor que se muestra en la Figura 19.1 (a) es un árbol de búsqueda binaria, encuentran en d subárbol
tzqderd o y todos bs nodos
pero el árbol mostrado en la Figura 19.1 (b) no lo es, porque la clave 8 no con daves mayores se
corresponde al subárbol izquierdo de la clave 7. Esta propiedad del árbol de encuentran en el sutórbol
búsqueda binaria implica que todos los elementos del árbol se pueden ordenar derecho. Hose permten
dupfcados.
de forma coherente (de hecho, un recorrido en orden nos proporciona los
elementos ordenados). Asimismo, esta propiedad Implica que no se permiten
678 Capitulo 19 Árboles de búsqueda binaria
elementas duplicados. Podríamos permitir esos elementos duplicados, pero suele ser mejor, general
mente, almacenar los diferentes elementos que tienen claves idénticas en una estructura secundaria.
Si estos elementos fueran duplicados exactos, lo mejor es mantener un elemento y llevar la cuenta
del número de duplicados.
Propiedad de orden en un árbol de búsqueda binaria
En un árbol de búsqueda binaría, para todo nodo X todas las claves contenidas en el árbol
izquierdo de X tienen valores inferiores al de la clave de Xy todas las claves contenidas en el
árbol derecho de Xtienen valores superiores al de la clave de X.
Rgura 19.1 Dos arboles binarlos: (a) ui árbol de búsqueda: (b) no es un árbol de búsqueda
19.1 Ideas básicas 679
Rflura 19.2 Mote de búsqueda binaria (a) ante de la inserción y (b) despite de la insertion de 6.
dón de un nodo puede dejar desconectadas panes del árbol. Si eso sucede, La operation remove es
deberemos reasocíar con cuidado el árbol manteniendo la propiedad de orden dfidl porque te nodos que
no son hojas mantienen
que hace de él un árbol de búsqueda binaria. También queremos evitar tener
ti árbol conectado y no
que hacer el árbol innecesariamente profundo, porque la profundidad del qxromos que quede
árbol afecta al tiempo de ejecución de los algoritmos del árbol. dwronetiado despite dti
oorraoo.
Cuando estamos diseñando un algoritmo complejo, a menudo lo más fácil
es resolver primero el caso más simple, dejando el caso más complicado para
el final. Por tanto, al examinar los diversos casos, vamos a comenzar por el S un nodo tiene un hjo. se
más fácil. Si el nodo es una hoja, su eliminación no desconecta el árbol, por te puede tiWrar haciendo
lo que lo podemos borrar de forma inmediata. Si el nodo tiene solo un hijo, que su padre b soslaye. La
taües un caso espodti,
podemos eliminarlo después de ajustar el enlace que su padre tiene para porque no done padre
apuntar al nodo con el fin de soslayar este. Esto se ilustra en la Figura 19.3,
con la eliminación del nodo 5. Observe que removeMln y removeMax no son
operaciones complejas, porque los nodos afectados o son hojas o solo tienen un hijo. Observe que
la raíz también es un caso especial, porque carece de padre. Sin embargo, al implementar el método
remove, el caso especial se gestiona de manera automática.
El caso más complicado es aquel en el que un nodo tiene dos hijos. La Uh nodo con doshjos
estrategia general consiste en reemplazar el elemento de este nodo por el se sustituye udbando ti
tiernento más pequeño
elemento más pequeño del subárbol derecho (que puede ser localizado
del subárbol derecho. A
fácilmente, como ya hemos mencionado antes) y luego eliminar ese nodo condnuadún, se efchha
(que ahora estará lógicamente vacío). Esa segunda operación remove es fácil ose otro nodo
de hacer porque, como acabamos de indicar, el nodo mínimo de un árbol no
Rgura 19.3 Borrado del nodo 5 con tn NJo: (a) ante y (b) despite.
680 Capitulo 19 Árboles de búsqueda binaria
Rgura 19.4 Borrado dei nodo 2 con dos NJos: (a) antes y (b) después.
tiene un hijo izquierdo. La Figura 19.4 muestra un árbol inicial y el resultado de eliminar el nodo
2. Sustituimos el nodo por el nodo más pequeño (3) de su subárbol derecho y luego eliminamos 3
del subárbol derecho. Observe que en todos los casos, eliminar un nodo no hace que el árbol sea
más profundo? Muchas alternativas sí que hacen que el árbol sea más profundo; por tanto, esas
alternativas son opciones menos recomendables.
Sn embargo. el borrado puede incrementar la profundidad media de nodo, si se elimina un nodo poco profundo.
19.1 Ideas básicas 681
! package weiss.nonstandard:
2
3 // Nodo básico almacenado en árboles de búsqueda binaria no equilibrados.
4 // Observe que esta clase no es accesible fuera
5 // de este paquete.
6
7 class BinaryNode<AnyType>
8 I
9 // Constructor
!0 BinaryNode( AnyType theElement )
ll {
¡? element ® theElement:
13 left - right - nul1:
14 1
!5
16 // Datos: accesibles por parte de otras rutinas del paquete
17 AnyType element: // Los datos del nodo
18 BinaryNode<AnyType> left: // Hijo izquierdo
19 BinaryNode<AnyType> right: // Hijo derecho
20 1
0 método insert añade x al árbol actual, invocando la rutina oculta insert con root como
parámetro adicional. Esta acción falla si x ya se encuentra en el árbol; en ese caso, se genera una
excepción DuplicateltemException. Las operaciones findHin, findHax y find devuelven el
elemento mínimo, el elemento máximo o el elemento indicado (respectivamente) del árbol. Si no
se encuentra el elemento, porque el árbol está vacío o porque el elemento indicado no está presente,
entonces se devuelve nul1. La Figura 19.7 muestra el método privado el ementAt que implementa
la lógica element At.
La operación removeHl n elimina el elemento mínimo del árbol; genera una excepción si el árbol
está vacío. La operación remove elimina un elemento especificado x del árbol; genera una excepción
en caso necesario. Los métodos make Empty e is Empty utilizan la lógica habitual.
Como suele ser típico en la mayoría de las estructuras de datos, la operación f i nd es más fácil
que Inserte insert es más fácil que remove. La Figura 19.8 ilustra la rutina find. Mientras que no
se alcance un enlace nuil, tendremos una correspondencia o necesitaremos bifurcamos a izquierda
o a derecha. 0 código implementa este algoritmo de forma bastante sucinta. Observe el orden de
las comprobaciones. La comparación con nul l debe realizarse en primer lugar; en caso contrario, el
acceso a t.element sería ilegal. Las restantes comprobaciones se ordenan poniendo en último lugar
el caso menos probable. Es posible hacer una implementación recursiva, pero nosotros empleamos
en su lugar un bucle; utilizaremos la recursión en los métodos insert y remove. En el Ejercicio
19.16 le pediremos que escriba los algoritmos de búsqueda de forma recursiva.
Capitulo 19 Árboles de búsqueda binaria
1 package weiss.nonstandard:
í.
3 // Clase BinarySearchTree
4 //
5 // CONSTRUCCIÓN: sin ningún inicializador
6 //
7 // ************ *****«OPERACIONES PÚBLICAS*********************
8 // void insertí x > -•> Insertar x
9 // void removeí x ) ••> Eliminar x
10 // void removeMiní ) --> Eliminar elemento mínimo
ij // Comparable findí x ••> Devolver elemento que se corresponde con x
12 // Comparable findMiní ) --> Devolver el menor elemento
13 // Comparable findMaxí ) --> Devolver el mayor elemento
14 // boolean isEmptyí ) ••> Devolver true si está vacío: false, en otro caso
15 // void makeEmptyi ) --> Eliminar todos les elementos
16 II ***************** «ERRORES********************************
17 II insert, remove y removeMin generan excepciones en caso necesario
18
19 public class BinarySearchTree<AnyType extends Comparable<? super AnyType>>
20 I
21 public BinarySearchTreei )
22 I root «nuil: I
23
24 public void insertí AnyType x )
25 I root = insertí x. root ): I
26 public void removeí AnyType x )
27 I root - removeí x. root ): I
28 public void removeMiní )
29 I root = removeMiní root ): I
30 public AnyType findMiní )
31 I return elementAtí findMiní root ) ): I
3? public AnyType findMaxí )
33 I return elementAtí findMaxí root ) ): I
34 public AnyType findí AnyType x )
35 I return elementAtí findí x. root ) ): I
36 public void makeEmptyi )
37 I root «nuil: I
38 public boolean isEmptyí )
39 I return root — null: 1
40
41 private AnyType elementAtí BinaryNode<AnyType> t )
4? 1 /* Figura 19.7 */ I
43 private B1naryNode<AnyType> findí AnyType x, BinaryNode<AnyType> t )
44 I /* Figura 19.8 */ 1
45 protected 8inaryNode<AnyType> findMiní BinaryNode<AnyType> t )
46 { /* Figura 19.9 */ 1
ConMnda
Figura 19.6
i /**
2 * Método interno para obtener el campo element.
3 * ©param t el nodo.
4 * ©return el campo element o nuil si t es nuil.
5 */
6 private AnyType elementAtí BinaryNode<AnyType> t )
7 (
a return t — null ? null : t.element:
9
1 /**
2 * Método interno para encontrar un elemento en un subárbol.
3 * ©param x es el elemento que hay que buscar.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * ©return el nodo que contiene el elemento encontrado.
6 ♦/
7 private BinaryNode<AnyType> findí AnyType x. 8inaryNode<AnyType> t )
8 l
9 whileí t !” nuil )
10 I
; i ifí x.compareToí t.element ) < 0 )
12 t » t.left:
13 else ifí x.compareToí t.element ) > 0 )
i4 t - t.right:
15 el se
16 return t: // Encontrada correspondencia
17 1
18
19 return null: // No encontrada
20
A primera vista, instrucciones como t~t.left parecen cambiar la raíz del árbol. Sin embargo,
no es así. porque t se pasa por valor. En la llamada inicial, t es simplemente una copiaáe root.
Aunque t cambia, root no lo hace. Las llamadas a findHin y findHax son
DeMdo a tatemada por
valor. U argumento real
aun más simples, porque la bifurcación se realiza siempre incondicionalmente
(root) no varía. en una única dirección. Estas rutinas se muestran en la Figura 19.9. Observe
oómo se maneja el caso de un árbol vacío.
La rutina Insert se muestra en la Figura 19.10. Aquí utilizamos la
tera insert, debemos recursión para simplificar el código. También es posible una implementación
devolverla nueva raíz dei
atol y reconectare! atol
no recursiva; aplicaremos esta técnica cuando hablemos de los árboles rojo-
negro posteriormente en el capítulo. El algoritmo básico es simple. Si el
árbol está vacío, podemos crear un árbol de un solo nodo. La comprobación
se lleva a cabo en la línea 10, y el nuevo nodo se crea en la línea 11. Observe que, como antes, los
cambios locales a t se pierden. Así, devolvemos la nueva raíz, t. en la línea 18.
1 /**
2 * Método interno para encontrar el elemento mínimo en un subárbol.
3 * ©param t el nodo que actúa como raíz del árbol.
4 * ©return el nodo que contiene el elemento mínimo.
5 */
6 protected 31naryNode<AnyType> findMiní BinaryNode<AnyType> t )
7 {
8 ifí t !» nuil )
9 whileí t. 1 eft != nul1 )
10 t = t.left:
11
12 return t:
13 I
14
15 /♦♦
16 * Método interno para encontrar el elemento máximo en un subárbol.
1/ * ©param t el nodo que actúa como raíz del árbol.
18 * ©return el nodo que contiene el elemento máximo.
19 */
20 private 8inaryNode<AnyType> findMaxí BinaryNode<AnyType> t )
21 {
22 if( t != nul1 )
23 whilei t.right 1= null )
24 t = t. right:
25
26 return t:
27 |
Rgura 19.9 Los métodos f inown y fInoMax para atoles de búsqueda Uñarla
19.1 Ideas básicas 685
j /**
2 * Método interno para insertar en un subárbol.
3 * ©param x el elemento que hay que insertar.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * ©return la nueva raíz.
6 * @throws OuplicateltemExcepticn si x ya esté presente.
7 */
8 protected BinaryNode<AnyType> insertí AnyType x. BinaryNode<AnyType> t )
9 {
10 ifí t = nuil )
i! t - new BinaryNode<AnyType>í x ):
i? else if( x.compareToí t.element ) < 0 )
13 t.left - insertí x. t.left ):
14 else if< x.compareToí t.element ) > 0 )
15 t.right ® insertí x. t.right ):
16 else
17 throw new DuplicateltemExceptloni x.toStringi ) ): // Duplicado
18 return t;
19 I
Si el árbol no está ya vacío, tenemos tres posibilidades. En primer lugar, si el elemento que
tenemos que insertar es más pequeño que el elemento contenido en el nodo t, podemos invocar
insert recursivamente sobre el subárbol izquierdo. En segundo lugar, si el elemento es más grande
que el elemento contenido en el nodo t. podemos invocar Insert recursivamente sobre el subárbol
derecho (estos dos casos están codificados en las líneas 12 a 15). En tercer lugar, si el elemento que
hay que insertar se corresponde con el elemento contenido en t, generamos una excepción.
Las restantes rutinas se ocupan del borrado. Como hemos descrito anteriormente, la operación
removeMi n es simple poique el nodo mínimo no tiene ningún hijo izquierdo. Por tanto, lo único que
hay que hacer es soslayar el nodo eliminado, lo que parece exigimos llevar la cuenta de quién es
el padre del nodo actual a medida que vamos descendiendo por el árbol. Pero, de nuevo, podemos
evitar el uso explícito de un enlace al padre, utilizando la recursión. El código se muestra en la
Figura 19.11.
Si el árbol t está vacío, removeMln falla. En caso contrario, si t tiene un la raíz del nuevo sutotoi
hijo izquierdo, eliminamos recursivamente el elemento mínimo del subárbol óefce ser devuelta en las
ruinas reaove, loque
izquierdo a través de la llamada recursiva de la línea 13. Si alcanzamos la
tocomos en b práctica os
línea 17, sabemos que estamos actualmente en el nodo mínimo, y por tanto t mantener al padre en b pita
será la raíz de un subárbol que carece de hijo izquierdo. Si asignamos a t el do recursion.
! /**
2 * Método interno para eliminar el elemento mínimo de un subárbol.
3 * ©param t el nodo que actúa como raíz del árbol.
4 * @return la nueva raiz.
5 * ©throws ItemNotFoundException sit está vacío.
6 */
7 protected 8inaryNode<AnyType> removeMiní 8inaryNode<AnyType> t )
8 I
9 if C t = nul l )
10 throw new ItemNotFoundExceptioní ):
l! else if( t.left I- nuil )
¡2 I
13 t.left - removeMiní t.left ):
14 return t:
15 )
16 else
17 return t.right:
18 I
llamada recursiva. El método que tiene p como parámetro (en otras palabras, el método que invocó
al método actual) cambia p.left al nuevo t. Así. el enlace left del padre hace referencia a t y
el árbol queda conectado. En conjunto, es una maniobra bastante ingeniosa -hemos mantenido el
padre en la pila de recursión. en lugar de mantenerlo explícitamente en un bucle iterativo.
Habiendo utilizado este truco para el caso simple, podemos a continuación adaptarlo para la rutina
general remove mostrada en la Figura 19.12. Si el árbol está vacío, la operación remove no tendrá
éxito y podemos generar una excepción en la línea 11. Si no encontramos una correspondencia,
podemos invocar reraove recursivamente para el subárbol izquierdo o derecho según sea apropiado.
Eh caso contrario, llegaremos a la línea 16. lo que indica que hemos encontrado el nodo que hay que
eliminar.
Recuerde (como se ilustra en la Figura 19.4) que. si hay dos hijos,
la rutina reaove
hefuye algunos (rucos de
sustituimos el nodo por el elemento mínimo del subárbol derecho y luego
axffcadon.peronoes eliminamos ese mínimo del subárbol derecho (lo que está codificado en las
demasiado compleja si se líneas 18 y 19). En caso contrarío, tendremos uno o cero hijos. Si hay un hijo
utiLrab recursion. los
casos correspondentes a izquierdo, hacemos t igual a su hijo izquierdo, como haríamos en removeMax.
w tínico hjo, a bratz con Eh caso contrario, sabemos que no hay hijo izquierdo y que podemos hacer t
un hjo y a cero NJos se
igual a su hijo derecho. Este procedimiento está codificado de forma sucinta
raían todos dos <fe forma
conjunta en b linea 22. en la línea 22. que también cubre el caso de un nodo hoja.
Hay dos observaciones que hacer con respecto a esta implementación.
Eh primer lugar, durante las operaciones básicas insert, find o reraove.
utilizamos dos comparaciones de tres vías por cada nodo al que accedemos, para distinguir entre los
casos <. - y >. Obviamente, podemos calcular x. compareToí t. el emen t) una vez por cada iteración
de bucle y reducir el ooste a una comparación de tres vías por nodo. Sin embargo, en realidad
19.2 Estadísticas de orden 687
! /**
2 * Método interno para eliminar de un subárbol.
3 * ©param x el elemento que hay que eliminar.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * ©return la nueva raíz.
6 * @throws ItemNotFoundException si no se encuentra x.
7 */
8 protected BinaryNode<AnyType> removeí AnyType x. BinaryNode<AnyType> t )
9 {
10 ifí t = nuil )
i! throw new ItemNotFoundExceptioní x.toStringi ) ):
i? ifí x.compareToí t.element ) < 0 )
13 t.left - removeí x. t.left ):
14 else if< x.compareToí t.element ) > 0 )
15 t.right ® removeí x. t.right ):
16 else ifí t.left l= null && t.right í= nuil ) // Dos hijos
17 {
18 t.element » findMiní t.right ).element:
19 t.right - removeMiní t.right ):
20 )
?l else
2? t - í t.left 1- nuil ) ? t.left : t.right:
23 return t:
?4 |
podemos apañamos con una sola comparación de dos vías por nodo. La estrategia es similar a la que
aplicamos en el algoritmo de búsqueda binaria de la Sección 5.6. Hablaremos de la aplicación de
esta técnica a los árboles de búsqueda binaria en la Sección 19.6.2. cuando ilustremos el algoritmo
de borrado para árboles AA.
Eh segundo lugar, no necesitamos utilizar recursión para llevar a cabo la inserción. De hecho,
tna implementación recursiva es probablemente más lenta que una implementación no recursiva.
Expondremos una implementación iterativa de insert en la Sección 19.5.3 en el contexto de los
árboles rojo-negro.
Rgura 19.13 UBIIratíón dei miembro de datos size para implementar findKth.
19.2 Estadísticas de orden 689
1 package weiss.nonstandard:
2
3 // Clase BinarySearchTreeWithRank
*♦4 //
5 // CONSTRUCCIÓN: sin ningún inicializador
6 //
7 II *****************‘OPERACIONES PÚBLICAS*********************
8 // findKth( k ) Comparable--> Oevolver k-ésimo elemento más pequeño
9 // Todas las demás operaciones se heredan
10 ¡ ¡ ******************eRRORES********************************
11 // Se genera IllegalArgumentException si k está fuera de límites
1?
13
public class BinarySearchTreeWithRankKAnyType extends Comparab1e<? super AnyType»
14 extends BinarySearchTree<AnyType>
15
I
16 private static class 8inaryNodeKithSize<AnyType> extends 8inaryNode<AnyType>
i7 1
18 BinaryNodeWithSize! AnyType x )
19 I super! x ): size - 0: 1
20
21 int size:
22 I
23
24 /•*
25 * Encontrar el k-ésimo elemento más pequeño del árbol.
26 * ©param k el rango deseado (1 es el elemento más pequeño).
27 * @return el k-ésimo elemento más pequeño del árbol.
28 * ©throws IllegalArgumentException si k es menor que 1
29 * o mayor que el tamaño del subárbol.
30 */
3! public AnyType findKth! int k )
32 I return findKth! k. root ).element: I
33
34 protected BinaryNode<AnyType> findKth! int k. B1naryNode<AnyType> t )
36 I /* Figura 19.15 */ 1
36 protected BinaryNode<AnyType> insert! AnyType x. BinaryNode<AnyType> tt )
37 ( /* Figura 19.16 ♦/ I
38 protected B1naryNode<AnyType> remove! AnyType x. BinaryNode<AnyType> tt )
39 1 /* Figura 19.18 */ 1
40 protected BinaryNode<AnyType> removeMin! 8inaryNode<AnyType> tt )
4! I /* Figura 19.17 */ I
42
la operación findKth 1.a operación findKth mostrada en la Figura 19.15 está escrita recur
se puedo Implemertar sivamente. aunque es obvio que no tendría por qué ser así. Sigue la descrip
tadmente una vez que se
conocen bs valores de tosción del algoritmo de forma textual. La comprobación con respecto a nuil en
miembros de datos que la línea 10 es necesaria, porque k podría tener un valor no legal. Las líneas
hdfcan el tanate 12 y 13 calculan el tamaño del subárbol izquierdo. Si el subárbol izquierdo
existe, obtenemos la respuesta necesaria accediendo a su miembro de datos
si ze. Sí el subárbol izquierdo no existe, podemos asumir que su tamaño es
las operaciones Insert y
remove son potondaknede igual a 0. Observe que esta comprobación se realiza después de aseguramos
complejas, porque no deque t no es nuil.
leñemos que achatarte La operación Insert se muestra en la Figura 19.16. La parte poten
wormaoonoe tamaño sta
Operación no Sene dito. cialmente compleja es que, si la llamada para la inserción tiene éxito, necesi
tamos incrementar el miembro de datos size de t. Sí la llamada recursiva
falla, el miembro de datos si ze de t no se ve modificado y debe generarse
tita excepción. En una inserción que no tenga éxito, ¿pueden verse modificados algunos tamaños?
La respuesta es no; si ze solo se actualiza si la llamada recursiva tiene éxito sin que se genere una
excepción. Observe que cuando se asigna a un nuevo nodo mediante una llamada a new. el miembro
de datos size se configura con el valor 0 en el constructor B1naryNodeW1thS1ze, y luego se
incrementa en la línea 20.
La Figura 19.17 muestra que hemos utilizado el mismo truco para removeMln. Si la llamada
recursiva tiene éxito, se reduce el valor del miembro de datos size: si la llamada recursiva falla.
s1 ze no se ve modificado. La operación remove es similar y se muestra en la Figura 19.18.
i /**
?. * Método Interno para encontrar el k-és1mo elemento mínimo de un subárbol.
3 * ©param k el rango deseado (1 es el elemento mínimo).
4 * ©return nodo que contiene el k-ésimo elemento más pequeño de un subárbol.
5 * ©throws IllegalArgumentException si k es menor que 1
6 * o mayor que el tamaño del subárbol.
7 */
8 protected B1naryNode<AnyType> findKth( 1nt k. B1naryNode<AnyType> t )
9 {
10 1f( t — nuil )
11 throw new IIlegalArgumentExceptioni ):
l? int leftSize » ( t.left I® null ) ?
13 ((8inaryNodeWithSize<AnyType>) t.left).size : 0;
14
15 1f( k <« leftSize )
16 return findKthi k. t.left ):
1? 1f( k — leftSize + 1 )
18 return t:
19 return findKthi k - leftSize • 1. t.right ):
?0 I
Rgura 19.1 S la operación f 1 ndKth para un áitri de búsqueda con estadteScas de orden.
19.2 Estadísticas de orden 691
! /**
? * Método interno para insertar en un subárbol.
3 * ©param x el elemento que hay que insertar.
4 * ©param tt el nodo que actúa como raíz del árbol.
5 * @return la nueva raí2.
6 * ©throws DuplicateltemException si x ya está presente.
7 */
8 protected BinaryNode<AnyType> insertí AnyType x, B1naryNode<AnyType> tt )
9 I
10 BinaryNodeWíthSize<AnyType> t - íBinaryNodeWithSize<AnyType>) tt:
i¡
1? ifí t == nuil )
13 t - new BinaryNodeWithSize<AnyType>( x );
14 else ifí x.compareToí t.element ) < 0 )
15 t.left » insertí x. t.left ):
16 else ifí x.compareToí t.element ) > O )
17 t.right ® insertí x, t.right ):
18 else
19 throw new DuplicateltemExceptloni x.toStringi ) ):
20 t.$ize++:
?! return t:
?.2 I
Rgura 19.16 La operación insert para in árbol de búsqueda con estadísticas de orden.
1 /**
? * Método interno para encontrar elemento más pequeflo de un subárbol.
3 * ajustando los campos de tamaño según sea apropiado.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * ©return la nueva raíz.
6 * ©throws ItemNotFoundException si el subárbol está vacío.
7 */
8 protected BinaryNode<AnyType> removeMiní BinaryNode<AnyType> tt )
9 I
10 BinaryNodeWithSize<AnyType> t - ÍBinaryNodekithS1ze<AnyType>) tt:
i!
\? i f( t nul1 )
13 throw new ItemNotFoundExceptioní ):
14 if( t.left = nul1 )
15 return t.right:
16
17 t.left ’ removeMiní t.left ):
18 t.size-•:
19 return t:
?0 |
Rgura 19.17 la operation reaoveiu n para un árbol de búsqueda con estadSí cas de orden.
692 Capítulo 19 Árboles de búsqueda binaria
! /**
? * Método interno para eliminar de un subárbol.
3 * ©param x el elemento que hay que eliminar.
4 * @param t el nodo que actúa como raíz del árbol.
5 * ©return la nueva raíz.
6 * ©throws ItemNotFoundException si no se encuentra x.
7 */
8 protected B1naryNo<Je<AnyType> removeí AnyType x. BinaryNode<AnyType> tt )
9 {
¡o BinaryNodeWithSíze<AnyType> t - (BinaryNodeWithSize<AnyType>) tt:
11
12 ifí t = nuil )
13 throw new ItemNotFoundExceptioní x.toStringi ) ):
14 if( x.compareToí t.element > < 0 )
15 t.left « removeí x, t.left ):
16 else 1f( x.compareToí t.element ) > 0 )
17 t.right = removeí x. t.right ):
18 else ifí t.left !- null && t.right í- nuil ) // Dos hijos
19 i
20 t.element = findMiní t.right ).element:
21 t.right - removeMiní t.right ):
?.2 I
23 el se
24 return ( t.left !- nuil ) ? t.left : t.right:
25
26 t.size-•:
27 return t:
28 I
Rgura 19.18 La operación renove para ir árbol de búsqueda con estatificas de orden.
Rgura 19.19 (a) Ei árbol eqtfllbrado tiene una profanidad de liog NI (b) El árbol no equilibrado Cene una proíunddad de N I
Rgura 19 JO Mióles de búsqueda binarla que se pueden obtener al Insertar una permutación de 1.2 y 3: el árbol eqJJbrado mostrado en (c) se
obtiene con una probabilidad dos veces mayor que cualquiera de los otos.
694 Capitulo 19 Árboles de búsqueda binaria
pueden obtenerse como resultado de estas inserciones se muestran en la Figura 19.20. Observe que
el árbol con raíz 2, mostrado en la Figura 19.20(c), se forma a partir de la secuencia de inserción (2,
3, 1) o de la secuencia (2, 1, 3). Por tanto, algunos árboles son más probables de obtener que otros y.
oomo veremos, los árboles equilibrados tienen una mayor probabilidad de obtenerse que los árboles
no equilibrados (aunque este resultado no es evidente a partir del caso de solo tres elementos).
Comenzamos con la siguiente definición.
La hngr'iirf edema
El algoritmo de inserción implica que el coste de una inserción es igual
eamhoseuBtopara mear al coste de una búsqueda que no tenga éxito, la cual se mide utilizando la
el costo óe ira búsqueda longitud extema de camino. En una inserción o en una búsqueda que no
que no tonga dito
tenga éxito, terminaremos por encontramos con la comprobación t—nuil.
Recuerde que en un árbol de Anodos hay N + 1 enlaces nul l. La longitud
externa de camino mide el número total de nodos a los que se accede, incluyendo el nodo nuil
para cada uno de estos N + 1 enlaces nul 1. El nodo nuil se denomina en ocasiones nodoexterno
del árbol, lo que explica el término longitud externa de camino. Como veremos más adelante en el
capítulo, puede ser conveniente sustituir el nodo nul 1 por un nodo centinela.
19.3 Análisis de las operaciones con árboles de búsqueda binaria 695
Rara cualquier árbol T, sea JPL(7) ta longitud intema de camino de Ty sea £P£(7)
su longitud externa de camino.Entonces.si 7’tiene A/nodos, EPL{7) = IPL{T) + 2 Ai
Demostración Este teorema se demuestra por inducción y se deja como Ejercicio 19.8 para el lector.
Resulta tentador decir inmediatamente que estos resultados implican que el tiempo medio de
ejecución de todas las operaciones es <?(log N). Esta implicación es cierta en la práctica, pero no
la hemos demostrado analíticamente, poique la suposición empleada para demostrar los resultados
anteriores no tiene en cuenta el algoritmo de borrado. De hecho, un examen atento sugiere que
podríamos tener problemas con nuestro algoritmo de borrado, porque la operación remove siempre
sustituye los nodos bonados de dos hijos por un nodo del subárbol derecho. Este resultado parecería
tener el efecto de llegar a desequilibrar el árbol, que tendería a estar escorado hacia la izquierda.
Se ha demostrado que si construimos un árbol de búsqueda binaria aleatorio y luego realizamos
aproximadamente A/2 parejas de combinaciones Insert/remove aleatorias, los árboles de búsqueda
binaria tendrán una profundidad esperada de O(\In ). Sin embargo, un
número razonable de operaciones Insert y remove aleatorias (en las que el Las operaciones recove
aleatorias no preservan
orden de Insert y reraove es también aleatorio) no desequilibra el árbol de b aleatoriedad de un
ira manera observable. De hecho, para árboles de búsqueda pequeños, el árbol- los efectos no se
algoritmo de remove parece equilibrar el árbol. En consecuencia podemos comprenden dei todo desde
el punto de vista teórico,
razonablemente asumir que, para entradas aleatorias, todas las operaciones pero aparentemente son
se efectúan en un tiempo promedio logarítmico, aunque este resultado no lo desprodafctes en b predica
hemos demostrado matemáticamente. En el Ejercicio 19.17 describiremos
algunas estrategias de borrado alternativas.
El problema más importante no es el potencial desequilibrio provocado por el algoritmo remove,
más bien, el problema es que si la secuencia de entrada está ordenada, nos encontraremos con el
árbol de caso peor. Cuando esto suceda, tendremos una grave situación. Nos encontraremos con un
tiempo lineal por operación (para una serie de Noperaciones) en lugar de con un coste logarítmico
por operación. Este caso es análogo al caso de pasar elementos a un algoritmo de ordenación
rápida, pero haciendo que en su lugar se ejecute una ordenación por inserción. El tiempo resultante
de ejecución es completamente inaceptable. Además, no son solo las entradas ordenadas las que
resultan problemáticas, sino también cualquier entrada que contenga largas secuencias no aleatorias.
Una solución a este problema consiste en insistir en una condición estructural adicional denominada
equilibria, no se permite que ningún nodo tenga una profundidad excesiva.
Se pueden utilizar diversos algoritmos para implementar un árbol de búsqueda binaria
equilibrado, que tiene una propiedad estructural adicional que garantiza una profundidad logarítmica
696 Capitulo 19 Árboles de búsqueda binaria
UhártoítfeixisguKb en caso peor. La mayoría óe estos algoritmos son mucho más complicados
Ctoatáajtftwífo tiene que los correspondientes a los árboles de búsqueda binaria estándar, y todos
tna propiedad estructural
anadda que garantiza ura ellos requieren un tiempo medio mayor para la inserción y el borrado. Sin
proAnldad tegartintea embargo, proporcionan protección contra esos embarazosos casos simples
en caso peor. Las que conducen a un rendimiento muy pobre de los árboles de búsqueda
actualizaciones son mis
lentas, pero ios accesos son binaria desequilibrados. Asimismo, puesto que son equilibrados, tienden a
ma$ rápidos. proporcionar un tiempo de acceso más rápido que el correspondiente a los
árboles estándar. Normalmente, sus longitudes intemas de camino están muy
próximas al valor óptimo JVlog N, en lugar de a 1,38/Vlog N, por lo que el
tiempo de búsqueda es aproximadamente un 25 por ciento más rápido.
19.4.1 Propiedades
Todo nodo de un árbol La Figura 19.21 muestra dos árboles de búsqueda binaría. El árbol ilustrado
AVUenosubart»tes en la Figura 19.21 (a) satisface la condición de equilibrio AVL y es. por tanto,
cuyas aburas dHeren como
ni árbol AVL. El árbol de la Figura 19.21 (b), que es el resultado de insertar
marino en 1. Unsutórbol
vado tiene una Ara -1. 1. utilizando el algoritmo habitual, no es un árbol AVL. porque los nodos
marcados con sombreado oscuro tienen subárboles izquierdos cuyas alturas
son 2 unidades mayores que las de sus subárboles derechos. Si se insertara el
valor 13 empleando el algoritmo de inserción habitual de los árboles de búsqueda binaria, el nodo
16 también violaría la regla de equilibrio. La razón es que el subárbol izquierdo tendría altura 1.
mientras que el subárbol derecho tendría altura -1.
19.4 Arbotes AVL 697
Rgura 19421 Dos árboles de búsqueda binaria- (a) un árbol AVL: (b) un árbol no AVL {los nodos no equilibrados se muestran más osaros).
la condición de equilibrio AVL implica que el árbol tiene solo una Un árbol AVI Üene una
profundidad logarítmica. Para demostrar esta afirmación, necesitamos atura que es como máximo
un 44 por cierto mayor que
demostrar que un árbol de altura Adebe tener al menos C" nodos para alguna
h atura mfrtma.
constante C > 1. En otras palabras, el número mínimo de nodos en un árbol
es exponencial con respecto a su altura Entonces, la profundidad máxima de
un árbol de Adementas estará dada por logcA El Teorema 19.3 muestra que todo árbol AVL de
altura Atiene un cierto número mínimo de nodos.
-Ay í Un árbol AVL de altura Atiene ai menos F/hz - 1 nodos, donde Ft es el /«simo
número de Rbonacd (véase ia Sección 7.3.4).
Demostración Sea St/el tamaño del árbol AVL más pequeño de altura //.Claramente, = lyS^ 2.
La Rgura 19.22 muestra que el árbol más pequeño de altura H debe tener subárboles
de alturas H - 1 y H - 2. La razón es que al menos uno de los subárboles tiene
altura H - 1 y la condición de equilibrio implica que tas alturas de los subárboles
pueden diferir como máximo en 1. Estos subárbotes deben, ellos mismos, tener el
menor número posible de nodos para sus correspondientes alturas, por lo que Sf{ =
S//} + S//2 + l.La demostración puede completarse utilizando un argumento de
inducción.
A partir del Ejercicio 7.8, z?« / >5. donde <ó=(l+>?5)/2« 1,618. En consecuencia, un árbol
AVL de altura H tiene al menos (aproximadamente) <5 nodos. Por tanto, su profundidad es
oomo máximo logarítmica La altura de un árbol AVL satisface la relación
698 Capitulo 19 Árboles de búsqueda binaria
tf<l,44log(A/+2)-L328 (19.1)
La profinSdad de un nodo por lo que la altura de caso peor es, como máximo, aproximadamente un 44
tlpk» de un árbol AVL es por ciento superior al mínimo posible para los árboles binarios.
muy prúadmo ai vator óptimo
bg«
la profundidad media de un nodo en un árbol AVL construido aleato
riamente tiende a ser muy próxima a log N. La respuesta exacta no se ha
establecido todavía de manera analítica Ni siquiera sabemos si la forma es
log N+ Co (1 4-e) log + C, para algún eque sería aproximadamente 0,01.
Unaacíu&lzactonenun
abol AVI podría destruir el Las simulaciones han sido incapaces de demostrar de manera convincente si
equl brio. H abrá entonces una de las dos formas es más plausible que la otra.
q» re-equUbrar elárbol
Una consecuencia de estos argumentos es que todas las operaciones
antes do poder considerar b
cpcraciún completada de búsqueda de un árbol AVL tienen cotas logarítmicas de caso peor. 1^
dificultad estriba en que las operaciones que modifican el árbol, como insert
y reraove. ya no son tan simples como antes. La razón es que una inserción
(o un honrado) puede destruir el equilibrio de varios nodos del árbol, como
Soto podra verse afectado ei
equtbrto de aquetas nodos se muestra en la Figura 19.21. El equilibrio deberá ser entonces restaurado
queesttnstuadosenet antes de poder considerar completada la operacióa Describiremos aquí el
camino que va de bratz
hasta el punto de hserdún
algoritmo de inserción y dejaremos el algoritmo de borrado como Ejercicio
________________ 19.10 para el lector.
Una observación crucial es que, después de una inserción, solo los nodos
que se encuentran en el camino que va desde el punto de inserción a la raíz podrán haber visto
modificado su equilibrio, ya que solo se modifican los subárboles de esos nodos. Este resultado
se aplica a todos los algoritmos de árboles de búsqueda equilibrados. A medida que seguimos el
camino hacia arriba hasta la raíz y actualizamos la información de equilibrado, podemos topamos
oon un nodo cuyo nuevo equilibrio viola la condición AVL. En esta sección, mostraremos cómo re
equilibrar el árbol en el primero (es decir, en el más profundo) de esos nodos y demostraremos que
este re-equilibrado garantiza que todo el árbol satisfaga la propiedad AVL.
El nodo que hay que re-equilibrar es X Puesto que cualquier nodo tiene
9 corregimos ei equUbrio oomo máximo dos hijos y un desequilibrio de alturas requiere que las alturas
en ei nodo desoquíbrato
de los dos subárboles de X difieran en 2, pueden producirse cuatro casos
más profundo, volvemos
are-egJfcrar dárbd distintos de violación del equilibrio:
completo. Hay cuato
casos Que tendríamos 1. Una Inserción en el subárbol izquierdo del hijo izquierdo de X
polendáimente que corregí
dos toctos son simétricos
2. Una inserción en el subárbol derecho del hijo izquierdo de X
con respecto a ios otos
dos
3. Una inserción en el subárbol izquierdo del hijo derecho de X
4. Una inserción en el subárbol derecho del hijo derecho de X
Ijos casos 1 y 4 son casos simétricos con respecto a X. al igual que lo son
Dequítorio se restaura los casos 2 y 3. Eh consecuencia, solo existen dos casos básicos desde el
medente rotaciones del
árbol. Una mtxián simple punto de vista teórico. Desde el punto de vista de la programación seguirán
Hercambia tos papeles dei existiendo, por supuesto, cuatro casos y numerosos casos especiales.
padre y dei Njo.ái mismo
El primer caso, en el que la inserción tiene lugár en el exterior (es decir,
tempo que mantened
oden de búsqueda izquierda-izquierda o derecha-derecha), se puede corregir mediante una única
rotación del árbol. Una rotación simple intercambia los papeles del padre y el
19.4 Arbotes AVL 699
hijo, al mismo tiempo que mantiene el orden de búsqueda El segundo caso, en el que la inserción
tiene lugar en el interior (es decir, izquierda-derecha o derecha-izquierda) se puede tratar mediante
una rotación doble, que es ligeramente más compleja. Estas operaciones fundamentales con el
árbol se utilizan en diversas ocasiones en los algoritmos para árboles equilibrados. En el resto de
esta sección vamos a describir estas rotaciones y a demostrar que son suficientes para mantener la
condición de equilibrio.
iharotacün basta para Este trabajo solo requiere cambiar ios pocos enlaces a hijos mostrados
SñáfWMfLS°Sly4€n m P^docódigo de la Figura 19.24. y nos da como resultado otro árbol
binario que es un árbol AVL. Este resultado se produce porque A se mueve
hacia arriba un nivel, Z? permanece en el mismo nivel y Cse mueve hacia
abajo un nivel. Por tanto. Á, y k2 no solo satisfacen los requisitos AVL. sino
que también tienen subárboles que son de la misma altura. Además, la nueva altura de todo el
subárbol es exactamente la misma que la altura del subárbol original antes de la inserción que hizo
que A creciera. Por tanto, no hace falta ninguna actualización adicional de las alturas en el camino
que va hasta la raíz y. en consecuencia, no hacen faltan rotaciones adicionales. En este capítulo
emplearemos esta rotación simple en otros algoritmos para árboles equilibrados.
ta Figura 19.25(a) muestra que después de la inserción de 1 en un árbol AVL. el nodo 8
pasa a estar desequilibrado. Se trata, claramente, de un problema de caso 1. porque el valor 1 se
encuentra en el subárbol izquierdo-izquierdo de 8. Por tanto, hacemos una rotación simple de 8 y 4,
obteniendo así el árbol mostrado en la Figura 19.25(b). Como hemos dicho anteriormente en esta
sección, el caso 4 representa un caso simétrico. La rotación requerida se muestra en la Figura 19.26
y el pesudocódigo que la implementa se proporciona en la Figura 19.27. Esta rutina, junto con otras
rotaciones presentadas en esta sección, está replicada en varios árboles de búsqueda más adelante en
el texto. Estas rutinas de rotación aparecen en el código en línea para diversas implementaciones de
árboles de búsqueda equilibrados.
i /*♦
? * Rotar un nodo del árbol binario con un hijo izquierdo.
3 ♦ Para árboles AVL. esta es una rotación simple para el caso 1.
*/
5 static BinaryNode rotateWithLeftChildí BinaryNode k2 )
6 i
7 BlnaryNode kl - k2.1eft:
8 k2.1eft ® kl .right:
9 kl.right ° k2:
10 return kl:
i! I
Rgura 19.25 Una rotación simple permite corregir el árbol AVL después de insertar el valor 1.
19.4 Arbotes AVL 701
1 /**
2 * Rotar un nodo del árbol binario con un hijo derecho.
3 * Para árboles AVL, esta es una rotación simple para el caso 4.
*/
5 static BinaryNode rotateWithRightChildí BinaryNode kl }
6 {
7 BinaryNode k2 = kl.right:
8 kl.right » k2.left:
9 k2.left » kl:
10 return k2:
11 I
Ibra re-equilibrar, no podemos dejar 4 como raíz. En la Figura 19.28 hemos mostrado que una
rotación entre y no funciona, por lo que la única alternativa es colocar k2 como raíz. Hacer esto
obliga a Á, a ser el hijo de izquierdo de 4 y a a ser el hijo derecho de 4- También determina las
ubicaciones resultantes de los cuatro subárboles, y el árbol resultante satisface la propiedad AVL.
Asimismo, como sucedía con la rotación simple, restaura la altura al valor que tenía antes de la
inserción, garantizando así que todo el re-equilibrado y actualización de alturas está completo.
Fbr ejemplo, la Figura 19.30(a) muestra el resultado de insertar 5 en un árbol AVL. Se provoca
m desequilibrio de altura en el nodo 8, lo que nos da como resultado un problema de caso 2.
Realizamos una rotación doble en ese nodo, obteniendo así el árbol mostrado
uraroucpndbtffies en la Figura 19.30(b).
eqjjArteabosrotacfanes pjgyya j9 3 j muestra que el caso 3 simétrico también se puede corregir
mediante una rotación doble. Finalmente, observe que. aunque una rotación
doble parece compleja, es equivalente a la siguiente secuencia:
■ Una rotación entre el hijo y el nieto de X.
■ Una rotación entre Xy su nuevo hijo.
19.4 Arbotes AVL 703
Rgura 19.30 Una rotación dotíe corrige el árbol AVL después de la Inserción de 5.
i /**
? * Rotación doble de un nodo de un árbol binario: primero hijo izquierdo
3 * con su hijo derecho: después el nodo k3 son su nuevo hijo izquierdo.
4 * Para árboles AVL. esta es una rotación doble para el caso 2.
5 */
6 static BinaryNode doubleRotateWithLeftChildí BinaryNode k3 )
7 {
8 k3.1eft - rotateWithRightChildí k3.1eft ):
9 return rotateWithLeftChildí k3 ):
10 |
1 /**
2 * Rotación doble de un nodo de un árbol binario: primero hijo derecho
3 * con su hijo Izquierdo: después el nodo kl son su nuevo hijo derecho.
4 * Para árboles AVL. esta es una rotación doble para el caso 3.
5 ♦/
6 static BinaryNode doubleRotateWithRightChildC BinaryNode kl )
7 {
8 kl.right ® rotateWithLeftChildí kl.right ):
9 return rotateWithRightChildí kl ):
10 l
Un árbol rojo-negro es un árbol de búsqueda binaria que tiene las Está prohfcfato que haya
siguientes propiedades de ordenación: nodos rojos consecuOm,
y todos los caminos donen
1. Todo nodo está coloreado de rojo o de negro. d mismo número de nodos
negros.
2. la raíz es negra.
3. Si un nodo es rojo, sus hijos deben ser negros.
4. Todo camino desde un nodo hasta un enlace nul 1 debe contener el mismo número de nodos
negros.
Eh este análisis de los árboles rojo-negro, los nodos sombreados repre Los note sombreados sen
sentan nodos rojos. La Figura 19.34 muestra un árbol rojo-negro. Todo «josa to largo <fe iodo este
artífcb.
camino desde la raíz hasta un nodo nul l contiene tres nodos negros.
Podemos demostrar por inducción que. si todo camino desde la raíz hasta
tn nodo nuil contiene B nodos negros, el árbol debe contener al menos
Esta garantizado que b
2fí - 1 nodos negros. Además, como la raíz es negra y no puede haber dos profundidad <te un ártxjl
nodos consecutivos rojos en un camino, la altura de un árbol rojo-negro es rojo-negro sea logarítmica.
Típicamente, b profinfdad
oomo máximo 2 log (N + 1). En consecuencia, se garantiza que la operación es b misma que pera un
de búsqueda sea una operación logarítmica. atol AVL
La dificultad, como de costumbre, es que las operaciones pueden
modificar el árbol y posiblemente destruir sus propiedades de coloreado.
Esta posibilidad hace que la inserción sea difícil y el borrado todavía más. En primer lugar, vamos a
implementar la inserción y luego examinaremos el algoritmo de borrado.
Rgura 19.34 Artiol rojo-negro: la secuencia (fe Inserciones 10.85.15.70.20.60.30.50.65.80,90.40.5 y 55 (tos noto sombreados son rojos).
706 Capitulo 19 Árboles de búsqueda binaria
los nuevos eksnertos se Tenemos que considerar varios casos (cada uno con sus correspondientes
deten cobrear de roj?. simetrías) sí el padre es rojo. En primer lugar, suponga que el hermano del
SI el padre fuera rojo
deberemos wher a cobrear padre es negro (adoptamos el convenio de que los nodos nul 1 son negros), lo
/oroíarparaeEWnartó que se aplicaría a la inserción de los valores 3 u 8, pero no a la inserción de
emienda do nodos rojos
99. Sea Xte nueva hoja añadida, sea Psu padre, sea Sel hermano del padre
coree cutiros
(si existe) y sea £el abuelo. Solo Xy Pson rojos en este caso; Ges negro
porque en caso contrario habría habido dos nodos rojos consecutivos antes
Si el hermano del padre es de la inserción -una violación de la propiedad 3. Adoptando la terminología
negro, una rotación simple de los árboles AVL. diremos que A'puede ser, en relación con G, un nodo
o dode pernio corregir las exterior o interior? Si A' es un nieto exterior, una rotación simple de su
cosas, como en in árbol
AVL padre y de su abuelo y una serie de cambios de color permitirán restaurar la
propiedad 3. Si Xes un nieto interior, será necesaria una doble rotación junto
oon algunos cambios de color. La rotación simple se muestra en la Figura
19.35 y la rotación doble se ilustra en la Figura 19.36. Aun cuando A'es una hoja, hemos dibujado un
caso más general que permite que Xse encuentre en mitad del árbol. Utilizaremos esta rotación más
gmeral posteriormente en el algoritmo.
Antes de continuar, veamos por qué estas rotaciones son correctas. Necesitamos aseguramos de
que nunca haya dos nodos rojos consecutivos. Por ejemplo, como se muestra en la Figura 19.36. los
Rgura 19.35 S 5es negro, una rotación simple entre el padre y el abuelo, ¡tnlo con los cambios apropiados de color permitirá restairar ta
propiedad 3. en caso de que A'sea un rteto exterior.
Rgura 19.36 9 Ses negro, una rotación doble que a tecle a X al padre y al abuelo, junto contos apropiados cambios de color permitirá restaurar
b propiedad 3. en caso de que A'sea un rteto Interior.
tilicos casos posibles de nodos rojos consecutivos serían entre Py uno de sus hijos o entre Gy C
Pero las raíces de A, By Cdeben ser negras; en caso contrario, habría habido violaciones adicionales
de la propiedad 3 en el árbol original. En el árbol original, hay un nodo negro en el camino que va
desde la raíz del subárbol hasta A, By C, y dos nodos negros en los caminos que van hasta Dy E
Podemos verificar que este patrón se sigue manteniendo después de la rotación y el recoloreado.
Hasta ahora, todo bien. ¿Pero qué sucede si Ses rojo cuando intentamos
Sel hermano del padre
insertar el valor 79 en el árbol de la Figura 19.34? Entonces, ni la rotación
es rojo entonces, después
simple ni la doble funcionan, porque ambas nos dan como resultado do corregí tes cosas.
nodos rojos consecutivos. De hecho, en este caso, debe haber tres nodos Mudmos nodos rojos
cortsocdfrosaunntel
en el camino que va hasta Dy E y solo uno de ellos puede ser negro. Por superior. Heces! tamos lerar
consiguiente, tanto 5como la nueva raíz del subárbol deberán colorearse de hada ante enei árbol pva
rojo. Por ejemplo, el caso de rotación simple que se produce cuando Zes un oanegí te situation.
Rgura 19.37 S Ses rejo, una rotation simple entre el padre y el abuelo. Junto con los cambios apropiados de color permitirá restaurar la
propiedad 3 entre XyP.
708 Capitulo 19 Árboles de búsqueda binaria
modificado. Sin embargo, si el padre de Xes rojo, introduciríamos dos nodos rojos consecutivos.
Pero en este caso, podemos aplicar la rotación simple de la Figura 19.35 o la rotación doble de la
Figura 19.36. ¿Pero qué sucede sí el hermano del padre de A'es también rojo? Esta situación no
puede suceder. Sí en el camino de bajada por el árbol vemos un nodo /que tiene dos hijos rojos,
sabemos que el nieto de Y tiene que ser negro. Y como los hijos de /también se transforman en
negros debido al cambio de color (incluso después de la rotación que pueda tener lugar) no nos
encontraremos con otro nodo rojo durante dos niveles. Por tanto, cuando nos encontramos con X, si
el padre de Xes rojo, el hermano del padre de A'no puede ser también rojo.
Por ejemplo, suponga que queremos insertar el valor 45 en el árbol mostrado en la Figura 19.34.
Eh el camino de bajada por el árbol, nos encontramos con el nodo 50, que tiene dos hijos rojos.
Por tanto, realizamos un cambio de color, haciendo que 50 pase a ser rojo y que 40 y 55 pasen a
ser negros. El resultado se muestra en la Figura 19.39. Sin embargo, ahora tanto 50 como 60 son
rojos. Realizamos una rotación simple (poique 50 es un nodo exterior) entre 60 y 70, haciendo así
que 60 sea la raíz negra del subárbol derecho de 30 y haciendo que 70 sea rojo, como se muestra en
la Figura 19.40. Después continuamos realizando una acción idéntica si vemos en el camino otros
nodos que tengan dos hijos rojos. En este caso sucede que no hay ninguno más.
Al llegar a la hoja, insertamos 45 como nodo rojo, y como el padre es negro, habremos terminado.
El árbol resultante se muestra en la Figura 19.41. Si el padre hubiera sido rojo, habríamos tenido que
efectuar una rotación.
Como muestra la Figura 19.41, el árbol rojo-negro resultante suele estar bien equilibrado. Los
experimentos sugieren que el número de nodos recorridos durante una búsqueda típica en un árbol
rojo-negro es casi idéntico al promedio correspondiente a los árboles AVL, aunque las propiedades
de equilibrado del árbol rojo-negro son ligeramente más débiles. La ventaja de un árbol rojo-negro
es el gasto adicional relativamente pequeño que se requiere para realizar una inserción y el hecho de
que, en la práctica, las rotaciones se producen de forma relativamente infrecuente.
Rgura 19.38 Cambo de color: sdo si ei padre de Xes rojo cortínuamas con una rotación
Rgura 19.39 Un cambo de color en 50 provocaría ira violación: puesto que la violación es exterior, una rotation simple permite correaría.
19.5 Árboles rojo-negro 709
Rgura 19.40 Resdtado de una rotación simj/e que corrige ta vWadún en ei nodo SO
Uneas 62 a 65 permite que sean compartidas por Insert y la rutina handleReorient. El método
remove no está implementado.
Las rutinas restantes son similares a las rutinas correspondientes de BlnarySearchTree. salvo
porque tienen diferentes implementaciones, debido a los nodos centinela. Podríamos proporcionar
al constructor el valor para inícializar el nodo de cabecera, pero no hacemos eso. 1.a alternativa
es utilizar el método compare, definido en las líneas 38 y 39. cuando sea apropiado. En la Figura
19.44 se muestra un constructor. El constructor asigna nul INode y luego la cabecera y configura los
enlaces left y rl ght de la cabecera con el valor null Node.
La Figura 19.45 muestra el cambio más simple que resulta del uso de los
Las comparaciones con nodos centinela. La comprobación con respecto al valor nuil necesita ser
nuil sesusttuyen
por comparaciones con
sustituida poruña comprobación con respecto a nul INode.
nullNode. Para la rutina f 1 nd mostrada en la Figura 19.46, utilizamos un truco común.
Antes de iniciar la búsqueda, colocamos x en el centinela nullNode. De esa
forma se garantiza que terminaremos por encontrar una correspondencia
AI realar inaoporadcn con x, incluso si x no está en el árbol. Si la correspondencia se produce en
find, copiamos xen el
centheb null Node para
nullNode, podemos decir que el elemento no se ha encontrado. Empleamos
ettar comprobaciones este truco en el procedimiento Insert.
afctonates. El método insert se deduce directamente de nuestra descripción y se
muestra en la Figura 19.47. El bucle while que abarca las líneas 11 a 20
19.5 Árboles rojo-negro 711
! package weiss.nonstandard:
2
3 // Clase RedBlackTree
4 //
5 // CONSTRUCCIÓN: sin parámetros
6 //
7 ¡i ************** ****OPERACIONES PÚBlICAS*********************
8 // Igual que BinarySearchTree: omitido por brevedad
Q fl ******************£^Q^£^********************************
I0 // remove e insert generan excepciones en caso necesario.
i!
i? public class RedBlackTree<AnyType extends Comparable<? super AnyType>>
i3 I
14 public RedBlackTreei )
15 I /♦ Figura 19.44 */ I
16
17 public void Inserti AnyType item )
18 I /* Figura 19.47 */ I
19 public void removeí AnyType x }
20 1 /* No implementado */ I
21
2? public AnyType findMiní )
23 1 /* Véase el código en línea */ 1
24 public AnyType findMaxí )
25 I /* Similar a findMin */ 1
26 public AnyType findí AnyType x )
27 { /* Figura 19.46 ♦/ 1
28
29 public void makeEmptyi )
30 l header.right - nullNode: 1
31 public boolean isEmptyí )
32 I return header.right == nullNode: 1
33 public void printTreeí )
34 ( printTreeí header.right ): I
35
36 private void printTreeí RedBlackNode<AnyType> t )
37 I /* Figura 19.45 */ 1
38 private final int compareí AnyType item, RedBlackNode<AnyType> t )
39 I /* Figura 19.47 */ 1
40 private void handleReorienti AnyType item )
41 | /* Figura 19.48 */ I Continúa
42 private Red81ackNode<AnyType>
43 rotateí AnyType Item. RedBlackNode<AnyType> parent )
44 { /* Figura 19.49 */ I
45
46 private static <AnyType>
47 RedB1ackNode<AnyType> rotateWithLeftChildí RedBlackNode<AnyType> k2 )
43 I /* Implementación de la forma usual: ver código en línea */ 1
49 private static <AnyType>
50 RedBlackNode<AnyType> rotateWithRightChildí RedBlackNode<AnyType> kl )
51 I /* Implementación de la forma usual: ver código en línea */ I
52 private static class RedBlackNode<AnyType>
53 l /* Figura 19.42 */ I
54
55 private RedBlackNode<AnyType> header;
56 private RedBlackNode<AnyType> nullNode:
57
58 prívate static final int BLACK - 1: // BLACK debe ser 1
59 private static final int RED - 0;
60
6! // Usados en la rutina insert y sus rutinas auxiliares
6? private Red81ackNode<AnyType> current:
63 private RedBlackNode<AnyType> parent:
64 private Red81ackNode<AnyType> grand:
65 private RedB1ackNode<AnyType> great:
66
1 /**
2 * Construir el árbol.
3 */
4 public RedBlackTreei )
5 I
6 nullNode = new RedBlackNode<AnyType>( null ):
7 null Node.left = nul1 Node.right = nullNode:
8 header ° new RedBlackNodeKAnyTypeX null ):
9 header.left - header.right - nullNode:
10 I
va recorriendo el árbol hacia abajo y corrige los nodos que tengan dos hijos rojos, invocando
handleReorient, como se muestra en la Figura 19.48. Para hacer esto, no solo mantiene el nodo
19.5 Árboles rojo-negro 713
1 /**
2 * Método interno para imprimir un subárbol en orden.
3 * ©param t el nodo que actúa como raiz del árbol.
4 */
5 private void prlntTreeí RedBlackNode<AnyType> t )
6 {
7 ifí t !” nullNode )
8 1
9 printTreeí t.left ):
10 System.out.println( t.element ):
1! printTree( t.right ):
12 I
13 1
1 /**
2 * Encontrar un elemento en el árbol.
3 * ©param x el elemento que hay que buscar.
4 * ©return el elemento correspondiente o nuil s1 no se encuentra.
5 */
6 public AnyType f1nd( AnyType x )
7 {
8 nul1 Node.element - x:
9 current - header.right:
10
1! for( : : )
12 I
13 if( x.compareToí current.element ) < 0 )
14 current - current.left:
15 else if( x.compareToí current.element ) > 0 )
16 current - current.right:
17 else ifi current i® nullNode )
18 return current.element:
19 else
20 return null;
21 1
2? I
Rgura 19.45 Laruflna f 1nd de RedBleckTree. Observe el uso de header y nul INode.
actual, sino también el padre, el abuelo y el bisabuelo. Observe que, después de una rotación, los
valores almacenados en el abuelo y el bisabuelo ya no son conectas. Sin embargo, serán restaurados
Capitulo 19 Árboles de búsqueda binaria
1 /**
í. * Insertar en el árbol.
3 * ©param item el elemento que hay que insertar.
M4 * ©throws DuplicateltemException si el elemento ya está presente.
5 */
6 public void insert! AnyType item )
7 i
8 current ® parent « grand ® header;
9 nul I Node .element « item;
10
il whilei compare! item, current ) I- 0 )
12 {
13 great = grand; grand = parent; parent = current;
14 current = compare! item, current ) < 0 ?
15 current.left : current.right;
16
17 // Comprobar si hay dos hijos rojos, corregir en caso afirmativo
18 if! current.left.color = RED && current.rlght.color ~ RED )
19 handleReorient! item );
20
21
22 // La inserción falla si ya está presente
23 if! current !- nullNode )
24 throw new DuplicateltemExceptloni item.toStrlng! ) );
25 current = new RedBlackNode<AnyType>( item. nullNode. nullNode );
26
27 // Asociar con el padre
28 if! compare! item, parent ) < 0 >
29 parent.left - current;
30 else
3! parent.right “ current;
32 handleReorient! item );
33
34
35 /**
36 * Comparar item y t.element, utilizando compareTo. teniendo en cuenta
37 * que si t es el nodo cabecera, entonces item es siempre mayor.
38 * Esta rutina se invoca si es posible que t sea la cabecera.
39 * Si no es posible que t sea la cabecera, usar compareTo directamente.
40 */
41 private final int compare! AnyType item. RedBlackNode<AnyType> t )
42 {
43 if! t = header )
44 return 1;
45 el se
46 return i tern.compareTo! t.element );
47
Rgura 19.47 Las rutinas i nsert y compare para la clase RedBI ac kTree.
19.5 Árboles rojo-negro 715
! /**
2 * Rutina Interna que se Invoca durante una Inserción si un nodo
3 * tiene dos hijos rojos. Realiza cambios de color y rotaciones.
4 * ©param Item el elemento que se está Insertando.
5 ♦/
6 private void handleReorientl AnyType Item )
7 {
3 // Hacer el cambio de color
9 current.color - REO:
10 current.left.color - BLACK;
!! current.rlght.color - BLACK:
12
13 1f( parent.color == RED ) // Hay que rotar
14 (
13 grand.color = RED:
16 if( ( compareí Item, grand ) < O ) != 1 compareí item, parent ) < O ) )
17 parent - rotateí Item, grand ); // Iniciar rotación doble
13 current = rotateí Item, great ):
19
20 current.color ® BLACK:
21 1
22 header.right.color - BLACK; // Hacer que la raíz sea negra
23 I
Rgura 19.48 La ruina handleReori ent. que selrwoca si w nodo Bene dos Njos rojaso cuando se Inserta un nuevo nodo.
antes del momento en que se los vuelva a necesitar. Cuando el bucle termina, o bien se ha
encontrado x (como indica current 1-nullNode) o bien no se ha encontrado x (como indica
current—nul INode). Si no se ha encontrado x. generamos una excepción en la línea 24. En caso
contrario, x no se encuentra en el árbol y habrá que hacer que sea un hijo
de parent. Asignamos un nuevo nodo (como nuevo nodo current), lo QcódgoesrelaWamerto
compacto para la serie de
asociamos al padre y llamamos a handl eReorlent en las líneas 25 a 32.
casos que edsten, y mis s i
Eh las líneas 11 y 14 vemos la llamada a compare, que se utiliza porque la tenemos en cuente el hecho
cabecera podía ser uno de los nodos implicados en la comparación. El valor <fe cpeb Imptementación
no esrocurslva. Por estas
en la cabecera es -<» desde el punto de vista lógico, pero en la práctica es tazones, el ato» rojo-negro
nuil. La implementación de compare garantiza que el valor de la cabecera ofrece w buen rendWerto
se considere siempre menor que cualquier otro valor, compare también se
muestra en la Figura 19.47.
El código utilizado para llevara cabo una rotación simple se muestra en el
D método rotate tiene
método rotate de la Figura 19.49. Puesto que el árbol resultante debe estar cuatro pos&Iáades. El
conectado a un padre, rotate toma el nodo padre como parámetro. En lugar operador?; hace el código
de llevar la cuenta del tipo de rotación (izquierda o derecha) a medida que mas compacto, pero es
lógicamente equivalente
descendemos por el árbol, lo que hacemos es pasar itera como parámetro. a una comprobación íf /
Esperamos que haya muy pocas rotaciones durante la inserción, por lo que else.
hacer las casas de esta forma no solo es más simple sino también más rápido.
716 Capítulo 19 Árboles de búsqueda binaria
! /**
2 * Rutina interna que realiza una rotación simple o doble.
3 * Puesto que el resultado se conecta al padre, hay 4 casos.
4 * Invocado por handleReorient.
5 * ©param item el elemento en handleReorient.
6 * ©param parent el padre de la raíz del subárbol rotado.
7 * ©return la raíz del subárbol rotado.
8 */
9 private Red81ackNode<AnyType>
10 rotate! AnyType item. RedBlackNcde<AnyType> parent )
1! (
i? 1f( compare! item, parent ) < 0 )
13 return parent.left - compare! item, parent.left ) < 0 ?
14 rotateWithLeftChiId! parent.left ) : // LL
15 rotateWi thRightChi Id! parent, left > : // LR
16 else
17 return parent.right » compare! item, parent.right ) < 0 ?
18 rotateWithLeftChild! parent.right ) : // RL
19 rotateWithRightChildí parent.right ): // RR
20 I
La rutina handleReorient invoca a rotate de la manera necesaria para llevar a cabo una
rotación simple o doble. Puesto que una rotación doble no es otra cosa que dos rotaciones simples,
podemos comprobar si estamos en un caso interior y. en caso afirmativo, realizar una rotación
extra entre el nodo actual y su padre (pasándole el abuelo a rotate). En cualquiera de los casos
efectuamos una rotación entre el padre y el abuelo (pasando el bisabuelo a rotate). Esta acción
está codificada de forma sucinta en las líneas 16 y 17 de la Figura 19.48.
rojo (inductivamente, por el invariante que estamos tratando de mantener) y que Xy 7son negros
(porque no podemos tener dos nodos rojos consecutivos). Hay dos casos principales, junto con las
variantes simétricas usuales (que aquí omitiremos).
Eh primer lugar, suponga que A'tiene dos hijos negros. Hay tres subcasos, que dependen de los
hijos de T.
1. 7’tiene dos hijos negros: cambiamos los colores (Figura 19.50).
2. 7’tiene un hijo rojo exterior: realizamos una rotación simple (Figura 19.51).
3. 7’tiene un hijo rojo interior: realizamos una rotación doble (Figura 19.52).
El examen de las rotaciones muestra que si 7’tiene dos hijos rojos, una rotación simple o una
doble permitirá conseguir nuestro objetivo (por lo que tiene sentido realizar solo una rotación
simple). Observe que, si A'es una hoja, sus dos hijos son negros, por lo que siempre podemos aplicar
ino de estos tres mecanismos para hacer que A'sea rojo.
Eh segundo lugar, suponga que uno de los hijos de A'es rojo. Puesto que las rotaciones en el
primer caso principal siempre colorean Xde rojo, si A'tiene un hijo rojo, se introducirían nodos rojos
consecutivos. Por tanto, necesitamos una solución alternativa. En este caso, pasamos al siguiente
Rgura 19.51 A'tiene dos NJos negros y el NJo exterior de su hermano es rejo hacemos una rotadán simple.
Rgura 19.52 A'tiene dos hijos negros y el NJo Interior de su hermano es rojo: hacemos una rotadon doNe.
718 Capitulo 19 Árboles de búsqueda binaria
nivel, obteniendo unos nuevos X, Ty PS\ tenemos suerte, nos toparemos con un nodo rojo (tenemos
al menos un 50 por ciento de posibilidades de que esto suceda), haciendo así que el nodo actual sea
rojo. En caso contrario, tenemos la situación mostrada en la Figura 19.53: es decir, que el factual
es negro, el 7 actual es rojo y el Pactual es negro. Podemos entonces rotar Ty P, haciendo así que
el nuevo padre de A'sea rojo; Xy su nuevo abuelo serán negros. Ahora A'todavía no es rojo, pero
ya estamos de nuevo en el punto de partida (aunque a un nivel más de profundidad). Este resultado
es lo suficientemente bueno, porque demuestra que podemos descender iterativamente por el árbol.
Así. mientras que terminemos por alcanzar un nodo que tenga dos hijos negros o nos topemos con
tn nodo rojo, no habrá problema Este resultado está garantizado para el algoritmo de borrado,
porque los dos posibles estados finales son
■ A'es una hoja, lo cual siempre será tratado por el caso principal, ya que X tiene dos hijos
negros.
■ A'tiene solo un hijo, en cuyo caso se aplica el caso principal si el hijo es negro y, si el hijo es
rojo, podemos borrar X en caso necesario, y hacer que el hijo sea negro.
a borrado rxmoso ocasiones se utiliza la técnica del borrado perezoso, en el que los
caíste enmara» tos elementos se marcan como borrados pero sin llegar a borrarlos realmente. Sin
ctemertoscomotxxTados embargo, el bonado perezoso desperdicia espacio y complica otras rutinas
(véase el Ejercicio 19.18).
19.6 Árboles AA
Q iríxIM es el método Debido a las muchas posibles rotaciones, el árbol rojo-negro es bastante
preferido cuando toce complicado de codificar. En particular, la operación reraove plantea un
tóto un árbol equribrado,
desafío considerable. En esta sección vamos a describir un árbol de búsqueda
cuando ira Imptomcrtaciún
desoxidad? es aceptable equilibrado simple, pero bastante útil, conocido con el nombre de árbo/AA.
y cuando hacen falta El árbol AA es el método preferido cuando hace falta un árbol equilibrado,
borrados
cuando resulta aceptable una implementación descuidada y cuando son
necesarios borrados. El árbol AA añade una condición adicional al árbol rojo-
negro: los hijos izquierdos no pueden ser rojos.
Esta sencilla restricción simplifica enormemente los algoritmos de los árboles rojo-negro por
dos razones: en primer lugar, elimina aproximadamente la mitad de los casos de reestructuración
del árbol; en segundo lugar, simplifica el algoritmo reraove al eliminar un caso bastante molesto;
es decir, sí un nodo intemo tiene solo un hijo, el hijo debe ser un hijo derecho rojo, porque los hijos
b c b c Qr) B
Rgura 19.53 A'es negro y al menos ir» de sus lijos es rojo: si pasamos al sigílente nivel y aterrizamos en un lijo rojo, no hay problema- si no es
asi, rolamos el hermano y el padre.
19.6 Árboles AA 719
izquierdos rojos son ahora ilegales, mientras que un único hijo negro violaría la propiedad 4 de los
árboles rojo-negro. Por tanto, siempre podemos sustituir un nodo intemo por el nodo más pequeño
de su subárbol derecho. Ese nodo más pequeño, o bien es una hoja, o tiene un hijo rojo y puede ser
fácilmente soslayado y eliminado.
Rara simplificar la implementación todavía más, representamos la infor- srt»e#<fet««dben
mación de equilibrio de una forma más directa. En lugar de almacenar un umtoiAArepresenta
color con cada nodo, almacenamos el nivel del nodo. El nivel de un nodo el número de ertsces
tzqJerdosenei csrrtnoque
representa el número de enlaces izquierdos en el camino que va hasta el nodo va testad nodo certteto
centinela null Node y es nullNode.
19.6.1 Inserción
La inserción de un nuevo elemento siempre se realiza en el nivel inferior.
Las hserdones se realzan
Como es habitual, eso puede crear problemas. En el árbol mostrado en uttzandoda^orbno
la Figura 19.54, la inserción de 2 crearía un enlace horizontal izquierdo, recurs No usual y dos
mientras que la inserción de 45 generaría enlaces derechos consecutivos. lanadas a método
Les ertsces batatales denominado skew. Corregimos los enlaces horizontales derechos consecu
tajuterdosseelmtnen tivos. efectuando una rotación entre el primero y el segundo (de las tres)
mecíanto ura operación
nodos unidos por los dos enlaces, un procedimiento denominado spl i t.
skew(cotadon enere un
nodoysuhJobqJerdo). El procedimiento skew se ilustra en la Figura 19.55 y el procedimiento
Los ertsces hatatales spl 11 en la Figura 19.56. Aunque una operación skew elimina un enlace
derechos caracotes
se corrigen mecíante ura
horizontal izquierdo, puede crear enlaces derechos horizontales consecutivos,
operación split (rotación porque el hijo derecho de X podría ser también horizontal. Por tanto,
entre un nodo y su NJo realizaríamos primero una operación skew y luego una spl i t. Después de una
derecho}. Una operación
skew precede a una
operación split, el nodo intermedio aumenta de nivel. Eso puede provocar
operación split. problemas para el padre original de X, al crear un enlace horizontal izquierdo
o enlaces derechos horizontales consecutivos: ambos problemas pueden
corregirse aplicando la estrategia skew/spl 11 en el camino de ascenso hacia
la raíz. Puede realizarse automáticamente si utilizamos recursión. y una implementación recursiva
de insert solo es dos llamadas a método más larga que la rutina correspondiente para un árbol de
búsqueda no equilibrado.
Rgura 19.56 0 procedimiento split es tita rotación simple entre Xy R observe que el nivel de £se incrementa
19.6 Árboles AA 721
Para ilustrar cómo funciona el algoritmo, insertamos 45 en el árbol AA Este es un raro algoritmo
mostrado en la Figura 19.54. En la Figura 19.57. cuando se añade 45 en en el soneto do que es más
Wide ¿talaren papel
el nivel inferior, se forman enlaces horizontales consecutivos. Entonces que de Implementar on
se aplican parejas skew/spl it según sea necesario, desde el nivel inferior computadora.
hacia la raiz. Asi. en el nodo 35 hace falta una operación spl i t debido a la
existencia de los enlaces horizontales derechos consecutivos. El resultado
de la operación spl it se muestra en la Figura 19.58. Cuando la recursión vuelve al nodo 50, nos
encontramos un enlace horizontal izquierdo. Por ello, realizamos una operación skew en 50 para
eliminar el enlace horizontal izquierdo (el resultado se muestra en la Figura 19.59) y luego una
operación split en 40 para eliminar los enlaces horizontales derechos consecutivos. El resultado
después de la operación spl i t se ilustra en la Figura 19.60. El resultado de la operación split es
Figura 19.57 Después de Id Inserción de 45 en el ártri de ejemplo: se Introducen enlaces horizontales consecutivos comenzando en 35.
j©------------------------
© © © © © © ©
Figura 19.58 Después de ta Operación sp i i t en 35: se Introduce m enlace horizontal IzqJerdo en 50.
Figura 19.59 Después de la operación skew en 50: se Introducen nodos horizontales consecutivos comenzando en 40
Rgura 19.60 Después de la operación spl11 en 40 ei nodo 50 esta en el mismo nivel que 70. Induciendo un enlace horizontal Izquierdo Ilegal.
722 Capitulo 19 Árboles de búsqueda binaria
que 50 estará en el nivel 3 y será un hijo horizontal izquierdo de 70. Por tanto, necesitaremos realizar
otra pareja de operaciones skew/split. La operación skew en 70 elimina el enlace horizontal
izquierdo del nivel superior, pero crea nodos horizontales derechos consecutivos, como se muestra
en la Figura 19.61. Después de aplicar la operación split final, los nodos horizontales consecutivos
se eliminan y 50 pasa a ser la nueva raíz del árbol. El resultado se muestra en la Figura 19.62.
19.6.2 Borrado
Q borrado se sknpüfca. ftira árboles de búsqueda binaria en general, el algoritmo remove se
descompone en tres casos: el elemento que hay que eliminar es una hoja,
porque el caso óe un úrico
hfc sob se puede presenta
tiene un único hijo o tiene dos hijos. Para los árboles AA. tratamos el caso
en el nWl y estamos
depuestos a utfzarb de un hijo de la misma forma que el caso de dos hijos, porque el caso de un
recursion. solo hijo solo se puede presentaren el nivel 1. Además, el caso de dos hijos
también es sencillo, porque el nodo utilizado como valor de sustitución está
^rantizado que se encuentre en el nivel 1, y en el caso peor solo tendrá un
enlace horizontal derecho. Por tanto, todo se reduce a ser capaz de aplicar la operación reraove para
eliminar un nodo de nivel 1. Claramente, esta acción podría afectar al equilibrio (considere, por
ejemplo, la eliminación del valor 20 en la Figura 19.62).
Sea 7el nodo actual y asumamos que vamos a emplear recursión. Si el borrado ha modificado
uto de los hijos de Ta un valor que sea dos menos que el nivel de T, también habrá que reducir el
nivel de 7’(solo el hijo en el que haya entrado la llamada recursiva podría verse afectado en realidad,
pero por simplicidad no vamos a preocupamos de cuál es). Además, si 7’tiene un enlace horizontal
derecho, el nivel de su hijo derecho también tendrá que ser reducido. En este punto, podríamos tener
Rflura 19.61 Después de la operadon skew en 70: se Introducen enlaces horizontales consecutivos comenzando en 30.
seis nodos en el mismo nivel: T, el hijo derecho horizontal £de T, los dos Después de un borrado
hijos de Ry los dos hijos derechos horizontales de esos hijos. La Figura 19.63 rocursM). ires operaciones
skew y dos operaciones
muestra el escenario más simple posible. split garantizan que
Después de eliminar el nodo l, el nodo 2 y por tanto el nodo 5 pasarán a se consiga re-eqdfcrard
ser nodos de nivel l. En primer lugar, deberemos corregir el enlace horizontal ¿bol.
19 6 3 Implementación Java
El esqueleto de la clase para árboles AA se muestra en la Figura 19.64 e La Imple mentad,ún es
incluye una clase anidada para los nodos. Buena parte de la clase mostrada relativamente staple
en la figura duplica el código anterior que hemos proporcionado para los comparada oon la del árbol
negro.
árboles. De nuevo, utilizamos un nodo centinela nullNode; sin embargo, no
necesitamos una pseudoraíz. El constructor asigna nullNode, como para los
árboles rojo-negro y hace que root lo referencia. El nullNode se encuentran
en el nivel 0. Las rutinas utilizan rutinas auxiliares privadas.
Eh la Figura 19.65 se muestra el método insert. Como hemos mencionado anteriormente en
esta sección, es casi idéntico al insert recursivo del árbol de búsqueda binaria La única diferencia
es que añade una llamada a skew seguida de una llamada a spl i t. En la Figura 19.66, skew y spl 11
se implementan fácilmente, utilizando las rotaciones de árbol ya existentes. Finalmente, reraove se
muestra en la Figura 19.67.
Como ayuda, mantenemos dos variables de instancia, deletedNode y
La variable del etedNode
lastNode. Cuando recorremos un hijo derecho, ajustamos deletedNode. hace referencia al nodo
ftiesto que invocamos reraove recursivamente hasta alcanzar la parte que contiene x(sl se
ha encontrado 4oa
inferior (no comprobamos la igualdad en el recorrido hacia abajo), tenemos nul 1 Node si xno se ha
¿prantlzado que, si el elemento que hay que eliminar se encuentra en el encontrado La variable
árbol, deletedNode hará referencia al nodo que lo contenga. Observe que lastNode hace referenda
atnododesustftudon
esta técnica se puede utilizar en el procedimiento find para sustituir las UKIzamos comparaciones
comparaciones de tres vías realizadas en cada nodo por comparaciones de dos de dos vías en ligar de
comparad ones de tres vías.
vías en cada nodo más una comprobación adicional de igualdad en la parte
inferior. lastNode apunta al nodo de nivel 1 en el que termina esta búsqueda.
Puesto que no nos detenemos hasta alcanzar la parte inferior, si el elemento se
Rgura 19.63 Cuando se borra 1. todos los nodos pasan a ser de nivel 1. Introduciendo asi enlaces horizontales IzqUerdos.
Capítulo 19 Arboles de búsqueda binaria
1 package weiss.nonstandard:
2
3 // Clase AATree
*♦4 //
5 // CONSTRUCCIÓN: sin ningún inicializador
6 //
7 II ******************gp[p^£[QNE5 PÚBLICAS*********************
8 // Igual que BlnarySearchTree: omitidos por brevedad
9 /¡ ******************cprqRES********************************
10 // insert y remove generan excepciones en caso necesario
11
12 public class AATree<AnyType extends Comparable<? super AnyType>>
13 I
14 public AATree( )
15 {
16 nullNode - new AANode<AnyType>( null. null, nuil ):
17 nullNode.left - nullNode.right - nullNode:
18 null Node.level - 0:
19 root ’ nullNode:
20 I
21
22 public void Insertí AnyType x )
23 I root = insertí x. root ): I
24
25 public void removeí AnyType x )
26 I deletedNode - nullNode: root - removeí x. root >: J
27 public AnyType findMiní )
28 I /* Implementación como es usual: véase código en linea */ I
29 public AnyType findMaxí )
30 I /* Implementación como es usual: véase código en linea */ I
31 public AnyType findí AnyType x )
32 I /* Implementación como es usual: véase código en linea */ 1
33 public void makeEmptyi )
34 1 root - nulINode: I
35 public boolean isEmptyí )
36 I return root = nullNode: 1
37
38 private AANode<AnyType> insertí AnyType x, AANode<AnyType> t )
39 ( /* Figura 19.65 */ 1
40 private AANode<AnyType> removeí AnyType x. AANode<AnyType> t )
41 I /* Figura 19.67 */ 1
Cónf/nüa
encuentra en el árbol. lastNode hará referencia al nodo de nivel l que contenga el valor sustituto y
debe ser eliminado del árbol.
Después de que termine una llamada recursiva dada, nos encontraremos en el nivel l o no. Si
estamos en el nivel l podemos copiar el valor del nodo en el nodo intemo que hay que sustituir;
a continuación, podemos soslayar el nodo de nivel 1. En caso contrario, estaremos en un nivel
superior y tendremos que determinar si se ha violado la condición de equilibrio. En caso afirmativo,
restauramos el equilibrio y luego hacemos tres llamadas a skew y dos a split. Como hemos
explicado anteriormente, estas acciones garantizan que se restauren las propiedades del árbol AA.
726 Capítulo 19 Árboles de búsqueda binaria
l /**
?. * Método interno para insertar en un subárbol.
3 * ©param x el elemento que hay que insertar.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * ©return la nueva raíz.
6 * ©throws DuplicateltemException si x ya está presente.
7 *7
8 prívate AANode<AnyType> insertí AnyType x. AANode<AnyType> t )
9 {
10 if ( t. =■ nul INode )
11 t = new AANode<AnyType>( x. nullNode, nullNode
12 else ifí x.compareToí t.element ) < 0 )
13 t.left = insertí x, t.left ):
14 else ifí x.compareToí t.element ) > 0 )
15 t.right = insertí x. t.right >:
16 el se
17 throw new DuplicateltemExceptioní x.toStringi
18
19 t = skewí t ):
20 t = splití t ):
2! return t:
22 }
! /**
2 * Primitiva skew para árboles AA.
3 * ©param t el nodo que actúa como raíz del árbol.
4 * @return la nueva raíz después de la rotación.
5 ♦/
6 private static <AnyType> AANode<AnyType> skew( AANode<AnyType> t )
7 {
8 ifí t.left.level == t.level )
9 t = rotateWi thLeftChl ldí t ):
10 return t:
i! I
i?
13 /**
14 * Primitiva split para árboles AA.
15 * @param t el nodo que actúa como raíz del árbol.
16 * ©return la nueva raíz después de la rotación.
17 */
18 private static <AnyType> AANode<AnyType> splití AANode<AnyType> t )
19 {
20 1f( t.right.right.level ~ t.level )
21 I
?? t - rotateWithRightChildí t ):
23 t.level++:
24 )
25 return t:
26 I
La iteración es la parte más complicada. Debemos decidir cómo queremos llevar a cabo el
recorrido. Tenemos varias alternativas a nuestra disposición:
1. Utilizar enlaces a los padres.
2. Hacer que el iterador mantenga una pila que represente los nodos que componen el camino
basta la posición actual.
3. Hacer que cada nodo mantenga un enlace a su sucesor según un recorrido en orden, una
técnica conocida con el nombre de árbol enhebrado.
Rara hacer que el código sea lo más parecido posible al código del árbol AA de la Sección 19.6,
utilizaremos la opción de hacer que el iterador mantenga una pila. Dejamos la solución basada en
fes enlaces a los nodos padre como Ejercicio 19.21 para el lector.
La Figura 19.68 muestra el esqueleto de la clase TreeSet. La declaración de nodo se muestra
en las líneas 12 y 13; el cuerpo de la declaración es idéntico al de AANode de la Sección 19.6. En la
línea 18 se especifica el miembro de datos que almacena el objeto función para comparación. Las
Capitulo 19 Árboles de búsqueda binaria
i 7**
2 * Método interno para borrar de un subárbol.
3 * ©paran, x el elemento que hay que borrar.
4 * @param t el nodo que actúa como raíz del árbol.
5 * @return la nueva raíz.
ó * ©throws ItemNotFoundException si no se encuentra x.
7 */
8 prívate AANode<AnyType> removeí AnyType x. AANode<AnyType> t )
9 {
10 ifí t !- nullNode )
1i I
1? // Paso 1: buscar hacia abajo en el árbol y
13 // configurar lastNode y deletedNode
14 lastNode - t:
15 ifí x.compareToí t.element ) < 0 )
16 t.left - removeí x. t.left ):
17 el se
18 I
19 deletedNode - t:
20 t.right - removeí x. t.right ):
?! I
22
23 // Paso 2: si estamos al final del árbol y
24 // x está presente, eliminarle
25 ifí t = lastNode )
26 I
27 ifí deletedNode = nullNode ||
28 x.compareToí deletedNode.element ) I- 0 )
29 throw new ItemNotFoundExceptioní x.toStringi ) ):
30 deletedNode.element = t.element:
3! t = t.right:
32 »
33
34 // Paso 3: en caso contrario, no estamos al final: re-equilibrar
35 el se
36 ifí t.left.level < t.level - 1 || t.right.level < t.level - 1 )
37 I
38 ifí t.right.level > --t.level )
39 t.right.level ’ t.level:
40 t - skewi t ):
41 t.right - skewi t.right ):
4? t.right.right - skewi t.right.right );
43 t - split( t ):
44 t.right - splití t.right ):
45 1
46 1
47 return t:
48
ratinas y campos de las líneas 54-55, 57-58, 62-63 y 70-77 son esencialmente idénticas a las
correspondientes del árbol AA. Por ejemplo, las diferencias entre el método insert de las líneas
54 y 55 y el de la clase AATree es que la versión de AATree genera una excepción si se inserta
un duplicado, mientras que esta rutina Insert vuelve inmediatamente; asimismo, esta versión de
insert mantiene los miembros de datos si ze y modCount y esta utiliza un comparador.
i package weiss.util:
?
3 import java.io.Serializable:
4 import java.io.IOException:
5
6 public class TreeSetCAnyType> extends Abstracted lectionCAnyType>
7 implements SortedSet<AnyType>
8 I
9 private class TreeSetlterator implements Iterator<AnyType>
10 I /* Figura 19.74 */ 1
11
12 private static class AANodeCAnyType> implements Serializable
13 I /* Igual que en la Figura 19.64 ♦/ I
14
15 private int modCount = 0:
16 private int theSize = 0:
17 private AANodeCAnyType> root - null:
18 private ComparatorC? super AnyType> cmp:
¡9 private AANodeCAnyType> nullNode:
20
21 public TreeSet( )
22 I /* Figura 19.69 */ I
23 public TreeSetí ComparatorC? super AnyType> c )
24 | /♦ Figura 19.69 */ 1
25 public TreeSetí SortedSetCAnyType> other )
26 l /* Figura 19.69 */ J
27 public TreeSetí CollectionC? extends AnyType> other )
28 I /* Figura 19.69 */ )
29
30 public ComparatorC? super AnyType> comparatorí )
31 I /* Figura 19.69 */ 1
32 private void copyFromí CollectionC? extends AnyType> other )
33 I /* Figura 19.69 */ 1
34
35 public int sizeí )
36 l return theSize: I
37
38 public AnyType firstí )
39 I /* Similar a findMin: véase el código en linea. */ I
40 publ1c AnyType 1 ast( )
4] I /* Similar a findMax: véase el código en linea. */ I
42
43 public AnyType getMatchí AnyType x )
44 l /* Figura 19.70 */ I
46
46 prívate AANode<AnyType> f1nd( AnyType x )
47 I /* Figura 19.69 */ I
48 prívate 1nt compareí AnyType Ihs. AnyType rhs )
49 l /♦ Figura 19.69 */ I
50 public boolean containsí Object x )
51 l /* Figura 19.69 */ I
5? public boolean addí AnyType x )
53 i /* Figura 19.71 ♦/ I
54 prívate AANode<AnyType> insertí AnyType x. AANode<AnyType> t )
56 [ /* Figura 19.71 ♦/ 1
56
57 prívate AANode<AnyType> deletedNode:
58 prívate AANode<AnyType> lastNode:
59
60 public boolean removeí Object x )
6! l 7* Figura 19.72 */ I
62 prívate AANode<AnyType> removeí AnyType x. AANode<AnyType> t )
63 ( /* Figura 19.73 */ I
64 publ ic void clearí )
65 ( /* Figura 19.72 */ I
66
67 public Iterator<AnyType> Iteratorí )
68 I return new TreeSetlteratori ); |
69
70 private static <AnyType> AANode<AnyType> skewi AANode<AnyType> t )
71 I /* Igual que en la Figura 19.66 */ I
72 private static <AnyType> AANode<AnyType> splití AANode<AnyType> t )
73 ( /♦ Igual que en la Figura 19.66 ♦/ I
74 private static <AnyType> AANode<Anyíype> rotateWithLeftChildí AAHode<AnyType> k2 )
75 I /* Igual que lo habitual*/ I
76 private static <AnyType> AANode<AnyType> rotateWithRightChildí AANode<AnyType> kl)
77 I /* Igual que lo habitual */ )
78
1 /**
9 * Construir un TreeSet vacío.
3 */
•}A public TreeSet( )
5 {
ó nullNode - new AANodeCAnyTypeX null. null, nuil ):
7 nullNode.left - nullNode.right - nullNode:
8 null Node.level - 0:
9 root » nulINode:
10 cmp = null:
11
12
13 /♦*
I4 * Construir un TreeSet vacío con un comparador especificado.
15 ♦/
16 public TreeSetí ComparatorC? super AnyType> c )
17 I this( ): cmp = c: I
18
19 /*♦
20 * Construir un TreeSet a partir de otro SortedSet.
21 */
22 public TreeSetí SortedSet<AnyType> other )
23 I thisí other.comparatorí ) ): copyFromí other ): I
24
25 /**
26 * Construir un TreeSet a partir de cualquier colección.
27 * Usa un algoritmo 0( N log N ). pero podría mejorarse.
28 */
29 public TreeSetí CollectionC? extends AnyType> other )
30 I thisí ): copyFromí other ): l
3!
32 /**
33 * Devuelve el comparador utilizado por este TreeSet.
34 * @return el comparador o nuil si se usa el comparador predeterminado.
35 ♦/
36 public ComparatorC? super AnyType> comparatorí )
37 I return cmp: )
38
39 /♦*
40 * Copia cualquier colección en un nuevo TreeSet.
41 */
42 private void copyFromí CollectionC? extends AnyType> other )
43 {
44 clearí );
45 forí AnyType x : other )
46 addí x ):
47
1 /**
2 * Este método no forma parte del estándar Java.
3 * Como contains, comprueba si x está en el conjunto.
4 * Si está, devuelve la referencia al objeto correspondiente:
5 * en caso contrario, devuelve nuil.
6 * @param x el objeto que hay que buscar.
7 * ©return si contains(x) es false, el valor de retorno es nuil;
8 * en caso contrario, el valor de retorno es el objeto que hace que
9 * contains(x) devuelva true.
10 */
i! public AnyType getMatchí AnyType x )
i2 t
13 AANode<AnyType> p - findí x ):
14 ifí p — null )
IS return nul l:
16 else
17 return p.element:
18 I
19
20
21 * Encontrar un elemento en el árbol.
2? * ©param x el elemento que hay que buscar.
23 * ©return el elemento correspondiente o nuil si no se encuentra.
24 */
25 private AANode<AnyType> findí AnyType x )
26
27 AANode<AnyType> current - root:
28 nullNode.element - x:
29
30 forí ; ; )
31 I
32 int result = compareí x. current.element ):
33
34 ifí result < 0 )
35 current - current.left:
36 else ifi result > 0 )
37 current - current.right:
38 else ifi current Í” nullNode )
39 return current:
40 el se
41 return nul l: Cortilnúa
42 )
43
44
45 prívate int compareí AnyType Ihs. AnyType rhs )
46 {
47 if( cmp =» nul1 >
43 return ((Comparable) Ihs).compareToí rhs ):
49 else
50 return cmp.compareí Ihs. rhs ):
51
1 /**
2 * Añade un elemento a esta colección.
3 * ©param x cualquier objeto.
4 * ©return true si este elemento se ha añadido a la colección.
5 */
6 public boolean addí AnyType x )
7 {
8 int oldSize - sizeí ):
9
10 root = insertí x. root ):
11 return sizeí ) i- oldSize:
1?
¡3
14 /**
15 * Método interno para insertar en un subárbol.
16 * ©param x el elemento que hay que insertar.
17 * ©param t el nodo que actúa como raíz del árbol.
18 * ©return la nueva raíz.
19 */
20 prívate AANode<AnyType> insertí AnyType x. AANode<AnyType> t )
21 {
22 ifí t = nulINode )
23 I
24 t - new AANode<AnyType>( x. nullNode. nullNode ):
25 modCount++:
26 theSize++:
27 )
23 else
29 1 Cbnf/nüa
1
2 * Elimina un elemento de esta colección.
3 * ©param x cualquier objeto.
,j * ©return true si este elemento se ha eliminado de la colección.
5 */
ó public boolean removeí Object x )
7 {
8 int oldSize - sizeí ):
9
10 deletedNode ~ nullNode:
1! root - removeí (AnyType) x. root ):
12
13 return sizeí ) I- oldSize:
14
15
16 /»*
17 * Cambiar el tamaño de esta colección a cero.
18 */
19 public void clearí )
20 {
21 theSize - 0:
22 modCount++:
23 root « nul INode:
24
! /**
? * Método interno para borrar de un subárbol.
3 * ©param x el elemento que hay que borrar.
4 * ©param t el nodo que actúa como raíz del árbol.
5 * @return la nueva raíz.
6 */
7 prívate AANode<AnyType> removeí AnyType x. AANode<AnyType> t )
8 í
9 ifí t != nullNode )
10 I
11 // Paso 1: buscar hacia abajo en el árbol y
12 // configurar lastNode y deletedNode
13 lastNode - t:
14 íf( compareí x. t.element ) < 0 )
IS t.left ’ removeí x. t.left ):
16 el se
17 I
18 deletedNode » t:
19 t.right - removeí x. t.right ):
20 I
21
2? // Paso 2: si estamos al final del árbol y
23 // x no está presente, eliminarlo
24 ifí t = lastNode )
25 I
26 ifí deletedNode = nullNode ||
27 compareí x. deletedNode.element ) i» 0 )
28 return t: // Elemento nc encontrado: no hacer nada
29 deletedNode.element - t.element:
30 t « t.right:
31 theSize - -:
32 modCount++:
33 I
34
35 // Paso 3: en caso contrario, no estamos al final: re-equi librar
36 el se
37 ifí t.left.level < t.level - 1 || t.right.level < t.level - 1 )
38 I
39 if( t.right.level > --t.level )
40 t.right.level = t.level:
4i t = skewi t ):
42 t.right » skewi t.right ):
43 t.right.right » skewi t.right.right ):
44 t = spliti t ):
45 t.right - spliti t.right ):
46 I
47 I
48 return t:
49
I /**
2 * Esta es la implementación de TreeSetlterator.
3 * Mantiene una noción de una posición actual y. por supuesto
>
* la referencia implícita al TreeSet.
5 */
6 private class TreeSetlterator implements Iterator<AnyType>
7 {
8 private int expectedModCount = modCount:
9 private int visited - 0:
SO private Stack<AANode<AnyType>> path - new Stack<AANode<AnyType>>( ):
11 private AANode<AnyType> current - null:
12 private AANode<AnyType> lastVisited => null:
13
14 public TreeSetlteratori )
15 I
16 ifí isEmptyí ) )
17 return:
18
19 AANode<AnyType> p » null;
20 fori p - root: p.left !- nullNode: p - p.left )
21 path.pushí p ):
22
23 current = p:
24 )
25
26 public boolean hasNextí )
27 I
28 ifi expectedModCount I- modCount )
29 throw new ConcurrentMcdificationExceptioni );
30
31 return visited < sizeí ):
32 )
33
34 public AnyType nextí >
35 I /* Figura 19.75 */ I
36
37 public void removeí )
38 I /* Figura 19.76 */ 1
39
l^as líneas 17 a 21 declaran los tres métodos abstractos. Se trata de factorías que crean
el objeto concreto apropiado y lo devuelven a través del tipo Interfaz. Por ejemplo, en
TreeMap, makeEmptyKeySet devuelve un TreeSet recién construido, mientras que en HashHap,
make Empty Key Set devuelve un HashSet recién construido. Lo más importante es que roakePair
crea un objeto de tipo Hap.Entry que representa la pareja clave/valor. Para un TreeSet. el objeto
Capítulo 19 Árboles de búsqueda binaria
1 package weiss.uti1:
•.
3 /**
4 * Maplmpl Implementa el Map sobre un conjunto.
5 * Debe ser ampliada por TreeMap y HashMap. con
6 * llamadas encadenadas al constructor.
7 */
8 abstract class Maplmpl<KeyType.ValueType> Implements MapCKeyType.ValueType>
9 I
10 private SetCMap. EntryCKeyType.Val ueType» theSet:
ij
12 protected Maplmpl( SetCMap.EntryCKeyType.ValueType» s )
13 I theSet = s: 1
14 protected Maplmpl( MapCKeyType.ValueType> m )
15 l theSet - clonePa1rSet( m.entrySetí ) ): 1
16
17 protected abstract Map.EntryCKeyType.ValueType>
18 makePairí KeyType key. ValueType value ):
19 protected abstract SetCKeyType> makeEmptyKeySetí ):
20 protected abstract SetCMap.EntryCKeyType.ValueType»
21 clonePairSetí SetCMap. EntryCKeyType .Val ueType» pairSet ):
22
23 private Map.EntryCKeyType.ValueType> makePairí KeyType key )
24 I return makePairí (KeyType) key. null ): I
25 protected SetCMap. EntryCKeyType.Val ueType» getSetí )
26 I return theSet: 1
27
28 public 1nt s1ze( )
29 I return theSet.sizeí ): I
30 public boolean isEmptyí )
31 I return theSet.IsEmptyí ): 1
3? public boolean contalnsKeyi KeyType key )
33 I return theSet.containsí makePairí key ) ): I
34 public void clearí )
35 ( theSet.clearí ); 1
36 public String toStringi )
37 {
38 StringBuilder result - new StringBuilderi ):
39 fori Map.EntryCKeyType.ValueType> e : entrySeti ) )
40 result.appendi e + ". “ ):
41 result.replaceí result.lengthí) - 2. result.lengthí). ):
4? return result.toStringi );
43 I
44
45 public ValueType getí KeyType key )
46 ( /* Figura 19.79 */ 1
resulta ser Comparabl e y aplica el comparador de TreeSet a la clave. Más adelante analizaremos los
detalles de esto.
Muchas de las rutinas del mapa se traducen a operaciones sobre el conjunto subyacente, como se
muestra en las líneas 28 a 35. Las rutinas básicas get. put y reraove se muestran en la Figura 19.79.
Estas rutinas simplemente efectúan la traducción a operaciones sobre el conjunto. Todas requieren
uta llamada a raakePal r para crear un objeto del mismo tipo de los contenidos en theSet; put es
uta rutina representativa de esta estrategia.
I>a parte complicada de la clase Haplrapl consiste en proporcionar la capacidad de obtener las
vistas de las claves y valores. En la declaración de la clase Haplrapl en la Figura 19.78, vemos
que keyset, implementada en las líneas 75 y 76, devuelve una referencia a una instancia de una
clase intema llamada KeySetClass. y values, implementada en las líneas 77 y 78. devuelve una
referencia a una instancia de una clase intema denominada ValueCol lectionClass. Las clases
KeySetClass y ValueCol lectionClass tienen algunos aspectos comunes, así que amplían la
clase interna genérica llamada VlewClass. Estas tres clases aparecen en las líneas 82 a 87 de la
declaración de la clase y su implementación se muestra en la Figura 19.80.
Eh la Figura 19.80, vemos que en la clase VlewClass genérica, las llamadas a clear y size se
delegan en el mapa subyacente. Esta clase es abstracta, porque AbstractCol lection no proporciona
el método Iterator especificado en Collection, como tampoco lo hace VlewClass. La clase
ValueCol lectionClass amplía V1ewClass<ValueType> y proporciona un método Iterator; este
método devuelve una instancia recién construida de la clase interna ValueCol lectionlterator
(que por supuesto implementa la interfaz Iterator). ValueCol lectionlterator delega las
Damadas a next y hasNext y se muestra en la Figura 19.81; la analizaremos dentro de unos
momentos. KeySetCl ass amplía V1 ewCl ass<KeyType>, pero como es un Set. debe proporcionar el
método (no estándar) getHatch además del método Iterator. Puesto que la propia clase KeySet no
será utilizada para representar un Hap, este método no será necesario, por lo que la implementación
simplemente genera una excepción. También proporcionamos un método reraove para eliminar el
par asociado clave/valor del mapa subyacente. Si este método no se proporciona, el predeterminado
que se hereda de AbstractCol lection utiliza una búsqueda secuencial. que es enormemente
ineficiente.
La Figura 19.81 completa la clase Haplrapl proporcionando implementaciones de KeySet
lterator y ValueCol lectionlterator. Ambas mantienen un iterador que tiene una vista sobre el
mapa subyacente, y ambas delegan las llamadas a next. hasNext y reraove en el mapa subyacente.
Bi el caso de next, se devuelve la parte apropiada del objeto Hap. Ent ry que está siendo vista por el
iterador del mapa.
1 /**
2 * Devuelve el valor almacenado en el mapa asociado con la clave.
3 ‘ gparam key la clave que hay que buscar.
< * greturn el valor correspondiente a la clave o nuil si no
5 * se encuentra la clave. Dado que los valores nuil están permitidos.
6 * comprobar si el valor de retorno es nuil puede no ser una forma
7 * segura de determinar si la clave está en el mapa,
ft ♦/
' Continúa
1 /**
2 * Ciase abstracta para modelar una vista (vista de clave o de valor).
3 * Implementa los métodos size y clear, pero no el método iterator.
4 * t.a vista delega en el mapa subyacente.
5 */
6 private abstract class ViewClass<AnyType> extends AbstractCollect1on<AnyType>
7 I
ft public int sizeí )
9 I return Maplmpl.this.sizeí ): I
10
11 publ ic void clearí )
1? i Maplmpl.this.clearí ): l
13 |
14
15 /♦*
16 * Clase para modelar la vista del conjunto de claves.
17 * remove se sustituye (en caso contrario se usa una búsqueda secuencial).
18 * iterator proporciona un KeySetlterator (véase la Figura 19.81).
19 * getMatch, la parte no estándar de weiss.uti1.Set no es necesaria.
20 */
21 private class KeySetClass extends ViewClass<KeyType> implements Set<KeyType>
22 I
23 public boolean removeí Object key )
24 I return Maplmpl.this.removeí (KeyType) key ) I- null: I
25
26 public Iterator<KeyType> Iteratorí )
27 ( return new KeySetlterator( ): I
28
29 public KeyType getMatchí KeyType key )
30 I throw new UnsupportedOperationExceptioni ); 1
3! 1
32
33 /*♦
34 * Clase para modelar la vista de la colección de valores.
35 * Se utiliza remove predeterminado que es una búsqueda secuencial.
36 * iterator proporciona un ValueCollectionlterator (véase la Figura 19.81).
37 *7
38 private class ValueCollectionClass extends ViewClass<ValueType>
39 |
40 public Iterator<ValueType> iteratorí )
4! | return new ValueCollectionlteratorí ): }
42 I
1 /**
2 * Clase utilizada para iterar a través de la vista de claves.
3 * Delega en un Iterador del conjunto subyacente de entradas.
4 */
5 private class KeySetIterator implements Iterator<KeyType>
6 {
7 private Iterator<Map. EntryCKeyType,Va lueType» itr « theSet. iteratorí):
8
9 public boolean hasNextí )
10 I return 1tr.hasNextí ): I
1!
12 public void removeí )
13 l itr.removeí ): 1
14
15 public KeyType nextí )
16 I return itr.nextí ).getKeyi ): I
17 l
18
19 /**
20 * Clase para Iterar a través de la vísta de la colección de valores.
21 * Delega en un iterador del conjunto subyacente de entradas.
22 */
23 private class ValueCollectionlterator implements Iterator<ValueType>
24 {
25 private IteratorCMap.EntryCKeyType.ValueType» itr = theSet. iteratorí):
26
27 public boolean hasNextí )
28 I return itr.hasNextí ): I
29
30 public void removeí )
31 | itr.removeí ): I
32
33 public ValueType nextí )
34 | return itr.nextí ).getValue( ): I
35 1
Habiendo escrito Haplrapl, TreeHap resulta ser simple, como se muestra en la Figura 19.82.
1.a mayor parte del código se centra en tomo a la deflnción de la clase privada interna Pair,
que implementa la interfaz Hap.Entry ampliando Haplrapl .Pair. La clase Pair implementa
Comparabl e, utilizando el comparador sobre la clave, si es que se suministra uno. o efectuando una
conversión de tipo a Comparable.
Capítulo 19 Árboles de búsqueda binaria
! package weiss.uti1:
?
3 public class TreeMapCKeyType.ValueType> extends MapImpKKeyType.ValueType>
4 I
5 public TreeMapí )
6 ( super( new TreeSetCMap.EntryCKeyType.ValueType>>( ) ): )
7 public TreeMapí MapCKeyType.ValueType> other )
8 ( super( other ); I
9 public TreeMapí ComparatorC? super KeyType> comparator )
10 I
11 super( new TreeSetCMap.EntryCKeyType.ValueType»( ) ):
12 keyCmp - comparator;
13
i4
15 public ComparatorC? super KeyType> comparator )
16 ( return keyCmp; 1
17
18 protected Map.EntryCKeyType,ValueType> makePa1r( KeyType key. ValueType value)
19 I return new Pair( key. value ); I
20
21 protected SetCKeyType> makeEmptyKeySett )
?? I return new TreeSetCKeyTypeX keyCmp ); )
23
24 protected SetCMap.EntryCKeyType.ValueType>>
25 clonePairSett SetCMap.EntryCKeyType.ValueType>> pairSet )
26 I return new TreeSetCMap.EntryCKeyType.ValueType»( pairSet ); I
27
28 private final class Pair extends Maplmpl.PairCKeyType.Va1ueType>
29 Implements ComparableCMap.EntryCKeyType.ValueType>>
30 t
31 public Pairt KeyType k. ValueType v )
32 I supert k ,v ): I
33
34 public int compareToí Map.EntryCKeyType.ValueType> other )
35 I
36 if( keyCmp !- null )
37 return keyCmp.compareí getKeyi ). other.getKeyi ) ):
38 el se
39 return (( Comparable) getKeyi ) ).compareToí other.getKeyi ) );
40 1
41
4?
43 private ComparatorC? super KeyType> keyCmp;
44
19.8 Árboles-B
Hasta ahora, hemos asumido que podemos almacenar una estructura de datos completa en la
memoria principal de una computadora. Sin embargo, suponga que tenemos más datos de los que
caben en la memoria principal y que, como resultado, debemos hacer que la estructura de datos
resida en disco. Cuando esto sucede, las reglas del juego cambian, porque el modelo O mayúscula
ya no tiene sentido.
El problema es que un análisis O mayúscula supone que todas las operaciones son iguales. Sin
embargo, esto no es cierto, especialmente cuando está involucrada la EZS de disco. Por un lado,
ira máquina de 500-MIPS supuestamente ejecuta 500 millones de instrucciones por segundo. Eso
es bastante rápido, principalmente porque la velocidad depende básicamente de las propiedades
eléctricas. Por otro lado, un disco es un dispositivo mecánico. Su velocidad depende en buena
medida del tiempo requerido para hacer girar el disco y desplazar un cabezal del disco. Muchos
discos rotan a 7.200 RPM. Por tanto, en 1 minuto, hacen 7.200 revoluciones; esto quiere decir
que una revolución tarda 1/120 segundos, es decir, 8,3 ms. En promedio, cabría esperar que
tengamos que hacer rotar un disco la mitad de una vuelta para encontrar lo que estamos buscando,
pero esto se ve compensado por el tiempo necesario para mover el cabezal del disco, por lo que
obtenemos un tiempo de acceso de 8,3 ms. (Esta estimación es bastante benévola; los tiempos
de acceso comprendidos entre 9 y 11 ms son más comunes.) En consecuencia, podemos hacer
aproximadamente 120 accesos a disco por segundo. Este número de accesos parece adecuado, hasta
que lo comparamos con la velocidad del procesador tenemos 500 millones de instrucciones frente a
120 accesos a dtsco. Dicho de otra forma, cada acceso a disco equivale a 4.000.000 de instrucciones.
Fbr supuesto, todos estos números son cálculos burdos, pero las velocidades relativas están bastante
claras; los accesos a disco son increíblemente costosos. Además, las velocidades de procesador se
están incrementando a una tasa mucho mayor que las velocidades de disco (son los tamaños de
disco los que se están incrementando muy rápidamente). Por tanto, estaríamos dispuestos a realizar
un montón de cálculos simplemente para ahorramos un acceso a disco. En casi todos los casos, el
rtímero de accesos a disco domina el tiempo de ejecución. Reduciendo a la mitad el número de
accesos a disco, podemos dividir por dos el tiempo de ejecución.
He aquí cómo se comportaría el árbol de búsqueda típico con un disco. Cuando bs datos son
Súponga que queremos acceder a los registros que contienen los permisos demasía dos pera cater
en memoria, el número do
de conducir de los ciudadanos del estado de Florida. Vamos a suponer que
accesos a dheo pasa a
tenemos 10.000.000 de elementos, que cada clave tiene 32 bytes (que repre ser importante. Un acceso
sentan un nombre) y que cada registro es de 258 bytes. Suponemos que este adteoestnaeíNemenfe
costoso comparado con
conjunto de datos no cabe en memoria principal y que nosotros somos uno una bsaruedún Bpica de
de los 20 usuarios del sistema (por lo que tenemos 1/20 de los recursos). En computadora.
esta situación, podemos ejecutar 25 millones de instrucciones en 1 segundo o
realizar seis accesos a disco.
El árbol de búsqueda binaria no equilibrado es un desastre. En el caso
Indusodrendbilento
peor, tiene una profúndidad lineal y por tanto podría requerir 10 millones de logarítmico es haceptable.
accesos a disco. En promedio, una búsqueda con éxito requeriría 1,38 log /V Necesitamos realzar
tas búsquedas en tres
accesos a disco y como log 10.000.000 es aproximadamente igual a 24, una o cuatro accesos, las
búsqueda media necesitaría 32 accesos a disco, lo que equivale a 5 segun adualzacbnes pueden
dos. En un árbol típico construido aleatoriamente, cabría esperar que unos fardar tn poco mas.
requerirían unos 100 accesos a disco o 16 segundos. Un árbol rojo-negro sería algo mejor: el caso
peor de 1,44 log Nes bastante improbable que se produzca y el caso típico es muy próximo a log N.
Por tanto, un árbol rojo-negro emplearía unos 25 accesos a disco como promedio, lo que requiere 4
segundos.
Queremos reducir los accesos a disco a un número constante que sea
Unártofrfetesjuafe
Ujrbpemie ratifica-
muy pequeño, como por ejemplo tres o cuatro accesos. Para ello, estamos
dones óe Mvlas A medida dispuestos a escribir código complicado porque las instrucciones de la
que «incrementad máquina prácticamente no tienen coste, siempre y cuando no lleguemos a
gado de ramificación, la
prcftrtJdaddbmhuye soluciones ridiculamente poco razonables. Un árbol de búsqueda binaria no
funciona porque el árbol rojo-negro típico está próximo a la altura óptima
y no podemos bajar de log /Vcon un árbol de búsqueda binaria. La solución
es intuitivamente simple: sí tenemos un mayor grado de ramificación, la altura disminuirá. Así.
mientras que un árbol binario perfecto de 31 nodos tiene 5 niveles, un árbol 5-ario de 31 nodos
solo tiene tres niveles, como se muestra en la Figura 19.83. Un árbol de búsqueda M-ar¡apexrc\\te
bifurcaciones de Mvías. y a medida que se incrementa el grado de bifurcación, la profundidad se
reduce. Mientras que un árbol binario completo tiene una altura que es aproximadamente log? N, un
árbol Mario completo tiene una altura de aproximadamente logv N.
Podemos crear un árbol de búsqueda María de forma bastante similar a como hemos creado un
árbol de búsqueda binaria. En un áibol de búsqueda binaria, necesitamos una clave para decidir cuál
de las dos ramas seguir. Eh un árbol de búsqueda Marta necesitamos M - 1 claves para decidir qué
tama tomar. Para hacer que este esquema sea eficiente en el caso peor, necesitamos garantizar que
el árbol de búsqueda María sea equilibrado de alguna forma. En caso contrario, como en un árbol
de búsqueda binaria, podría degenerar en una lista enlazada. De hecho, queremos una condición de
equilibrio todavía más restrictiva: es decir, no queremos que un árbol de búsqueda María degenere
ni siquiera en un árbol de búsqueda binaría, porque entonces estaríamos limitados a un número de
acceso del orden de log N.
Una forma de implementar esto consiste en utilizar un árbol-B^ es la
Un ártcASes la estructura estructura de datos más popular para búsquedas limitadas por disco. Aquí,
de datos más popubr para vamos a describir el árbol-B básico3: existen muchas variantes y mejoras, y
búsquedas kn tedas por
dbco
las implementaciones son un tanto complejas, porque es necesario tener en
cuenta unos cuantos casos distintos. Sin embargo, en principio esta técnica
^rantiza que solo se realice un pequeño número de accesos a disco.
Un árbol-B de orden A/es un árbol Mario con las siguientes propiedades? Baw-8 «ene toda una
SG(Í6 Ú9 pf OpiodddGS
1. Los elementos de datos se almacenan en las hojas. es&wtraies.
2. Los nodos que no son hojas almacenan hasta N - l claves con el fin
de guiar la búsqueda; la clave i representa la clave más pequeña en el
subárbol /+ 1.
3. I Ji raíz es una hoja o tiene entre 2 y Mhijos.
4. Todos los nodos que no son hojas (excepto la raíz) tienen entre f M12 ly A/hijos.
5. 'Iodas las hojas están a la misma profundidad y tienen entre í L Í21 y L elementos de datos, para
un cierto valor de £ (enseguida explicaremos cómo se determina
Eh la Figura 19.84 se muestra un ejemplo de un árbol-B de orden 5. Los nodos deben estar
Observe que todos los nodos que no son hojas tienen entre tres y cinco hijos leños ai menos a fe mitad,
para garantor que ei arto)
(y por tanto entre dos y cuatro claves); la raíz podría posiblemente tener no degenere en un árbol
solo dos hijos. Aquí. £ = 5, lo que quiere decir que £y M coinciden en este Oralo
ejemplo, aunque esta condición no es necesaria Puesto que £es 5. cada hoja
tiene entre tres y cinco elementos de datos. Exigir que los nodos estén llenos
a la mitad garantiza que el árbol-B no degenere en un árbol binario simple. Diversas definiciones
de los árboles-B modifican esta estructura, fundamentalmente en algunos aspectos menores, pero la
definición presentada aquí es una de las más comúnmente utilizadas.
Cada nodo representa un bloque de disco, por lo que elegimos M y £ Dejaros los máxtnos
teniendo en cuenta el tamaño de los elementos que se estén almacenando. wires de .Wy ¿que
permitan encajar un nodo en
Suponga que cada bloque almacena 8.192 bytes. En nuestro ejemplo de un sob Moque dedsoo.
Florida, cada clave utiliza 32 bytes, así que en un árbol-B de orden M,
tendríamos M - 1 claves, lo que nos da un total de 32A/ - 32 bytes más M
ramas. Puesto que cada rama es esencialmente el número de otro bloque de disco, podemos asumir
que una rama ocupa 4 bytes. Por tanto, las ramas utilizan 4M bytes y la memoria total requerida para
un nodo que no sea una hoja es 36A/ - 32. El valor máximo de Mpara el que 36A/ - 32 no supera
& 192 es 228, así que elegiríamos M = 228. Como cada registro de datos ocupa 258 bytes, podríamos
4 Las propiedades 3 y 5 deben retajarse para las primeras ¿ primeras inserciones (¿ es un parámetro utilizado en la propiedad 5).
750 Capitulo 19 Árboles de búsqueda binaria
almacenar 32 registros en cada bloque. Por tanto, seleccionaríamos L = 32. Cada hoja tendrá entre
16 y 32 registros de datos y cada nodo intemo (excepto la raíz) puede ramificarse en al menos 114
vías. Para los 10.000.000 de registros, habrá como máximo 625.000 hojas. En consecuencia, en el
caso peor, las hojas se encontrarían en el nivel 4. En términos más concretos, el número de accesos
de caso peor está dado por aproximadamente log,« N, más o menos l.
El problema que nos resta es cómo añadir y eliminar elementos del árbol-B. En las ideas que
hemos esbozado, observe que vuelven a aparecer muchos de los temas que hemos comentado
anteriormente.
Comenzaremos examinando la operación de inserción. Suponga que
SU hoja frene espado
para un nuevo demento, queremos insertar el valor 57 en el árbol-B de la Figura 19.84. Una búsqueda
b insertamos y habremos hacía abajo del árbol revela que 57 no se encuentra ya incluido en el árbol.
terminado.
Fbdemos añadir 57 a la hoja como un quinto elemento, pero podríamos tener
que reorganizar todos los datos de la hoja para hacerlo. Sin embargo, el coste
es despreciable comparado con el del acceso a disco, que en este caso incluye
Si la hoja esta lena, también una escritura en disco.
podemos insertar un nuevo
Este procedimiento parece relativamente sencillo, porque la hoja no se
demento dMdlendota
hoja y formando dos nodos encontraba ya llena. Suponga que ahora queremos insertar el valor 55. La
medto vados. Figura 19.85 muestra un problema: la hoja en la que debería ir 55 ya está
Dena La solución es simple: ahora tenemos L + l elementos, por lo que lo
dividimos en dos hojas, que está garantizado que tendrán el número mínimo
de registros de datos necesario. Aquí, formaríamos dos hojas con tres elementos cada una. Hacen
falta dos accesos a disco para escribir estas hojas y un tercer acceso a disco para actualizar el
padre. Observe que en el padre, se modifican tanto las claves como las ramas, pero lo hace de una
forma controlada que se puede calcular fácilmente. El árbol-B resultante se muestra en la Figura
19.86. Aunque dividir los nodos consume tiempo, porque requiere al menos dos escrituras en disco
adicionales, es un suceso relativamente raro. Por ejemplo, si L es 32, cuando se divide un nodo se
crean dos hojas con 16 y 17 elementos, respectivamente. Para la hoja con 17 elementos, podemos
realizar 15 inserciones más sin que se produzca otra división. Dicho de otra forma, por cada división
que se realiza habrá aproximadamente L/2 no divisiones.
La división de un nodo en el ejemplo anterior ha funcionado porque el padre no tenía el máximo
número de hijos. ¿Pero qué sucedería si lo tuviera? Suponga que insertamos 40 en el árbol-B
mostrado en la Figura 19.86. Debemos dividir la hoja que contiene las claves 35 a 39 (y que ahora
Rflura 19.86 La Inserción de 55 end arbd-8 mostrado en la Figura 19-86 provoca una dMston en dos hojas.
contiene también 40 en dos hojas. Pero hacer esto haría que el padre tuviera taduWúndeunnodocrea
sets hijos, y solo está permitido que tenga cinco. La solución consiste en uihfoaddond parad
pede dd nodo hoja. SI d
dividir el padre, mostrándose el resultado de esta operación en la Figura
padre ya tened número
19.87. Al dividir el padre, tenemos que actualizar los valores de las claves y raiidmo dehjos, saá
también el padre del padre, lo que exige dos escrituras en disco adicionales necesario dXdrio también.
(así que esta inserción nos cuesta cinco escrituras en disco). De nuevo, sin
embargo, las claves cambian de una forma muy controlada, aunque es verdad
que el código no resulta simple debido al gran número de casas pasibles. Puede que tengamos quo
conteuardutfendo nodos
Cüando se divide un nodo que no es una hoja, como aquí, su padre pasa a drante todo el cami no
tener un hijo más. ¿qué sucede si el padre ya ha alcanzado su límite de hijos? de subida por dárbd
(aunque esta pesibttad
Entonces continuamos dividiendo nodos hacia arriba del árbol hasta encontrar
eshxxobable). End caso
tn padre que no necesite ser dividido o hasta alcanzar la raíz. Observe que ya peor, dddrtamos bratz
hemos presentado esta idea en los árboles rojo-negro de actualización abajo- creando una nueva raíz con
doshjos
anriba y en los árboles AA. Si dividimos la raíz, tendremos dos raíces, pero
obviamente este resultado es inaceptable. Sin embargo, podemos crear una
nueva raíz que tenga esas raíces divididas como hijos, lo cual es la razón
de que a la raíz se le conceda la excepción de tener un mínimo de dos hijos. También es la única
manera de que un árbol-B pueda crecer en altura. No hace falta decir que tener que dividir nodos a
lo largo de todo el camino que va hasta la raíz es un suceso extremadamente raro, porque un árbol
con cuatro niveles indica que la raíz ha sido dividida dos veces a lo largo de toda la secuencia de
inserciones (suponiendo que no se hayan producido borrados). De hecho, la división de cualquier
nodo que no sea una hoja es también bastante rara.
192 97
2 [si wl F26l ͻ1 38 41 48 [sil [m1 [sTl [66] 72 pal 83l 87 [92 97
4 10 20 28 36 39 42 49 52 55 58 68 73 79 84 89 93 98
6 12 22 30 37 40 44 SO 53 56 59 69 74 81 85 90 95 99
14 24 31 46 70 76
16 32
Rflura 19.87 La Inserción de 40 en d arbd-8 mostrado en la Figura 1986 provoca una dMsión en dos hojas y luego una didst ún dd nodo padre.
752 Capitulo 19 Árboles de búsqueda binaria
26 41 66 83
,-------------------------- --------------------------1
,¡8|.¡18¡.| II II |38¡'¡ l| || ||l48l.lsi||l&4||l57l¡ 11721,178Ú Í1 [1 f.|87|.|9?|,¡
7 8 18 26 35 38 41 48 51 54 57 66 72 78 83 87 92
28 36 39 4? 49 52 55 58 68 73 79
30 37 40 44 50 53 56 59 69 74 81
31 46 70 76
32
Existen otras formas de tratar el desbordamiento de hijos. Una técnica consiste en enviar un hijo
hacia arriba para su adopción, por si acaso un vecino tiene espacio para él. Por ejemplo, para insertar
29 en el árbol-B mostrado en la Figura 19.87, podríamos hacer sitio para él desplazando 32 a la
siguiente hoja. Esta técnica requiere una modificación del padre, porque las claves se ven afectadas.
Sin embargo, tiende a hacer que los nodos estén más llenos y ahorra espacio a largo plazo.
Podemos realizar un borrado encontrando el elemento que haya que
0 borrado fúndona ata
hversa: si una hoja pierde
eliminar y borrándolo. El problema es que si la hoja en la que estaba
un h(o, puede que sea contenido tuviera un número mínimo de elementos de datos, ahora pasaría
necesario cornbharta con a estar por debajo del mínimo. Podemos rectificar la situación adoptando un
otra hoja. La combirodcn
de nodos puede continuar
elemento de una hoja vecina si es que el nodo vecino no tiene él mismo el
durante todo el canino número mínimo de elementos. Si lo tuviera, podemos combinar nuestro nodo
naoa arnoa oe» anxx,
hoja con el vecino para formar una hoja completa. lamentablemente, en este
aunque esta postoBdad
es Improbabl e. En el caso caso el padre habrá perdido un hijo. Si eso hace que el padre caiga por debajo
peor, bratz pierde uno de de su mínimo, seguiremos la misma estrategia Este proceso podría repetirse
sus dos hjos. Entonces
durante todo el camino de ascenso hacia la raíz. La raíz no puede tener un
podemos borrar bratz y
emplear el otro h|o como ia solo hijo (e incluso si eso se permitiera, sería algo bastante estúpido). Si una
nueva raíz. raíz se quedara con un solo hijo como resultado del proceso de adopción, lo
que hacemos es eliminar la raíz, haciendo que su hijo sea la nueva raíz del
¿bol -esta es la única forma de que un árbol-B pierda altura. Suponga que
queremos eliminar el elemento 99 del árbol-B mostrado en la Figura 19.87. La hoja tiene solo dos
elementos y su vecino está ya en su valor mínimo de tres, así que combinamos los elementos en una
nueva hoja de cinco elementos. Como resultado el padre solo tiene dos hijos. Sin embargo, puede
adoptar de un vecino, porque el vecino tiene cuatro hijos. Como resultado de la adopción, ambos
terminan teniendo tres hijos, como se muestra en la Figura 19.88.
Resumen
Ijos árboles de búsqueda binaria soportan casi todas las operaciones útiles en el diseño de
algoritmos, y el coste promedio logarítmico es muy pequeño. Las implementaciones no recursivas
de los árboles de búsqueda son algo más rápidas que las versiones recursivas, pero estas últimas
son más compactas, más elegantes y más fáciles de comprender y de depurar. El problema con los
árboles de búsqueda es que su rendimiento depende fuertemente de que la entrada sea aleatoria. Si
no lo es, el tiempo de ejecución se incrementa significativamente, incluso hasta el punto en que los
árboles de búsqueda se transforman en costosas listas enlazadas.
Conceptos clave 753
Las formas de tratar con este problema implican todas ellas reestructurar el árbol para garantizar
que haya un cierto equilibrio en cada nodo. La reestructuración se consigue mediante rotaciones
del árbol que preservan la propiedad del árbol de búsqueda binaria. El coste de una búsqueda es
típicamente menor que para un árbol de búsqueda binaria no equilibrado, porque el nodo promedio
tiende a estar más próximo a la raíz. Sin embargo, los costes de inserción y borrado suelen ser
mayores. Las variantes equilibradas difieren en la cantidad de esfuerzo de codificación requerido
para implementar las operaciones que modifican el árbol.
El esquema clásico es el árbol AVL en el que. para todo nodo, las alturas de sus subárboles
izquierdo y derecho pueden diferir en como máximo 1. El problema práctico con los árboles
AVL es que implican un gran número de casos distintos, haciendo que el coste adicional de cada
inserción y borrado sea relativamente alto. Hemos examinado dos alternativas en este capítulo.
La primera era el árbol rojo-negro de procesamiento arriba-abajo. Su ventaja principal es que el
re-equilibrado se puede implementar en una única pasada descendente del árbol, en lugar de tener
que realizar la tradicional pasada hacia abajo y luego otra hacia arriba. Esta técnica proporciona un
código más simple y una mayor velocidad que los árboles AVL I^a segunda variante es el árbol
AA. que es similar al árbol rojo-negro de procesamiento abajo-amiba. Su ventaja principal es una
implementación recursiva relativamente simple, tanto para la inserción como para el borrado.
Ambas estructuras utilizan nodos centinela para eliminar los molestos casos especiales.
Solo debería utilizar un árbol de búsqueda binaria no equilibrado sí está seguro de que los datos
son razonablemente aleatorios o que la cantidad de datos es relativamente pequeña. Utilice el árbol
rojo-negro si le preocupa la velocidad (y no le preocupa demasiado el borrado). Use el árbol AA si
desea obtener una implementación sencilla con una velocidad más que aceptable. Emplee el árbol-B
cuando la cantidad de datos sea demasiado grande como para almacenarlos en la memoria principal.
Eh el Capítulo 22 examinaremos otra alternativa: el árbol splay. Se trata de una alternativa
interesante al árbol de búsqueda equilibrado: es simple de codificar y bastante competitivo en la
práctica. En el Capítulo 20 examinaremos la tabla hash, un método completamente distinto que se
utiliza para implementar operaciones de búsqueda.
Conceptos clave
árbol AA Un árbol de búsqueda equilibrado que es el preferido cuando hace falta un caso
peor ¿Xlog /V), cuando una implementación descuidada es aceptable y cuando hacen
falta borrados. (718)
árbol AVL Un árbol de búsqueda binaria con la propiedad adicional de equilibrio de que,
para cualquier nodo del árbol, la altura de los subárboles izquierdo y derecho puede
diferir en como máximo 1. Tiene una importancia histórica, ya que fue el primer árbol
de búsqueda equilibrado. También ilustra la mayoría de las ideas utilizadas en otros
esquemas de árbol de búsqueda. (696)
árbol-B La estructura de datos más popular para búsquedas limitadas por disco. Existen
muchas variantes de la misma idea. (748)
árbol AebánpwAabbHsbi Una estructura de datos que soporta la inserción, la búsqueda
y el borrado en un tiempo promedio CXlog N). Para cualquier nodo del árbol de
búsqueda binaria, todos los nodos con claves más pequeñas se encuentran en el subár
754 Capitulo 19 Árboles de búsqueda binaria
bol Izquierdo y todos los nodos con claves más grandes se encuentran en el subárbol
derecho. No se permiten duplicados. (677)
árbol áe básqwsáa bisarla cfaflbraáo Un árbol que tiene una propiedad estructural
añadida para garantizar una profundidad logarítmica en caso peor. Las actualizaciones
son más lentas que con el árbol de búsqueda binaria, pero las accesos son más rápidos.
(696)
árbol Mario Un árbol que permite bifurcaciones de Afvías; a medida que se incrementa
la bifurcación, la profundidad se reduce. (748)
árbol rofo-negro Un árbol de búsqueda equilibrado que constituye una buena alternativa
al árbol AVL porque se puede utilizar una sola pasada arriba-abajo durante las rutinas
de inserción y borrado. Los nodos se marcan con color rojo y negro en una forma
restringida que garantiza una profundidad logarítmica. Los detalles de codificación
tienden a proporcionar una implementación más rápida. (704)
bárralo peratoso Un método que marca los elementos como borradas, pero que no los
borra en realidad. (718)
ralnrr bwlinatnl En un árbol AA, una conexión entre un nodo y un hijo que está situado
al mismo nivel. Un enlace horizontal solo debería ir hacia la derecha y no deben existir
dos enlaces horizontales consecutivos. (719)
lon^baá <Kbnaáe<araáso La suma del coste de acceder a todos los nodos extemos del
árbol en un árbol binario, lo que mide el coste de una búsqueda que no tenga éxito.
(694)
ioo^bai Interna áe caraba» La suma de las profundidades de los nodos en un árbol
binario, lo que mide el coste de una búsqueda que tenga éxito. (694)
■M lea nodo Eh un árbol A A. el número de enlaces izquierdos en el camino que va
hasta el nodo centinela nullNode. (719)
aoáo<Báemo ád árbol El nodo nuil. (694)
lotadán doble Equivalente a dos rotaciones simples. (701)
iotadónabapte intercambio de los papeles del padre y del hijo al tiempo que se mantiene
el orden de búsqueda. El equilibrio se restaura medíante rotaciones del árbol. (698)
skew Eliminación de enlaces horizontales izquierdos, realizando una rotación entre un
nodo y su hijo izquierdo. (720)
split Eliminación de enlaces horizontales derechos consecutivos, realizando una rotación
entre un nodo y su hijo derecho. (720)
Errores comunes
L Utilizar un árbol de búsqueda no equilibrado cuando la secuencia de entrada no sea
aleatoria dará un rendimiento muy pobre.
& La operación remove es bastante complicada de codificar correctamente, especialmente
para un árbol de búsqueda equilibrado.
Ejercicios 755
& El borrado perezoso es una buena alternativa a la operación remove estándar, pero
entonces es necesario modificar otras rutinas, como fi ndHi n.
< El código para árboles de búsqueda equilibrados es siempre muy proclive a errores.
& Olvidarse de devolver una referencia a la nueva raíz del subárbol es un error para los
métodos auxiliares privados insert y remove. El valor de retomo se debe asignar a
root.
A Si se utilizan nodos centinela y luego se escribe código que se olvida de esos nodos
centinela, pueden aparecer bucles infinitos. Un error bastante común es comprobar si
ni nodo es nul 1 cuando lo que se está empleando es un nodo centinela nul 1 Node.
Internet
Ejercicios
EN RESUMEN
1A1 Dibuje todos los árboles AVL que pueden resultar de la inserción de permutaciones
de 1. 2 y 3. ¿Cuántos árboles habrá? ¿Cuáles son las probabilidades de obtener cada
árbol, si todas las permutaciones son equiprobables?
1A2 Repita el Ejercicio 19.1 para cuatro elementos.
1A3 Repita los Ejercicios 19.1 y 19.2 para un árbol rojo-negro.
1A4 Muestre el resultado de insertar 3.1.4.6.9.2,5 y 7 en un árbol de búsqueda binaria
inicialmente vacío. A continuación muestre el resultado de borrar la raíz.
1A5 Dibuje todos los árboles de búsqueda binaria que pueden resultar de la inserción de
permutaciones de 1.2,3 y 4. ¿Cuántos árboles habrá?¿Cuáles son las probabilidades
de obtener cada árbol, si todas las permutaciones son equiprobables?
756 Capitulo 19 Árboles de búsqueda binaria
EN TEORÍA
1(17 Muestre el resultado de insertar los elementos l a 15 en orden en un árbol AVL
inicialmente vacío. Generalice este resultado (con la consiguiente demostración)
para demostrar qué sucede con los elementos l a 2* -1 cuando se insertan en un
árbol AVL inicialmente vacío.
l&ft Demuestre el Teorema 19.2.
ÍM Demuestre que todo árbol AVL se puede colorear como un árbol rojo-negro.
¿Satisfacen todos los árboles rojo-negro la propiedad del árbol AVL?
IflllO Proporcione un algoritmo para realizar la operación remove en un árbol AVL.
Iftll Demuestre que la altura de un árbol rojo-negro es como máximo aproximadamente
igual a 2 log Ny proporcione una secuencia de inserción que permita alcanzar dicha
cota.
EN LA PRÁCTICA
IftlS Escriba un método para un árbol de búsqueda binaria que admita dos claves, low y
ht gh, e imprima todos los elementos Zque se encuentran en el rango especificado
por low y high. Su programa debería ejecutarse en un tiempo promedio 0{K +
log N). donde /fes el número de claves impresas. Por tanto, sí /fes pequeña, solo
se debería examinar una pequeña parte del árbol. Utilice un método recursivo oculto
y no emplee un iterador en orden. Acote el tiempo de ejecución de su algoritmo.
IftlS Implemente de forma no recursiva findKth, utilizando la misma técnica empleada
para una operación f i nd no recursiva.
lftMUna representación alternativa que permite la operación findKth consiste en
almacenar en cada nodo el valor del tamaño del subárbol izquierdo más 1. ¿Por qué
sería ventajosa esta técnica? Escriba de nuevo la clase de árbol de búsqueda para
emplear esta representación.
IftlS Escriba un método para un árbol de búsqueda binaria que tome dos enteros, low
y high, y construya un árbol de búsqueda binaria Bi narySearchTreeWithRank
óptimamente equilibrado que contenga todos los enteros comprendidos entre low y
hi gh. ambos incluidos. Todas las hojas deben estar al mismo nivel (si el tamaño del
árbol es i menos que una potencia de 2) o en dos niveles consecutivos. Su rutina
debe ejecutarse en un tiempo iineai. Pruebe su rutina utilizándola para resolver el
problema de Josefo presentado en la Sección 13.1.
IflllO Implemente recursivamente find, findHin y findHax.
PROYECTOS DE PROGRAMACIÓN
lftl7 Escriba un programa para evaluar empíricamente las siguientes estrategias para
eliminar nodos con dos hijos. Recuerde que una estrategia implica sustituir el
Ejercicios 757
valor de un nodo borrado por algún otro valor. ¿Qué estrategia proporciona el
mejor equilibrado? ¿Cuál requiere menos tiempo de procesador para procesar una
secuencia completa de operaciones?
a Sustituya por el valor del nodo mayor, X, en Tt y elimine recursivamente X.
b. Alternativamente, sustituya por el valor en el nodo mayor de 7j o el valor en el
nodo más pequeño de 7^ y elimine de forma recursiva el nodo apropiado.
1618 Rehaga la clase BlnarySearchTree para implementar el borrado perezoso. Observe
que el hacer esto afecta a todas las rutinas. Especialmente complicadas son f 1 ndHi n
y findHax, que ahora deben implementarse recursivamente.
1619 Implemente el árbol de búsqueda binaria para utilizar una sola comparación de dos
vías por nivel para find, insert y reraove.
IftSO Implemente las operaciones del árbol de búsqueda con estadísticas de orden para el
árbol de búsqueda equilibrado que prefiera.
1821 Implemente de nuevo la clase TreeSet utilizando enlaces a los padres.
19J2Z Implemente el método higher de TreeSet. que devuelve el menor elemento del
conjunto que sea estrictamente superior al elemento especificado. A continuación,
implemente el método cel l i ng, que devuelve el menor elemento del conjunto que
sea mayor o igual que el elemento especificado. Ambas rutinas devuelven nuil si
no existe tal elemento.
1&23 Implemente el método lower de TreeSet, que devuelve el mayor elemento del
conjunto que sea estrictamente menor que el elemento especificado. A continuación,
implemente el método f 1 oor, que devuelve el mayor elemento del conjunto que sea
menor o igual que el elemento especificado. Ambas rutinas devuelven nuil si no
existe tal elemento.
1824 Modifique las clases TreeSet y TreeMap de modo que sus iteradores sean bidirec-
cionales.
1825 Implemente de nuevo la clase TreeSet añadiendo a cada nodo dos enlaces: next
y previous, que representen el elemento previo y el elemento siguiente que se
obtendrían en un recorrido del árbol en orden. Añada también nodos de cabecera y
de cola para evitar los casos especiales relativos a los elementos máximo y mínimo.
Esto simplifica considerablemente la implementación del iterador, pero requiere
revisar los métodos mutadores.
1828 Implemente el método descendíngSet de TreeSet que devuelve una vista del
conjunto cuyos métodos iteradores y toStrlng ven los elementos en orden decre
ciente. Los cambios en el conjunto subyacente deberían reflejarse inmediatamente
en la vista, y viceversa.
1827 E3 método subList (frora. to) de List devuelve una vista de la lista que va
desde la posición frora, inclusive, a la posición to, exclusive. Añada subList
a la implementación de ArrayLIst en el Capítulo 15 (puede modificar weiss.
utn .ArrayLIst o ampliarla en otro paquete, o ampliar java.utll .ArrayLIst).
En la librería Java, subList está escrita en términos de la interfaz List, pero
escriba la suya en términos de ArrayLIst. Necesitará definir una clase anidada
758 Capitulo 19 Árboles de búsqueda binaria
SubList que amplíe ArrayLIst. y hacer que subList devuelva una referencia
a una nueva instancia de SubList. Para que las cosas sean más sencillas, haga
que SubList mantenga una referencia al ArrayLIst principal, que almacene el
tamaño de la sublista y que almacene el desplazamiento dentro del ArrayLIst
principal. También puede hacer que todos los mutadores de la SubLl st generen una
excepción, de modo que la sublista devuelta sea en la práctica inmutable. Su clase
SubLl st debería proporcionar una clase intema SubLl st Iterator que implemente
weiss.utn .L1stlterator. Pruebe su código con el método de la Figura 19.89.
Observe que las sublistas pueden crear sublistas, todas las cuales hacen referencia
a porciones más pequeñas del mismo ArrayLIst principal. ¿Cuál es el tiempo de
ejecución de su código?
1&28 Utilice el método subLl st que ha escrito para implementar el algoritmo recursivo
de suma máxima de subsecuencia contigua de la Figura 7.20 para listas ArrayLi st.
Utilice iteradores en las dos sublistas para gestionar los dos bucles. Observe que
basta Java 6. la implementación será bastante lenta si utiliza java.utll. pero si
el Ejercicio 19.27 se ha implementado razonablemente, su código debería tener un
rendimiento comparable con la implementación de la Figura 7.20.
Md» Amplíe el Ejercicio 19.27 para añadir código que compruebe la existencia de
modificaciones estructurales en el ArrayLIst principal. Una modificación estruc
tural en el ArrayLIst principal invalida todas las sublistas. Después, añada compro
baciones de la existencia de modificaciones estructurales en una sublista, lo que
invalidaría todas las sublistas descendientes.
103© El método headSet se puede utilizar para obtener el rango de unvalorxenel TreeSet
t: t.headSet(x.true).s1ze().Sin embargo, no hay ninguna garantía de que esto
sea eficiente. Implemente el código de la Figura 19.90 y compruebe el tiempo de
if( arr.sizei ) — 0 )
6 return 0:
el se
8
9 int idx = r.nextlnti arr.sizei ) )
10
15 return arr.get( idx ) +
12 sum( arr.subL1st( 0, 1dx ) ) +
13 sum( arr.subListi 1dx + 1. arr.sizei ) ) )
14
15
Figura 19.89 Recosía n y sub Islas de matrices. Un ejemplo para el EJercWo 1927.
Referencias 759
ejecución para diversos valores de N. ¿Qué puede decir acerca del coste de obtener
el rango de un valor x? Describa cómo mantener el TreeSet / las vistas de modo
que el coste de si ze y por tanto el coste de calcular un rango sea O( log N).
Hiede encontrar más información sobre árboles de búsqueda binaria, y en particular sobre
las propiedades matemáticas de los árboles en [ 18] y (19].
Diversos artículos tratan con la falta teórica de equilibrio provocada por los algoritmos
de borrado sesgados en los árboles de búsqueda binaria. Hibbard ¡16] propuso el algoritmo
de borrado original y estableció que un borrado preserva la aleatoriedad de los árboles.
Solo se ha realizado un análisis completo para árboles con tres nodos (17] y cuatro nodos
¡3]. Eppinger (101 proporcionó evidencias empíricas tempranas de no aleatoriedad y
Culberson y Munro ¡7] y ¡8] proporcionaron algunas evidencias analíticas, pero no una
demostración completa para el caso general de inserciones y borrados entremezclados.
La afirmación de que el nodo más profundo en un árbol de búsqueda binaria aleatorio es
tres veces más profundo que el nodo típico está demostrada en [11]; el resultado no es en
absoluto sencillo.
Adelson-Velskii y Landis propusieron los árboles AVL (1). Eh [19] se presenta un
algoritmo de borrado. El análisis de los costes medios de búsqueda en un árbol AVL está
incompleto, pero en ¡20] se proporcionan algunos resultados. El algoritmo para árboles
rojo-negro con procesamiento arriba-abajo está tomado de ¡15]; en ¡21] se proporciona
ma descripción más accesible. En [12] se incluye una implementación de árboles rojo-
negro con procesamiento arriba-abajo sin nodos centinela; proporciona una demostración
convincente de la utilidad de nullNode. El árbol AA está basado en el árbol-B binario
simétrico presentado en [4]. La implementación mostrada en el texto está adaptada a
partir de la descripción de [2]. En [13] se describen muchos otros árboles de búsqueda
equilibrados.
760 Capitulo 19 Árboles de búsqueda binaria
17. A. T. Jonassen y D. E. Knuth, "A Trivial Algorithm Whose Analysts Isn’t", Journal
ofComputer and System Sciences 16 (1978), 301 -322.
1& D. E. Knuth, The Art ofComputer Programming: Vol. 1: Fundamental Algorithms.
3’ed., Addison-Wesley, Reading. MA, 1997.
1ft. D. E. Knuth. The Art ofComputer Programming: Vol. 3: Sorting and Searching, 2’
ed., Addtson-Wesley, Reading. MA. 1998.
SQL K. Melhorn, "A Partial Analysis of Height-Balanced Trees Under Random
Insertions and Deletions". SIAMJournal on Computing 11 (1982), 748-760.
21. R. Sedgewick, Algorithms in C++, Partes 1-4 (Fundamental Algorithms. Data
Structures, Sorting, Searching) 3’ed., Addison-Wesley, Reading, MA, 1998.