Está en la página 1de 30

MÓDULO 10

ARBOLES

Contenido
10.1 Definición
10.2 Terminología
10.3 Representación
10.4 Determinación de número de hojas
10.5 Determinación del grado

Objetivos
Conocer la estructura árbol, su representación y la forma de manipularlos.

Preguntas básicas
1. ¿Qué es un árbol?
2. ¿Cuáles son las hojas de un árbol?
3. ¿Qué es el grado de un nodo?
4. ¿Qué es el grado de un árbol?
5. ¿Qué es la altura de un árbol?
6. ¿Cuáles son los ancestros de un registro?
7. ¿Cómo se representa un árbol?
8. ¿Qué es un árbol n-ario?
9. ¿Qué es un bosque?

Introducción
Las estructuras que hemos tratado hasta ahora son estructuras lineales. Vamos a comenzar a tratar en
este módulo estructuras no lineales. Comenzaremos considerando la estructura árbol.

10.1 Definición:
Un árbol es un conjunto de n registros (n > 0, árbol vacío no está definido), tales que hay un registro
especial llamado raíz y los demás registros están particionados en conjuntos disjuntos, cada uno de los
cuales es también un árbol.

De la definición se desprende que un árbol es una estructura recursiva, además cada registro se podrá
considerar como la raíz de un sub-árbol. Es decir, un árbol está compuesto de árboles. Un ejemplo de un
árbol se presenta en la figura 10.1.

Raiz Nivel

a 1

b f g 2

c d h i m n 3

e j k 4

l 5

Figura 10.1

En la figura 10.1 se tiene el árbol cuya raíz es a. Dicho árbol está compuesto por el árbol cuya raíz es b, el
árbol cuya raíz es f y el árbol cuya raíz es g. A su vez, b está compuesto por el árbol cuya raíz es c y el
árbol cuya raíz es d. Y así sucesivamente. Es supremamente importante entender esta parte de la
definición.
10.2 Terminología de árboles.
Las ramificaciones de cada nodo o registro suelen llamarse hijos y los nodos de los cuales salen las
ramificaciones se llaman padres. Los registros que tienen un mismo padre se denominan hermanos.

Grado de un registro: Es el número de ramificaciones que salen de ese registro. En el ejemplo de la


figura se tiene: a tiene grado 3, b tiene grado 2, j tiene grado 0.
Grado de un árbol: Es el máximo grado de cualquier registro del árbol. El árbol de la figura 10.1 tiene
grado 4, ya que el registro que contiene la g tiene grado 4 y es el registro que mayor grado tiene..

Hojas: Son los registros con grado cero (registros que no tienen hijos). En el árbol de la figura 10.1 las
hojas son los registros que tienen los datos f, c, h, m, n, e, j, l.

A la raíz se le asigna el nivel 1, a sus hijos el nivel 2, a sus nietos el nivel 3 y así sucesivamente.

Para un registro que se halle en un nivel i cualquiera, su padre se halla en el nivel i – 1, y sus hijos en el
nivel i + 1.

Altura de un Arbol: Es el máximo nivel de cualquier registro del árbol. En el ejemplo de la figura 10.1 la
altura es 5 ya que el registro que tiene la l está en el nivel 5 y ese es el máximo nivel del árbol..

Ancestros de un Registro x: Son todos los registros en la trayectoria desde la raíz hasta x. Los ancestros
de d son a, b y d.

En general, los árboles en los cuales cualquier registro puede tener cualquier cantidad de hijos, se
denominan árboles n-arios.

10.3 Representación de árboles: Una forma de hacerlo es usando listas ligadas. Recordemos que
siempre que se usen listas ligadas debemos configurar el nodo. En este caso debemos tener en cuenta el
grado del árbol. Para el ejemplo de la figura 10.1 la configuración del nodo es:

L1 L2 L3 L4 dato

Se tienen 4 campos de liga, ya que el grado del árbol es 4. Cada campo de liga apuntará hacia un hijo.
Adicionalmente se tiene un campo para el dato. En caso de que un registro tenga menos de 4 hijos habrá
algunos campos de liga que no se utilizan. Es decir su contenido será null.

Sólo el registro para el dato g usa completamente el registro de lista ligada. Para los demás registros se
presenta desperdicio de memoria. Sin embargo, esto no es muy grave, lo grave es que se desarrolla
software para cada grado de árbol, lo cual es impráctico, además de absurdo. Este problema se resuelve
representando los árboles como listas generalizadas.

Representación de árboles como listas generalizadas:


La configuración del registro será de 3 campos:

Sw Dato Liga

0: En el campo de dato hay realmente un dato.


Sw =
1: En el campo de dato hay un apuntador hacia un subárbol.

Por cada raíz del árbol se tendrá una lista simplemente ligada cuyo primer nodo representa la raíz del
árbol y los demás registros representan los hijos de esa raíz. La Lista eneralizada que representa el árbol
de la figura 10.1 se presenta en la figura 10.2.
Para construir dicha lista hay que definir la forma como entran los datos. Estos entrarán como una hilera
de abre paréntesis, átomos, comas y cierre paréntesis. Para el ejemplo de la figura 10.1 la hilera que lo
representa es:
(a(b(c, d(e)), f, g(h, i(j, k(l)), m, n)))

Fíjese que la coma se utiliza sólo como separador entre hermanos. Los datos que tienen hijos van
seguidos inmediatamente de un abre paréntesis. Con base en esta hilera se elabora algoritmo que
construya la representación mostrada en la figura 10.2.

R  0 a  1  0 f  1 ┤
.. .. .. .. .. ..
0 g  0 h  1  0 m  0 n ┤

0 i  0 j  1 ┤

0 k  0 l ┤

0 b  0 c  1 ┤

0 d  0 e ┤

Figura 10.2

10.4 Determinación del número de hojas de un árbol


En la elaboración de esta algoritmo consideramos que se tiene definida una clase nodoLg, en la cual se
define un nodo con tres campos: sw, dato y liga. Dado lo anterior dispondremos de los métodos para
acceder y modificar los atributos de dicha clase. Adicionalmente también se considera que se tiene
definida una clase arbolLg, la cual tiene como atributo privado un objeto de la clase nodoLg, el cual es el
primero de la lista que representa el árbol. Un algoritmo con el cual se determine el número de hojas de un
árbol representado como lista generalizada deberá contar los nodos cuyo campo de sw sea 0,
exceptuando las raíces, es decir el primer nodo de cada sublista. Un algoritmo que efectúa dicha tarea se
presenta a continuación. Recuerde que este algoritmo corresponde a un método que se debió haber
definido en la clase arbolLg.

1. entero hojasLg(nodoLg r)
2. if (r == null) then
3. return 0
4. end(if)
5. if (r.retornaLiga() == null) then
6. return 1
7. end(if)
8. hojas = 0
9. p = r.retornaLiga()
10. while (p != null) do
11. if (p.retornaSw() == 0) then
12. hojas = hojas + 1
13. else
14. hojas = hojas + hojasLg((nodoLg)p.retornaDato())
15. end(if)
16. p = p.retornaLiga()
17. end(while)
18. return hojas
19. Fin(hojasLg)
Con las instrucciones 2 a 4 se controla que el árbol esté vacío. De estarlo retorna 0, puesto que un árbol
vacío no tiene hojas.

Con las instrucciones 5 a 7 se controla que el árbol esté conformado sólo con la raíz. De ser así ese nodo
es simultáneamente la raíz y una hoja. Por tanto el árbol sólo tendrá una hoja, valor que se retorna en la
instrucción 6.

Al ejecutar la instrucción 8 significa que el árbol no es vacío y tiene más de un registro. Con la instrucción
8 se inicializa el contador de hojas en 0, y con la instrucción nos posicionamos en el nodo siguiente a l, es
decir, en el segundo nodo de la lista ligada, con el fin de no procesar el primer nodo, ya que este es una
raíz. En el ciclo de las instrucciones 10 a17 se recorre la lista evaluando el campo de sw de cada nodo. Si
el sw es 0 significa que ese nodo es una hoja, por consiguiente se incrementa el contador de hojas en 1
(instrucción 12), de lo contrario significa que de ese nodo depende una sublista, por tanto hay que contar
las hojas de dicha sublista. Este conteo se efectúa con la llamada recursiva que se realiza en la instrucción
14, y su resultado se acumula en el contador de hojas. Cualquiera que hubiera sido la situación se avanza
sobre la lista en la instrucción 16. Al terminar el ciclo se retorna el número de hojas, el cual se halla en la
variable que llamamos hojas.

10.5 Determinación del grado de un árbol


En este algoritmo tenemos las mismas consideraciones que en el anterior. Se tiene una clase nodoLg y la
clase arbolLg. Es te algoritmo pertenece al método para determinar el grado de un árbol representado
como lista generalizada.

1. entero gradoLg(nodoLg r)
2. if (r == null or r.retornaLiga() == null) then
3. return 0
4. end(if)
5. grado = 0
6. cuenta = 0
7. p = r.retornaLiga()
8. while (p != null) do
9. cuenta = cuenta + 1
10. if (p.retornaSw() == 1) then
11. g = gradoLg((nodoLg)p.retornaDato()
12. if (g > grado) then
13. grado = g
14. end(if)
15. end(if)
16. p = p.retornaLiga()
17. end(while)
18. if (cuenta > grado) then
19. grado = cuenta
20. end(if)
21. return grado
22. Fin(gradoLg)

Si el árbol está vacío o sólo tiene un registro (la raíz) el grado del árbol es 0. Estas situaciones se
controlan con las instrucciones 2 a 4.

Recordemos que el grado de un árbol es el máximo grado de cualquier nodo del árbol. Fíjese también que
para determinar el grado de un registro basta contar los nodos correspondientes a la lista que representa
el árbol, exceptuando el primero que es la raíz. Fíjese además que con base en la figura 10.2 a cada árbol
le corresponde una sublista.

Teniendo en cuenta lo anterior, lo que se debe hacer es contar el número de nodos de cada sublista
(incluyendo la lista principal, la del nodo cuya raíz es a) y determinar el mayor de todos estos contadores.

La variable en la cual guardamos el mayor de estos contadores la llamamos grado. La variable en la cual
se cuentan los nodos de la lista principal la llamamos cuenta y la variable en la cual se cuentan los nodos
de una sublista la llamamos g.
Inicialmente grado y cuenta valen 0. Con el ciclo de la instrucciones 8 a 17 se recorre la lista generalizada.
Con la instrucción 9 se cuentan los datos de la lista principal, luego se evalúa el campo de sw del nodo
actual: si es uno significa que de ese nodo depende un subárbol, por tanto se cuentan los nodos de la
sublista con la llamada recursiva de la instrucción 11 y se compara su resultado con el contenido de la
variable grado. Si g es mayor que grado se actualiza el contenido de grado. Con la instrucción 16 se
avanza sobre la lista principal. Al terminar el ciclo se compara el número de nodos de la lista principal con
el mayor de los de las sublistas (variable grado) y se actualiza el contenido de la variable grado si es
necesario. Con la instrucción 21 se retorna el grado del árbol.

EJERCICIOS PROPUESTOS

1. Elabore algoritmo que construya la representación como lista generalizada de un árbol con base en una
hilera de abre paréntesis, átomos, comas y cierre paréntesis.

2. Elabore algoritmo que determine y retorne el grado de un dato entrado como parámetro.

3. Elabore un algoritmo para determinar y retornar el padre de un registro en un árbol representado como
lista generalizada.

4. Elabore algoritmo que determine y retorne los ancestros de un dato entrado como parámetro en un árbol
representado como lista generalizada.

5. Elabore un algoritmo que determine y retorne la altura de un árbol representado como lista generalizada

6. Elabore un algoritmo que escriba la hilera de abre paréntesis, átomos, comas y cierre paréntesis de un
árbol representado como lista generalizada.

7. Elabore un algoritmo que imprima, por niveles, el recorrido sobre un árbol representado como lista
generalizada.
MÓDULO 11
ARBOLES BINARIOS

Contenidos
11.1 Definición
11.2 Propiedades
11.3 Representación
11.4 Recorridos
11.5 Ordenamiento de datos usando árbol binario (Heap Sort)
11.6 Representación de un árbol n-ario como binario

Objetivos
Conocer los árboles binarios, sus propiedades, su representación y la forma de manipularlos

Preguntas básicas
1. ¿Qué es un árbol binario?
2. ¿Cuáles son las propiedades de un árbol binario?
3. ¿Cuál es el máximo grado de un árbol binario?
4. ¿Cuál es el máximo número de registros en un nivel i cualquiera en un árbol binario?
5. ¿Cuál es el máximo número de registros en un árbol binario de altura k?
6. ¿Cómo se representa un árbol binario en un vector?
7. ¿Qué es un árbol binario lleno?
8. ¿Qué es un árbol binario completo?
9. ¿Qué propiedades se tienen al representar un árbol binario en un vector?
10. ¿Cuáles son los recorridos sobre un árbol binario?
11. ¿Qué es un Heap Sort?

Introducción
En el módulo anterior se presentó el concepto de árbol, la terminología usada en el manejo de esta
estructura, su representación y manipulación. Trataremos en este módulo un caso particular de árboles
que tienen una gran utilización en la representación y manejo de muchos objetos: los árboles binarios.

11.1 Definición: Un árbol binario es un conjunto de n registros (n >= 0), el cual, puede ser vacío o constar
de una raíz y los demás registros particionados en dos conjuntos disjuntos, cada uno de los cuales es un
árbol binario (definición recursiva), que se conocen como sub-árbol izquierdo y sub-árbol derecho.

El árbol binario es una estructura recursiva por definición. Es importante notar que el árbol binario vacío
está definido. Recuerde que en la definición de árboles en general el árbol vacío no estaba definido. Un
ejemplo de un árbol binario se presenta en la figura 11.1.

Raiz NIVEL

a 1

b i 2
2 3

c d j k 3
4 5 6 7

e f g h l m n o 4

8 9 10 11 12 13 14 15

Figura 11.1
Toda la terminología definida en árboles es aplicable a árboles binarios. Es obvio que el máximo grado de
todo árbol binario es 2.

11.2 Propiedades del árbol binario:

1. El máximo número de registros en un nivel i cualquiera es 2i – 1

2. Para un árbol binario de altura K el máximo número de registros es 2k – 1


Arbol LLeno: Es un árbol binario de altura K que tiene 2k – 1 registros.

3. Sea n0 = Número de hojas del árbol (registros con grado 0).


n2 = Número de registros con grado 2.

Siempre se cumplirá: n0 = n2 + 1 excepto para el árbol vacío.

En el ejemplo de la figura 11.1

n2 = 7 y n0 = 8

11.3 Representación de árboles binarios:

1. En un vector: El árbol de la figura 11.1 representado como vector se muestra en la figura 11.2

1
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
V a b i c d J k e f g h l m n o
Nivel | Nivel | Nivel | Nivel |
1 2 3 4
Figura 11.2

Al nivel 1 le corresponde la posición 1 del vector; al nivel 2 le corresponden las siguientes dos posiciones
del vector ya que el máximo número de registros en el nivel 2 es dos; al nivel tres le corresponden las
siguientes 4 posiciones del vector, ya que el máximo número de registros del nivel 3 es cuatro, y así
sucesivamente. Los registros en cada nivel se llenan de izquierda a derecha. A cada registro le
corresponderá una posición fija del vector.

Si se tiene un árbol como el de la figura 11.3a su representación como vector se muestra en la figura
11.3b

a 1 2 3 ....... 7 ....... 15

a b ....... c ....... d
b
Figura 11.3b

Sólo se utilizan las posiciones 1, 3, 7 y 15.


c las demás posiciones del vector no se utilizan,
por tanto se consideran desperdicio de memoria.

Figura 11.3a

Propiedades de la Representación de un Árbol Binario en un Vector:

Para un registro en la posición i del vector:


1. Su padre se halla en la posición i/2 del vector (i > 1 división entera)

2. Su hijo izquierdo se halla en la posición 2*i. (2*i <= n, n: número de registros del árbol).

3. Su hijo derecho se halla en la posición 2*i + 1. (2*i + 1 <= n)

Árbol Completo: Es un árbol binario tal que al representarlo en un vector, utiliza efectivamente todas las
posiciones del vector.

Todo Árbol Binario Lleno es Completo

Lo contrario NO siempre se cumple, Por ejemplo: El siguiente es un árbol binario completo, aunque no sea
lleno:

Raiz NIVEL

a 1

b i 2

2 3

c d j k 3

4 5 6 7

e 4

8
Figura 11.4

Al representarlo en un vector, se requieren 8 posiciones, y utilizamos efectivamente las ocho posiciones


del vector.

1 2 3 4 5 6 7 8
V a b i c d j k e
Nivel | Nivel | Nivel | Nivel
1 2 3 4
Figura 11.5

2. Como Listas Ligadas: Siempre que se desee representar un objeto como lista ligada, lo primero que
debemos hacer es definir la configuración del nodo. Utilizaremos objetos de la clase nodo doble

Liga Dato Liga


Izquierda Derecha

El campo de liga izquierda apunta hacia el nodo que representa el hijo izquierdo de x.
El campo de liga derecha apunta hacia el nodo que representa el hijo derecho de x.
Si tenemos el siguiente árbol binario:

Raiz
a

b c

d e f g

Figura 11.6

Su representación como lista ligada será:

RAIZ

3 a 7
8
5 b 1 4 c 6
3 7
┤ d ┤ ┤ e ┤ ┤ f ┤ ┤ g ┤
5 1 4 6
Figura 11.7

Con esta representación se usan los registros que efectivamente se necesitan.

Con esta representación podremos conocer fácil y eficientemente los hijos de cada registro, ya que los
campos de liga izquierda y liga derecha apuntan hacia los registros que los representan. Si se desea
conocer el padre de un registro habrá que ejecutar un algoritmo para tal fin, el cual tiene orden de
magnitud lineal. Si la aplicación que se desea implementar manejando árboles, requiere permanentemente
conocer el padre de un registro, la permanente ejecución del algoritmo para determinar el padre genera
ineficiencias, por consiguiente sería mejor agregar un cuarto campo llamado LIGA AL PADRE, que permita
conocer eficientemente el padre de cada registro. La configuración del registro sería:

Liga Liga Liga


Izquierda Dato Derecha Al Padre

Esta configuración se utiliza sólo cuando se tenga ese requerimiento. Por lo general este requerimiento no
aparece, así que en nuestra representación no tendremos en cuenta el campo de liga al padre, y cuando
haya que determinar el padre de un registro simplemente se ejecuta el algoritmo definido para este
propósito.

11.4 Recorridos Sobre Arboles Binarios:


Una de las aplicaciones de árboles binarios es representar expresiones aritméticas. Si queremos
representar la expresión a + b en un árbol binario tendríamos:

I D

a b

Figura 11.8

Donde R representa raiz, I hijo izquierdo y D hijo derecho.


Es bueno observar que los operandos están en las hojas. Siempre que se desee representar una
expresión en un árbol binario los operadores serán raíces y los operandos hojas.

Si deseamos recorrer el árbol de la figura 11.8 lo podemos hacer de varias maneras. Veamos:

Recorrido Resultado obtenido

1. IRD a+b
2. IDR ab+
3. RID +ab
4. RDI +ba
5. DRI b+a
6. DIR ba+

En las tres primeras formas obtenemos la expresión en INfijo, POSfijo y PREfijo respectivamente. Por
analogía a la forma como se obtiene la expresión aritmética a estas primeras tres formas de recorrido se
les han asignado los nombres de inorden, posorden y preorden.

Las formas 4, 5 y 6 no están definidas para recorrer árboles binarios. En general, digamos que las
llamaremos, a forma de chiste, en DESORDEN.

Nótese que los prefijos IN, POS y PRE hacen referencia a la ubicación de la raíz en cada recorrido.

Resumiendo tenemos:

1. IRD (Izquierdo, Raíz, Derecho) ----------> INORDEN

2. IDR (Izquierdo, Derecho, Raíz) ----------> POSORDEN

3. RID (Raíz, Izquierdo, Derecho) ----------> PREORDEN

Es bueno hacer notar que en todos los recorridos definidos siempre se visita el hijo izquierdo antes que el
derecho. A continuación damos un ejemplo más amplio de los recorridos sobre un árbol binario.

Consideremos el siguiente árbol binario:

RAIZ

b l

c d k m

e f j n o

g h p

i
Figura 11.9

Recorrido inorden: I R D  gehicbfdajklnmop


Recorrido posorden : I D R  gihecfdbjknpomla
Recorrido preorden : R I D  abceghidflkjmnop
Algoritmos para ejecutar los recorridos:
Para un árbol representado como lista ligada, el algoritmo recursivo para recorrerlo en INORDEN es:

1. void inorden(nodoDoble r)
2. if (r != null) then
3. inorden(r.retornaLi())
4. write(r.retornaDato())
5. inorden(r.retornaLd())
6. end(if)
7. Fin(inorden)

El algoritmo sólo imprime el recorrido INORDEN sobre un árbol binario. La filosofía del algoritmo es: Se
puede hacer recorrido cuando el árbol binario no sea vacío (if r != null); antes de imprimir el dato de la raíz
debo recorrer, también en INORDEN, la rama izquierda de la raíz (primera llamada recursiva); luego de
imprimir el dato de la raíz debo recorrer, también en INORDEN, la rama derecha de la raíz (segunda
llamada recursiva).

Si deseamos efectuar alguna operación diferente a imprimir el recorrido del árbol basta con reemplazar la
instrucción write por la instrucción o grupo de instrucciones correspondientes a lo que deseemos efectuar
cuando estemos ubicados en ese registro.

A continuación presentamos los algoritmos correspondientes a los recorridos POSORDEN y PREORDEN.

1. void posorden(nodoDoble r)
2. if (r != null) then
3. posorden(r.retornaLi())
4. posorden(r.retornaLd())
5. write(r.retornaDato())
6. end(if)
7. Fin(posorden)

1. void preorden(nodoDoble r)
2. if (r != null) then
3. write(r.retornaDato())
4. preorden(r.retornaLi())
5. preorden(r.retornaLd())
6. end(if)
7. Fin(preorden)

11.5 Ordenamiento de datos usando un árbol binario (Heap-sort).


Si consideramos todo vector como un árbol binario completo con n elementos, se pueden ordenar
ascendentemente los datos en él, usando una técnica muy ingeniosa conocida como heap-sort. Veamos
cómo funciona este método de ordenamiento.

Un heap es un árbol binario completo, en el cual se cumple que para todo registro x perteneciente al árbol
el dato de x es mayor que los datos de sus hijos. Si consideramos el árbol de la figura 11.10a vemos que
no cumple la característica de heap. Sin embargo, lo podemos convertir a un heap. Dicho árbol convertido
en heap se presenta en la figura 11.10b.

Tenga presente que el árbol de la figura 11.10a representado como vector es:

1 2 3 4 5 6 7 8
V 5 9 4 1 6 2 8 3

Y el de la figura 11.10b es:


1 2 3 4 5 6 7 8
V 9 6 8 3 5 2 4 1
Raiz

5
1

9 4
2 3

1 6 2 8
4 5 6 7

3
8 Figura 11.10a

Raiz

9
1

6 8
2 3

3 5 2 4
4 5 6 7

1
8 Figura 11.10b

El algoritmo que efectúa la tarea de convertir un árbol binario en un heap es:

1. void ajuste(int i, int n)


2. int d, j
3. d = V[i]
4. j=2*i
5. while (j <= n) do
6. if (j < n and V[j] < V[j+1]) then
7. j=j+1
8. end(if)
9. if (d >= V[j]) then
10. V[j / 2] = d
11. return
12. end(if)
13. V[j / 2] = V[j]
14. j=2*j
15. end(while)
16. V[j / 2] = d
17. Fin(ajuste)

Fijémonos cómo funciona este algoritmo. El parámetro de entrada i indica la posición del árbol a partir de
la cual se debe crear el heap. Es decir, se va a convertir en heap el árbol cuya raíz se halla en la posición i
del vector. Para ello se debe determinar el mayor entre el dato de la posición i y sus hijos, los cuales se
hallan en las posiciones 2*i y 2*i+1. Para efectuar esta tarea se usa la variable d, en la cual se guarda el
dato que se halla en la posición i del vector que representa el árbol binario (instrucción 3), y la variable j,
en la cual se guarda la posición en la que se almacena el hijo izquierdo del dato que se halla en la posición
i mediante la instrucción 4. El ciclo de las instrucciones 5 a 15 funciona así: En instrucciones 6 a 8 se
determina el mayor entre los dos hijos del dato que se halla en la posición i del vector. Luego, con la
instrucción 9 se controla si el dato de la raíz es mayor que los datos de sus dos hijos (se compara d con el
mayor dato de los hijos), de ser así, simplemente se guarda el dato d en la posición j/2 y se retorna. De ser
falsa la condición de la instrucción 9, se almacena en la posición j/2 el mayor entre los datos del padre que
se halla en la posición j/2 y sus dos hijos. Este proceso se repite hasta que la j sea mayor que la n.
Cuando se termina el ciclo simplemente se almacena el dato d en la posición j/2. Recomendamos al
estudiante hacer un seguimiento concienzudo a este algoritmo con el fin de que se apropie de él y
comprenda mejor su funcionamiento. Recuerde que este algoritmo sólo convierte el árbol binario en un
heap, no lo ordena. El proceso de ordenamiento se presenta en el algoritmo a continuación.

1. void ordenaAscendenteHeapSort()
2. int i
3. for (i = n / 2; i > 0; i--) do
4. ajuste(i, n)
5. end(for)
6. for (i = n – 1; i > 0; i--) do
7. intercambia(i+1, 1)
8. ajuste(1, i)
9. end(for)
10. Fin (ordenaAscendenteHeapSort)

El algoritmo de ordenamiento, en el ciclo de las instrucciones 3 a 5 se convierte el árbol binario completo


en un heap. El ciclo de las instrucciones 6 a 9 es el que efectúa el proceso de ordenamiento. El proceso de
ordenamiento funciona así: como el árbol es un heap, el mayor dato se halla en la posición 1 del vector; lo
que se hace es intercambiar el dato de la posición 1 con el dato que se halla en la última posición de los
datos que faltan por ordenar (instrucción 7), y recrear nuevamente el heap con la instrucción 8; esto
garantiza que el mayor dato ya está ubicado en el sitio que le corresponde. Lo que falta es ordenar los
primeros n – 1 datos. Eso es lo que se hace en las siguientes iteraciones del ciclo. Al ejecutar la primer vez
el intercambio de datos de la instrucción 7 el árbol binario queda:

Raiz

1
1

6 8
2 3

3 5 2 4
4 5 6 7

9 Figura 11.10c
8
Y al recrear el heap el árbol queda

Raiz

8
1

6 4
2 3

3 5 2 1
4 5 6 7

9 Figura 11.10d
Fíjese que el 9 quedó en la posición que le corresponde y en la raíz del árbol quedó el mayor dato de los
que faltan por ordenar y el árbol es un heap. Cada vez que se ejecutan las instrucciones de este ciclo se
intercambia el primer dato del vector con el último de los que faltan por ordenar y se recrea el heap. Al
terminar el ciclo los datos estarán ordenados ascendentemente. Recomendamos nuevamente hacer una
buena prueba de escritorio a este algoritmo para afianzar el conocimiento de su funcionamiento.

11.6 Representación de un árbol n-ario como binario.

Raiz

b c d

e g h i j k

f l m

Figura 11.11

Cualquier árbol, sin importar su grado puede representarse en árbol binario.

Veamos la representación binaria equivalente al árbol de la figura 11.11 en la figura 11.12

Raiz

e c

f g d

l j

m k

Figura 11.12

La construcción del árbol binario se hace de la siguiente manera: para un registro x que tenga n hijos, su
primer hijo se representa como hijo izquierdo y los demás hijos de x se insertan como hijos derechos del
primero. Es decir, los registros que estén en una rama derecha son hermanos del primer registro de esa
rama.

EJERCICIOS PROPUESTOS

1. Escriba un algoritmo para determinar el número de hojas de un árbol binario representado como lista
ligada.

2. Escriba un algoritmo para determinar el grado de un árbol binario representado como lista ligada.

3. Escriba un algoritmo para determinar la altura de un árbol binario representado como lista ligada.

4. Escriba un algoritmo para determinar el padre de un registro en un árbol binario representado como lista
ligada.

5. Escriba un algoritmo para determinar los ancestros de un registro en un árbol binario representado
como lista ligada.

6. Elabore un algoritmo que imprima por niveles el recorrido sobre un árbol binario representado como lista
ligada.

7. Escriba un algoritmo que construya la representación como árbol binario en lista ligada de un árbol n-
ario, representado como lista generalizada.

8. Elabore un algoritmo para construir un árbol binario al cual se le dieron los recorridos preorden e
inorden.

9. Elabore algoritmo para construir la representación como lista ligada de un árbol binario completo dada
su representación en vector.

10. Se dice que un árbol binario es zurdo si: está vacío, o es una hoja, o sus hijos izquierdo y derecho son
ambos zurdos, y más de la mitad de sus descendientes están en el hijo izquierdo. Elabore un algoritmo
que determine si un árbol binario representado como lista ligada es zurdo o no. Su algoritmo debe retornar
verdadero si es zurdo falso de lo contrario.
MÓDULO 12
ARBOLES BINARIOS ENHEBRADOS

Contenido
12.1 Representación de árboles como listas ligadas enhebradas.
12.2 Recorrido inorden de un árbol binario enhebrado en inorden.

Objetivos
Conocer la representación de árboles binarios como listas ligadas enhebradas y sus ventajas.

Preguntas básicas
1. ¿Qué es una hebra?
2. ¿Para qué se utilizan los campos de bit en los nodos de árboles enhebrados?
3. ¿En qué consiste un árbol binario enhebrado en inorden?
4. ¿En qué consiste un árbol binario enhebrado en preorden?
5. ¿En qué consiste un árbol binario enhebrado en posorden?
6. ¿Cuál es el beneficio que se obtiene al representar un árbol binario enhebrado?

Introducción
En el módulo anterior vimos cómo se representa un árbol binario como lista ligada y sus recorridos. Los
algoritmos resultantes para recorrer árboles binarios bajo esta representación tienen orden de magnitud
exponencial. Veremos en este módulo una mejora a esta representación, con la cual se obtienen
algoritmos con orden de magnitud lineal.

12.1 Representación de árboles como listas ligadas enhebradas.


Cuando representamos un árbol binario como lista ligada, todos los registros que son hojas tendrán dos
campos de liga cuyo valor es cero (null), además aquellos registros que sólo tienen un hijo su otro campo
de liga es cero (null).

Consideremos el siguiente árbol binario en su representación como lista ligada:

Raiz

2 8
a
0 0
10
3 7
b ┤ c 90
0 0
8
20
0
5 6
40 j ┤ h , 5 d 15
0 0
3 9
70
0 0
┤ k ┤ ┤ l ┤ ┤ g ┤ ┤ e ┤ ┤ f ┤
4 1
50 60 5
0 5
Figura 12.1

Dicho árbol tiene 11 registros, o sea 22 campos de liga, de los cuales hay 12 campos de liga que son null.
En general podemos afirmar que en un árbol binario, representado como lista ligada, que tenga n
registros, habrá n+1 campos de liga que son null.

Como los campos cuyo valor es cero se consideran desperdicio de memoria se ha buscado la forma de
utilizarlos de tal forma que redunden favorablemente ya sea en la elaboración de los algoritmos o en la
eficiencia de ellos.

Consideremos el árbol de la figura 12.1 y escribamos su recorrido en INORDEN:


kjbihgacedf

el cual, si lo escribimos con los registros, en vez de con los datos que hay en cada registro tendremos:
40, 30, 20, 50, 70, 60, 10, 80, 5, 90, 15

es decir, ese es el orden en que pasamos por los registros.

La utilización que daremos a los campos de liga es: Si hay un campo de liga izquierda que valga null lo
pondremos a apuntar hacia el registro anterior en el recorrido INORDEN; si hay un campo de liga derecha
que valga 0 lo pondremos a apuntar hacia el registro siguiente en recorrido INORDEN. El árbol quedará
como en la figura 12.2

Note que el campo de liga izquierda del primer registro en recorrido INORDEN (el registro 40) sigue
valiendo null; igual cosa sucede con el campo de liga derecha del último registro en recorrido INORDEN
(el registro 15).

Para obviar este problema lo que hacemos es añadir un registro cabeza al árbol. Entonces el campo de
liga izquierda del primer registro apuntará hacia el registro cabeza y el campo de liga derecha del último
registro también apuntará hacia el registro cabeza.

Raiz

20 a 80
1
0
1
30 b 70 c 90
0
2
80
0
40 j 20 50 h 60 , 5 d 15
3 7 9
0 0 0
1 8
┤ k 30 20 i 70 70 g e 90 90 f ┤
0 0
40 50 60 5 15
Figura 12.2

Nuestro árbol quedará como en la figura 12.3.

En el registro cabeza el campo de liga izquierda apuntará hacia la raíz del árbol, mientras que el campo de
liga derecha apuntará hacia sí mismo.

Cuando tenemos el árbol representado de esta forma se nos presenta un nuevo problema. Si
consideramos un campo de liga izquierda, por ejemplo, debemos reconocer cuando apuntas hacia su hijo
izquierdo o cuando apunta hacia el registro anterior en recorrido INORDEN. Es decir, el campo de liga
izquierda tiene dos significados; con el campo de liga derecha sucede exactamente lo mismo.

A los campos de liga que apuntan hacia el registro anterior o hacia el siguiente se les denominan hebras.

Para distinguir cuando un campo de liga es una hebra o no, añadimos dos campos (que pueden ser bits)
más al registro: Bit izquierdo y Bit derecho. El registro quedará entonces así:

Liga Liga
Izquierda Bi Dato Bd Derecha

0: Li es una hebra (apunta hacia el registro anterior)


Bi =
1: Li apunta hacia el hijo izquierdo.

0: Ld es una hebra (apunta hacia el registro siguiente)


Bd =
1: Ld apunta hacia el hijo derecho.
Nodo cabeza

10 25
25

Raiz
20 a 80
1
0
3 9
b 70 10 c
0 0
20 80
2
40 j 50 h 60 , 5 d 15
0
7 90
30
0
2 9 2
k 30 20 i 70 70 g 10 80 e 90 f
5 0 5
40 50 60 5 15
Figura 12.3

Para el registro cabeza: Bd siempre será 1


Ld siempre apunta hacia sí mismo.

Y además,

1: Li apunta hacia la raíz del árbol


Bi =
0: árbol vacío y Li apunta hacia sí mismo.

13 0 1 13
13
Registro Cabeza de un árbol binario enhebrado vacío.

La representación de nuestro árbol con los registros completos, incluyendo los bits de diferenciación de
ligas se presenta en la figura 12.4.

Nodo cabeza

10 1 1 25
25

Raiz
20 1 a 1 80
10
30 1 b 1 70 10 0 c 1 90
20 80
40 1 j 0 20 50 1 h 1 60 , 5 1 d 1 15
30 70 90
25 0 k 0 30 20 0 i 0 70 70 0 g 0 10 80 0 e 0 90 90 0 f 0 25
40 50 60 5 15
Figura 12.4

12.2 Recorrido inorden de un árbol binario enhebrado en inorden.


Con la representación de un árbol binario como lista ligada enhebrada se podrá saber fácilmente cual es el
siguiente registro en recorrido inorden, para cualquier registro x perteneciente al árbol.

Si x.retornaBd() == 0  siguiente(x) = x.retornaLd()

Si x.retornaBd() == 1  Nos pasamos al hijo derecho de x y a partir de ese


nodo avanzamos con x.retornaLi() hasta encontrar
un nodo con Bit izquierdo igual a cero. Ese nodo es
es el siguiente(x).

Elaboremos una función que ejecute dicha tarea:

1. nodoDoble siguiente(nodoDoble x)
2. siguiente = x.retornaLd()
3. if (x.retornaBd() == 1)
4. while (siguiente.retornaBi() == 1)
5. siguiente = siguiente.retornaLi()
6. end(while)
7. end(if)
8. return siguiente
9. Fin(siguiente)

Teniendo dicha función podremos recorrer en inorden el árbol binario sin necesidad de utilizar recursión.

Dicho algoritmo es el siguiente:

1. void inorden(nodoDoble r)
2. p = siguiente(r)
3. while (p != r) do
4. write(p.retornaDato())
5. p = siguiente(p)
6. end(while)
7. Fin(inorden)

EJERCICIOS PROPUESTOS

1. Elabore algoritmo para determinar y retornar el número de hojas de un árbol binario representado como
lista ligada enhebrada.

2. Elabore algoritmo para determinar y retornar el grado de un árbol binario representado como lista ligada
enhebrada.

3. Elabore algoritmo para determinar y retornar la altura de un árbol binario representado como lista ligada
enhebrada.

4. Elabore un algoritmo para determinar el padre de un registro en un árbol binario representado como lista
ligada enhebrado en inorden.

5. Elabore algoritmo para enhebrar en inorden un árbol representado como lista ligada no enhebrado.
6. Elabore algoritmos para recorrer en preorden un árbol binario representado como lista ligada enhebrada
en inorden.

7. Elabore algoritmos para recorrer en posorden un árbol binario representado como lista ligada enhebrada
en inorden.
MÓDULO 13
ARBOLES BINARIOS DE BÚSQUEDA

Contenido
13.1 Definición
13.2 Arboles AVL
13.3 Rebalanceo de un árbol AVL
13.4 Construcción de árbol AVL

Objetivos
Conocer lo que son árboles binarios de búsqueda y árboles AVL, sus ventajas y sus aplicaciones.

Preguntas básicas
1. ¿Qué es un árbol binario de búsqueda?
2. ¿Cómo salen los datos de un árbol binario de búsqueda al recorrerlo en inorden?
3. ¿Qué es un árbol AVL?
4. ¿Qué es factor de balance?
5. ¿En qué consiste el rebalanceo en la construcción de un árbol AVL?
6. ¿Qué es una rotación a la derecha?
7. ¿Qué es una rotación a la izquierda?
8. ¿Qué es doble rotación a la derecha?
9. ¿Qué es doble rotación a la izquierda?

Introducción
En los módulos anteriores tratamos lo correspondiente a árboles binarios y algunas de sus aplicaciones.
Los árboles binarios también se pueden utilizar con el fin de almacenar datos y obtener algoritmos de
búsqueda eficientes. Para ello los árboles binarios deben tener ciertas características. Veremos en este
módulo árboles binarios que satisfagan estas necesidades.

13.1 Definición Son árboles binarios en los cuales se cumple que para cualquier registro x perteneciente
al árbol, todos los datos de los registros a la izquierda de x son menores que el dato de x y todos los datos
de los registros a la derecha de x son mayores que el dato de x.

Consideremos el siguiente conjunto de datos:

mevcxlpais

Construyamos un árbol binario de búsqueda con ellos.

Comencemos leyendo el primer dato, es decir, m. Como el árbol está vacío simplemente conseguimos un
registro, le asignamos el dato m y decimos que este registro es la raíz:

Raiz

Luego leemos el siguiente dato, es decir e. Como ya empezamos a construir el árbol hay que buscar
dónde insertarlo. Para ello, comparamos el dato de la raíz con el dato leído. Se el dato leído es mayor que
el dato de la raíz avanzamos por la rama derecha de la raíz, de lo contrario avanzamos por la rama
izquierda de la raíz. En nuestro caso avanzamos por el lado izquierdo de la raíz, como no tiene hijo
izquierdo simplemente conseguimos registro y lo insertamos como hijo izquierdo de la raíz. Nuestro árbol
queda:

Raiz

e
Continuamos leyendo el siguiente dato, es decir, v.

Debemos buscar dónde insertarlo. Empezamos comparando el dato leído con el dato de la raíz. Si el dato
leído es mayor que el dato de la raíz avanzamos por la rama derecha de la raíz, en caso contrario por el
lado izquierdo. En nuestro caso avanzaremos por el lado derecho. Como no existe hijo derecho
simplemente conseguimos un registro y lo insertamos como hijo derecho de la raíz. Nuestro árbol quedará:

Raiz

e v

Leemos el siguiente dato que es la c. Habrá que buscar dónde insertarlo.

Comenzamos comparando el dato leído con el dato de la raíz. Como el dato leído es menor que el dato de
la raíz avanzamos por la rama izquierda de la raíz. Quedamos en el registro que tiene la e, comparamos el
dato leído con este dato, como el dato leído es menor avanzamos por la rama izquierda, como dicho
registro no tiene hijo izquierdo simplemente conseguimos un registro y lo insertamos como hijo izquierdo
del registro que tiene la e. Nuestro árbol quedará:

Raiz

e v

Continuando esta metodología, de añadir un registro al árbol binario, por cada dato que se lea, nuestro
árbol quedará así:

Raiz

e v

c l p x

a i s

Todos los datos de la izquierda de cualquier registro son menores que el dato de ese registro, y todos los
de la derecha son mayores. Además si hacemos un recorrido INORDEN sobre dicho árbol obtenemos los
datos ordenados ascendentemente: aceilmpsvx

13.2 Arboles AVL.


Son árboles binarios balanceados por altura. Su nombre se debe a que sus inventores son los soviéticos
Adelsson-Velski y Landis.

Factor de Balance:
Es el concepto clave para definir los árboles AVL. El factor de balance para cualquier registro x se define
como la diferencia entre la altura del hijo izquierdo de x y la altura del hijo derecho de x. Es decir:
Fb(x) = Altura(Li(x)) – Altura(Ld(x)).

Un árbol binario es AVL si:

│Fb (x)│ < 2 para todo x perteneciente al árbol

o sea el conjunto de valores permitidos en el factor de balance, para que un árbol binario sea AVL es:

FB = {-1, 0, +1}

Consideremos el árbol de la figura 13.1, al cual le hemos colocado, al lado de cada registro, el factor de
balance correspondiente.

Raiz
–1
a

–1 –3
b g

0 0 +1
c d h

0 0 +1 0
e f i j

0
k
Figura 13.1

Dicho árbol no es AVL ya que el registro que contiene el dato g tiene factor de balance -3. Nuestro objetivo
será construir árboles AVL.

13.3 Rebalanceo de un árbol AVL


Consiste en reacomodar los registros de un árbol binario de tal forma que los factores de balance de todos
los registros sean -1, 0, ó +1 y que el recorrido INORDEN sea el mismo que antes del reacomodo.

Operaciones de Rebalanceo:
1. Una rotación a la derecha.
2. Una rotación a la izquierda.
3. Doble rotación a la derecha.
4. Doble rotación a la izquierda.

Para explicar lo referente a las rotaciones asumamos la siguiente convención:

Sea P la dirección del registro con factor de balance no permitido.(+2 ó -2).

Sea Q la dirección del hijo izquierdo o del hijo derecho de P, dependiendo de si Fb(P)=+2 ó Fb(P)= - 2, es
decir, si factor de balance de P es +2 entonces Q es el hijo izquierdo de P y si factor de balance de P es -2
entonces Q es el hijo derecho de P.

13.3.1. Una rotación a la derecha:

Fb (P) = + 2
Se efectúa cuando: y
Fb (Q) = + 1

Consiste en girar, en sentido de las manecillas del reloj, el registro P alrededor del registro Q.
Consecuencias:

 P pasará a ser el nuevo hijo derecho de Q.


 El anterior hijo derecho de Q será el nuevo hijo izquierdo de P.
 Q será la nueva raíz del árbol balanceado.
 Los nuevos factores de balance de P y Q serán cero (0).
 La altura del árbol balanceado disminuye en uno (1).

En las figuras 13.2a y 13.2b mostramos un árbol que se acomoda a esta primera situación: antes y
después del balanceo.

+2 0
P c Q b

+1 0 0
Q b a c P

0
a

Figura 13.2a Figura 13.2b

Note además que los recorridos INORDEN sobre ambos árboles es el mismo, antes y después de la
rotación.

El algoritmo que efectúa las operaciones descritas anteriormente es el siguiente:

1. void unaRotacionALaDerecha(nodoAVL p, nodoAVL q)


2. p.asignaLi(q.retornaLd())
3. q.asignaLd(p)
4. p.asignaFb(0)
5. q.asignaFb(0)
6. Fin(unaRotacionALaDerecha)

En las figuras 13.3a y 13.3b presentamos otro ejemplo del rebalanceo en el caso de una rotación a la
derecha. La figura 13.3a es antes del rebalanceo y la figura 13.3b es después del rebalanceo.

+2 0
P e Q c

+1 0 +1 0
Q c f b e P

+1 0 0 0 0
b d a d f

0
a Figura 13.3b

Figura 13.13a

Note que los recorridos INORDEN son iguales en ambos árboles.


Fíjese que el registro P pasó a ser el hijo derecho del registro Q; el registro que tiene el dato d, que era el
hijo derecho del registro Q, pasó a ser el hijo izquierdo del registro P; el registro Q es la nueva raíz del
árbol balanceado, y los factores de balance de los registros P y Q quedaron en cero.

13.3.2. Una Rotación a la Izquierda:

Fb(P) = – 2
Se efectúa cuando y
Fb(Q) = – 1

Consiste en girar, en sentido contrario de las manecillas del reloj, P alrededor de Q.

Consecuencias:

 P será el nuevo hijo izquierdo de Q.


 El nuevo hijo derecho de P será el anterior hijo izquierdo Q.
 Q será la nueva raíz del árbol balanceado.
 Los factores de balance P y Q quedarán en cero.
 La altura del árbol balanceado disminuye en uno (1).

-2 0
P a Q b

-1 0 0
Q b P a c

0 Figura 13.4b
c
Figura 13.4a

El algoritmo que ejecuta las operaciones descritas es el siguiente:

1. void unaRotacionALaIzquierda(nodoAVL p, nodoAVL q)


2. p.asignaLd(q.retornaLi())
3. q.asignaLi(p)
4. p.asignaFb(0)
5. q.asignaFb(0)
6. Fin(unaRotacionALazquierda)

Consideremos otro ejemplo:

-2 0
P b Q d

0 -1 0 -1
a d Q P b e

0 -1 0 0 0
c e a c f

0 Figura 13.5b
f

Figura 13.5a

Note que los recorridos INORDEN son iguales en ambos árboles.


Fíjese que el registro P pasó a ser el hijo izquierdo del registro Q; el registro que tiene el dato C, que era el
hijo izquierdo del registro Q, pasó a ser el hijo derecho del registro P; el registro Q es la nueva raíz del
árbol balanceado, y los factores de balance de los registros P y Q quedaron en cero.

Para definir las dobles rotaciones adoptemos una nueva convención:

Sea R el registro que representa el hijo izquierdo o el hijo derecho de Q dependiendo de si el factor de
balance de Q es +1 ó -1. Es decir, si el factor de balance del registro Q es +1, el registro R es el hijo
izquierdo del registro Q, y si el factor de balance de Q es -1, R es el hijo derecho del registro Q.

13.3.3. Doble Rotación a la Derecha:

Fb(P) = +2
Se efectúa cuando
Fb(Q) = -1

Consiste en una rotación a la izquierda de Q alrededor de R seguida de una rotación a la derecha de P


alrededor de R.

Consecuencias:

 P será el nuevo hijo derecho de R.


 Q será el nuevo hijo izquierdo de R.
 El anterior hijo derecho de R será el nuevo hijo izquierdo de P.
 El anterior hijo izquierdo de R será el nuevo hijo derecho de Q.
 La altura del árbol balanceado disminuye en uno (1).
 R será la nueva raíz del árbol balanceado.
 El factor de balance de R será cero.
 Los factores de balance de P y Q tomarán nuevos valores, los cuales dependerán del factor de
balance inicial del registro R. El factor de balance inicial del registro R puede ser cero (0), más uno
(+1) o menos uno (-1).

Consideremos el primer caso, cuando el factor de balance inicial del registro R es cero.

+2 0
P c R b

-1 0 0
Q a Q a c P

0 Figura 13.6b
R b

Figura 13.6a

En nuestro primer caso, figuras 13.6a y 13.6b, el factor de balance inicial del registro R es cero. Para este
caso los factores de balance finales de los registros P y Q serán cero.

Fb(P) = 0
Si Fb inicial de R es cero
Fb(Q) = 0

Consideremos el segundo caso, cuando el factor de balance inicial del registro R es más uno (+1)

Las figuras 13.7a y 13.7b ilustran esta situación. Como podrá observarse, el factor de balance final del
registro Q es cero (0), mientras que el factor de balance final del registro P es menos uno (-1).
Fb(P) = -1
Si Fb inicial de R es más uno (+1)
Fb(Q) = 0

+2 0
P e R d

-1 0 0 -1
Q b f Q b e P

0 +1 0 0 0
a d R a c f

0
c Figura 13.7b

Figura 13.7a

Consideremos finalmente el caso en el cual el factor de balance inicial del registro R es menos uno (-1):

Las figuras 13.8a y 13.8b ilustran esta situación. Como podrá observarse, el factor de balance final del
registro P es cero (0), mientras que el factor de balance final del registro Q es más uno (+1).

Fb(P) = 0
Si Fb inicial de R es menos uno (-1)
Fb(Q) = +1

+2 0
P e R a

-1 0 +1 0
Q b f Q b e P

0 -1 0 0 0
c a R c d f

0
d figura 13.8b

Figura 13.8a

En la siguiente página presentamos el algoritmo completo que ejecuta la doble rotación a la derecha. Es
importante hacer notar que el parámetro Q debe ser un parámetro por referencia, ya que en él retornará la
raíz del nuevo árbol balanceado. Esto con el fin de unificar los algoritmos de balanceo: en la variable Q
tendremos la raíz del árbol balanceado.

1. void dobleRotacionALaDerecha(nodoAVL p, nodoAVL q)


2. r = q.retornaLd()
3. q.asignaLd(r.retornaLi())
4. r.asignaLi(q)
5. p.asignaLi(r.retornaLd()
6. r.asignaLd(p)
7. Casos de r.retornaFb()
8. 0: p.asignaFb(0)
9. q.asignaFb(0)
10. break
11. 1: p.asignaFb(–1)
12. q.asignaFb(0)
13. break
14. –1: p.asignaFb(0)
15. q.asignaFb(1)
16. fin(casos)
17. r.asignaFb(0)
18. q=r
19. Fin(dobleRotacionALaDerecha)

13.4 Construcción e inserción de un nodo en un árbol AVL. Veamos ahora cómo construir un árbol
binario de tal forma que cumpla las propiedades AVL.

Hay que tener en cuenta que en la construcción de un árbol AVL todo registro que se inserte, se insertará
como una hoja. Los pasos a seguir son:

1. Buscar donde insertar el nuevo registro.


2. Insertar el nuevo registro.
3. Recalcular factores de balance.
4. Rebalancear el árbol si es necesario.

Consideremos el árbol de la figura 13.9 y que vamos a insertar un registro con el dato 77.

-1
35

0 -1
20 55

0 +1 0 0
10 30 45 70

0 0 0 0 0 +1 0
5 15 25 40 50 65 80

0 0 0
60 75 85

Figura 13.9

Los datos a la izquierda de cada registro son menores que el dato de ese registro y los de la derecha
mayores.

Para el primer paso, el cual consiste en buscar dónde insertar el nuevo registro utilizaremos las siguientes
variables:

P: Para recorrer el árbol buscando donde insertar el nuevo registro.


Q: Apuntará hacia el padre de P.

El primer paso consiste en comparar el dato de un registro con el dato leído. En caso de que el dato de
ese registro sea mayor que el dato leído avanzaremos por la rama izquierda del registro con el cual se hizo
la comparación, de lo contrario avanzaremos por la rama derecha. Cuando P = null, Q apuntará hacia el
registro que será el padre del registro que contiene el nuevo dato.

Inicialmente P será la raíz y Q será null.

Ahora bien, como el tercer paso es rebalancear el árbol, se requiere conocer cuál será el registro "raíz" del
sub-árbol a balancear. Llamaremos este registro pivote. Es decir, pivote será la raíz del árbol a balancear.
Como al rebalancear un árbol, éste siempre cambia de raíz, la nueva raíz, que es Q, habrá que pegarla del
registro que era padre de pivote. Esto implica que debemos conocer el padre de pivote. Llamaremos este
registro PP.

Resumiendo tenemos:

PIVOTE: Dirección del registro que posiblemente quede desbalanceado como consecuencia de la
inserción.
PP: Dirección del registro padre de pivote

Para determinar pivote debemos tener en cuenta que los únicos registros que pueden quedar
desbalanceados son aquellos cuyo factor de balance es diferente de cero (0). En algunos casos puede
suceder que pivote sea la raíz, con factor de balance cero. En concreto, el registro que será pivote es el
registro más cercano al registro donde se hará la inserción, con factor de balance diferente de cero. Es
bueno aclarar que es el registro más cercano en la trayectoria desde la raíz hasta el registro que será el
nuevo padre.

Inicialmente pivote será la raíz y pp será cero (0).

El algoritmo de construcción quedará así:

1. void insertaDatoEnAVL(d)
2. x = new nodoAVL(d) Consigue nuevo registro y lo configura
3. if (r == null) then Si es la primer vez que se llama el
4. r=x procedimiento, el registro conseguido
5. return es la raíz.
6. end(if)
7. p=r Asigna valores iniciales a las variables
8. q = null que se utilizan para buscar el sitio donde
9. pivote = r se insertará nuevo registro y determinar
10. pp= null PIVOTE y su padre.
11. while (p != null) do
12. if (p.retornaDato() == d) then Controla que no vayan a
13. return quedar datos repetidos
14. end(if)
15. If (p.retornaFB() != 0) then
16. pivote = p Determina valor de PIVOTE
17. pp = q y de su padre.
18. end(if)
19. q=p
20. if (p.retornaDato() > d) then Actualiza Q
21. p = p.retornaLi() y
22. else avanza con P
23. p = p.retornaLd()
24. end(if)
25. end(while)
26. if (q.retornaDato() > d) then
27. q.asignaLi(x) Conecta x como hijo izquierdo
28. else o como hijo derecho de Q
29. q.asignaLd(x)
30. end(if)
31. aux = pivote.retornaFb()
32. if (pivote.retornaDato() > d) then
33. pivote.asignaFb(aux + 1)
34. q = pivote.retornaLi() Recalcula factor de balance
35. else de PIVOTE
36. pivote.asignaFb(aux – 1)
37. q = pivote.retornaLd()
38. end(if)
39. p=q
40. while (p != x) do
41. if (p.retornaDato() > d) then
42. p.asignaFb(+1) Recalcula factores de balance
43. p = p.retornaLi() de todos los registros en la
44. else trayectoria desde PIVOTE hasta
45. p.asignaFb(–1) el registro insertado x
46. p = p.retornaLd()
47. end(if)
48. end(while)
49. if (abs(pivote.retornaFb()) < 2) then Si el árbol siguió balanceado
50. return retorna al programa llamante.
51. end(if)
52. if (pivote.retornaFb() == +2) then
53. if (q.retornaFb() = +1) then
54. unaRotacionALaDerecha(pivote, q) Determina qué
55. else tipo de
56. dobleRotacionALaDerecha(pivote, q) rotación que hay
57. end(if) que efectuar
58. else y llama
59. if (q.retornaFb() == –1) then el
60. unaRotacionALaIzquierda(pivote, q) procedimiento
61. else correspondiente
62. dobleRotacionALaIzquierda(pivote, q)
63. end(if)
64. end(if)
65. if (pp == null) then Si el registro desbalanceado
66. r=q era la raíz actualiza la nueva
67. return raíz y regresa.
68. end(if)
69. if (pivote == pp.retornaLi() then
70. pp.asignaLi(q) Pega la nueva raíz del árbol
71. else rebalanceado al registro Q.
72. pp.asignaLd(q)
73. end(if)
74. Fin(insertaDatoEnAVL)

EJERCICIOS PROPUESTOS

1. Elabore algoritmo para hacer doble rotación a la izquierda.

2. Elabore algoritmo que determine si un árbol binario cualquiera está balanceado por altura o no. Su
algoritmo debe retornar verdadero si está balanceado por altura falso de lo contrario.

3. Modifique el algoritmo dobleRotacionALaDerecha de tal manera que no utilice instrucciones de decisión


para recalcular los nuevos factores de balance de los nodos p, q y r. Es decir, las instrucciones 7 a 16 las
debe reemplazar por instrucciones de asignación.

También podría gustarte