Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Lectura 5-2 - Árboles Binarios

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 19

ESTRUCTURAS DE DATOS

UNIDAD TRES - SEMANA CINCO


ÁRBOLES BINARIOS *

TABLA DE CONTENIDO

1. DEFINICIÓN 2
2. CONCEPTOS BÁSICOS 3
2.1. RELACIONES DE PARENTESCO ENTRE NODOS 3
2.2. HOJAS Y NODOS INTERNOS 3
2.3. CAMINOS Y RAMAS 4
2.4. DESCENDIENTES Y ANCESTROS 5
2.5. NIVEL 5
2.6. PESO Y ALTURA 5
2.7. ÁRBOLES DEGENERADOS, LLENOS, COMPLETOS Y PERFECTOS 6
3. IMPLEMENTACIÓN DE ÁRBOLES BINARIOS CON ÁRBOLES SENCILLAMENTE ENCADENADOS 7
3.1. CREACIÓN DE ÁRBOLES BINARIOS 9
3.2. MÉTODOS DE CONSULTA DE LOS ATRIBUTOS DE UN ÁRBOL BINARIO 10
3.3. MÉTODOS PARA CALCULAR EL PESO Y LA ALTURA DE UN ÁRBOL BINARIO 11
3.4. MÉTODOS DE BÚSQUEDA SOBRE UN ÁRBOL BINARIO 11
4. RECORRIDOS SOBRE ÁRBOLES BINARIOS 12
5. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO A PARTIR DE SUS RECORRIDOS 15
5.1. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO DADO SU INORDEN Y SU PREORDEN 16
EN RESUMEN 19
PARA TENER EN CUENTA 19

*
Resumen del libro Estructuras de Datos en Java de Alejandro Sotelo Arévalo, cuya publicación está pendiente.

ESTRUCTURAS DE DATOS 1
1. DEFINICIÓN

Un árbol binario es una estructura de datos recursiva que está compuesta por un valor,
llamado raíz, y por dos árboles, llamados subárbol izquierdo y subárbol derecho.

Formalmente:
1. El árbol que no posee ningún elemento, que representaremos gráficamente mediante el
símbolo , es un árbol binario denominado el árbol vacío.
2. Dado un valor val, y dos árboles binarios izq y der, se tiene que el árbol
val val : raíz

izq : subárbol izquierdo

izq der der : subárbol derecho

es un árbol binario cuya raíz es val, cuyo subárbol izquierdo es izq, y cuyo subárbol derecho
es der.

Tabla 1: Árboles binarios de ejemplo.


Árbol binario Raíz Subárbol izquierdo Subárbol derecho
no tiene no tiene no tiene

Un nodo se define como la raíz de un árbol binario no vacío. En particular, , , , , ,


y son los nodos que conforman los árboles del ejemplo anterior.

Observe que:
El árbol vacío no tiene raíz, no tiene subárbol izquierdo y no tiene subárbol derecho.
Todo árbol no vacío tiene raíz y tiene exactamente dos subárboles: el subárbol izquierdo y el
subárbol derecho. Es posible que uno o ambos subárboles sean vacíos.
El árbol vacío nunca puede ser considerado como nodo porque no es raíz de un árbol
binario no vacío.

ESTRUCTURAS DE DATOS 2
2. CONCEPTOS BÁSICOS

2.1. RELACIONES DE PARENTESCO ENTRE NODOS

Las relaciones familiares se utilizan para nombrar relaciones típicas entre nodos:

Tabla 2: Relaciones de parentesco entre nodos.


Parentesco Definición
Hijo Un nodo es hijo de un nodo si y sólo si es la raíz de alguno de los
subárboles de .
Padre Un nodo es padre de un nodo si y sólo si es hijo de .
Hermano Un nodo es hermano de un nodo si y sólo si y son distintos y tienen el
mismo padre.
Nieto Un nodo es nieto de un nodo si y sólo si es hijo de un hijo de .
Abuelo Un nodo es abuelo de un nodo si y sólo si es nieto de .
Tío Un nodo es tío de un nodo si y sólo si es hermano del padre de .
Sobrino Un nodo es sobrino de un nodo si y sólo si es tío de .
Primo Un nodo es primo de un nodo si y sólo si el padre de es hermano del padre
de .

De la misma forma se puede definir bisnieto, tataranieto, bisabuelo y tatarabuelo. Además,


tenga en cuenta que el árbol vacío no puede estar emparentado porque no es un nodo.

Tabla 3: Ejemplos sobre relaciones de parentesco.


Árbol binario Ejemplos
El árbol vacío no tiene nodos.
es hijo de , y es padre de .
no tiene padre, y no tiene hijos.
Los nodos y no tienen hermanos.
y son hijos de . es el único hijo de . y son hijos de .
es padre de y de . es padre de . es padre de y de .
es el único nodo que no tiene padre.
Los nodos que tienen exactamente dos hijos son , y .
Sólo hay un nodo que tiene exactamente un hijo: el .
Los nodos que no tienen hijos son , , y .

2.2. HOJAS Y NODOS INTERNOS


Tabla 4: Definición de hoja y de nodo interno.
Clasificación Definición
Hoja Un nodo es una hoja si y sólo si no tiene hijos.
Nodo interno Un nodo es un nodo interno si y sólo si tiene por lo menos un hijo.

ESTRUCTURAS DE DATOS 3
En otras palabras, un nodo es una hoja si sus dos subárboles son vacíos y es un nodo interno
si no es una hoja.

Tabla 5: Ejemplos sobre los conceptos de hoja y nodo interno.


Árbol binario Ejemplos
El número total de nodos del árbol es .
La raíz del árbol es el nodo con valor .
Los nodos , , , y son hojas porque no tienen hijos.
Los nodos , , , y 23 son nodos internos porque no son hojas.
Todo nodo del árbol es una hoja o es un nodo interno.

2.3. CAMINOS Y RAMAS

Un camino es una lista no vacía de nodos donde cada uno es padre del
que le sigue en la lista. La longitud de un camino es , o sea, el
número de nodos que enumera menos uno. Además, se define una rama como un camino
que parte de la raíz del árbol y llega a una hoja.

Tabla 6: Ejemplos sobre los conceptos de camino y rama.


Árbol binario Ejemplos
Hay caminos de longitud : , , , , ,y .
Hay caminos de longitud : , , , ,y .
Hay caminos de longitud : , ,y .
No hay caminos de longitud en adelante.
, y son hojas. Las ramas son , y .

Siempre existe un camino de longitud que va desde un nodo hacia sí mismo, y siempre
existe un camino que va desde la raíz del árbol hasta cualquier nodo. A veces puede no existir
camino entre un par de nodos.

Tabla 7: Ejemplos sobre existencia y no existencia de caminos.


Árbol binario Ejemplos
no es camino porque no es padre de . no es
camino pues no es padre de y no es padre de .
tampoco es camino. No hay camino que vaya desde hasta .
Hay camino del nodo hacia todos los demás porque es la raíz del árbol.

ESTRUCTURAS DE DATOS 4
2.4. DESCENDIENTES Y ANCESTROS
Tabla 8: Definición de ancestro y descendiente.
Relación Definición
Ancestro Un nodo es ancestro de un nodo si y sólo si hay camino que va de a .
Descendiente Un nodo es descendiente de un nodo si y sólo si es ancestro de .

Tabla 9: Ejemplos sobre los conceptos de ancestro y descendiente.


Árbol binario Nodo Ancestros Descendientes
, , , y .
y , y .
y .
y .
y .

2.5. NIVEL

El nivel de un nodo es la longitud del camino que va desde la raíz hasta el mismo nodo. Se
dice que un nivel está lleno si alberga la máxima cantidad posible de nodos que puede tener.

Tabla 10: Ejemplos sobre el concepto de nivel.


Árbol binario Ejemplos
El único nodo que está en el nivel es el .
Los nodos que están en el nivel son y .
Los nodos que están en el nivel son , , y .
Los nodos que están en el nivel son , y .
No hay nodos que estén desde el nivel en adelante.
Los niveles , y están llenos. El nivel no está lleno.

El nivel puede tener máximo un nodo, el nivel puede tener máximo dos nodos, el nivel
puede tener máximo cuatro nodos, el nivel puede tener máximo ocho nodos, y así
sucesivamente. En general, el nivel puede tener máximo nodos porque cada nivel aloja
máximo el doble de los nodos que el nivel anterior.

2.6. PESO Y ALTURA

El peso ( ) de un árbol binario es el número de nodos que tiene. La altura ( ) de un árbol


binario es uno más la longitud de la rama más larga. Tanto el peso como la altura del árbol
vacío se definen como cero.

ESTRUCTURAS DE DATOS 5
Tabla 11: Ejemplos sobre el concepto de peso y altura.
Árbol binario Longitud de la rama más larga Peso ( ) Altura ( )
No tiene ramas.

Para todo árbol binario no vacío, su peso se puede calcular como uno más la suma de los
pesos de sus dos subárboles, y su altura se puede calcular como uno más el máximo entre las
alturas de sus dos subárboles. Dado un árbol con altura , el peso mínimo que puede tener
es y el peso máximo que puede tener es . Así mismo, dado un árbol con peso , la
altura mínima que puede tener es la parte entera de y la altura máxima que
puede tener es . ¿Por qué se cumple todo esto?

Tabla 12: Ejemplos que ilustran la máxima cantidad de nodos que pueden tener los árboles según su altura.
Árbol binario Peso ( ) Altura ( )

2.7. ÁRBOLES DEGENERADOS, LLENOS, COMPLETOS Y PERFECTOS


Tabla 13: Definición de árbol degenerado, árbol lleno, árbol completo y árbol perfecto.
Concepto Definición (puede variar según la referencia bibliográfica)
Degenerado Un árbol es degenerado cuando su altura es igual a su peso.
Lleno Un árbol está lleno cuando todos sus nodos tienen cero o dos hijos.
Completo Un árbol está completo cuando todos sus niveles hasta el penúltimo están llenos, y
todos los nodos del último nivel están lo más a la
izquierda posible.
Perfecto Un árbol es perfecto cuando todos sus niveles están llenos.

ESTRUCTURAS DE DATOS 6
Tabla 14: Ejemplos sobre los conceptos de árbol degenerado, árbol lleno, árbol completo y árbol perfecto.
Árbol binario ¿Degenerado? ¿Lleno? ¿Completo? ¿Perfecto?
SI SI SI SI

SI NO NO NO

NO SI NO NO

NO NO SI NO

NO SI SI NO

NO SI SI SI

3. IMPLEMENTACIÓN DE ÁRBOLES BINARIOS CON ÁRBOLES


SENCILLAMENTE ENCADENADOS
Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

La clase VEDArbin<E> representa un árbol binario de elementos de tipo E (Arbin abrevia Árbol
binario). Los atributos de la clase VEDArbin<E> son:
 val: es una variable de tipo E que almacena el valor de la raíz del árbol.
 izq: es una variable de tipo VEDArbin<E> que apunta al subárbol izquierdo del árbol.
 der: es una variable de tipo VEDArbin<E> que apunta al subárbol derecho del árbol.

El árbol vacío se representa asignando null a los atributos val, izq y der, y los árboles no
vacíos se representan asignando valores distintos de null a los atributos val, izq y der, que
apuntan a la raíz, al subárbol izquierdo y al subárbol derecho, respectivamente.

Gráfica 15: Representación en memoria del árbol binario vacío, implementado con árboles sencillamente encadenados.
VEDArbin<E>
val 
izq 
der 

ESTRUCTURAS DE DATOS 7
Gráfica 16: Representación en memoria del árbol binario no vacío,
implementado con árboles sencillamente encadenados.
VEDArbin<E>
val apunta a la raíz …
izq apunta al subárbol izquierdo …
der apunta al subárbol derecho …

En toda situación, los tres atributos tienen el valor null (cuando el árbol es vacío) o los tres
atributos tienen valores distintos de null (cuando el árbol no es vacío). Entonces, bajo la
estructura de datos, para diferenciar un árbol vacío de uno no vacío basta inspeccionar el
atributo val: si val es null es porque el árbol es vacío, y si val no es null es porque el árbol
no es vacío.

Gráfica 17: Representación interna de un árbol binario, implementado con un árbol sencillamente encadenado.
VEDArbin<E>
val 8
izq
árbol der

VEDArbin<E> VEDArbin<E>
val 5 val 3
izq izq
der der

VEDArbin<E> VEDArbin<E> VEDArbin<E> VEDArbin<E>


val  val  val  val 
izq  izq  izq  izq 
der  der  der  der 

Durante la implementación de la estructura de datos, abreviaremos el peso del árbol con la


letra y la altura del árbol con la letra . Todas las complejidades temporales serán
expresadas en términos de y de .

Código 18: Declaración de la clase VEDArbin<E>.


public class VEDArbin<E> { // Declaración de la clase
// *************************
// * Atributos de la clase *
// *************************
protected E val;
protected VEDArbin<E> izq;
protected VEDArbin<E> der;
// ***********************
// * Métodos de la clase *
// ***********************
// ...
}

ESTRUCTURAS DE DATOS 8
3.1. CREACIÓN DE ÁRBOLES BINARIOS

La clase VEDArbin<E> tiene dos métodos constructores: uno para crear un árbol vacío y otro
para crear un árbol no vacío a partir de su raíz, de su subárbol izquierdo y de su subárbol
derecho.

Código 19: Métodos constructores de la clase VEDArbin<E>.


public VEDArbin() {
// El árbol vacío es representado como un árbol binario con raíz null, subárbol
// izquierdo null y subárbol derecho null.
val=null;
izq=null;
der=null;
}
public VEDArbin(E pVal, VEDArbin<E> pIzq, VEDArbin<E> pDer) {
// Si la raíz dada es null, lanzar error:
if (pVal==null) {
throw new NullPointerException("¡Un árbol no vacío debe tener raíz!");
}
// Si el subárbol izquierdo dado es null, lanzar error:
if (pIzq==null) {
throw new NullPointerException("¡Un árbol no vacío necesita subárbol izquierdo!");
}
// Si el subárbol derecho dado es null, lanzar error:
if (pDer==null) {
throw new NullPointerException("¡Un árbol no vacío necesita subárbol derecho!");
}
// Inicializar los atributos del árbol de acuerdo a los parámetros dados:
val=pVal;
izq=pIzq;
der=pDer;
}

VEDArbin<E> provee métodos para exportar un árbol como texto o como imagen.
Específicamente,
System.out.println(arb.toString());
imprime en consola la representación textual del árbol arb según cierto formato que
definiremos más adelante, y la instrucción
arb.exportarComoImagen("ejemplo.png");
es capaz de exportar una imagen en formato .png con una representación gráfica del árbol
arb.

Tabla 20: Ejemplos que ilustran cómo construir árboles y cómo transformarlos en texto e imagen.
Imagen exportada
Programa
y texto impreso
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
System.out.println(vacio.toString());
vacio.exportarComoImagen("ejemploCreacion01.png"); Ø
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
VEDArbin<Integer> arb5=new VEDArbin<Integer>(5,vacio,vacio);
System.out.println(arb5.toString());
arb5.exportarComoImagen("ejemploCreacion02.png"); [5:Ø,Ø]

ESTRUCTURAS DE DATOS 9
VEDArbin<Integer> vacio=new VEDArbin<Integer>();
VEDArbin<Integer> arb89=new VEDArbin<Integer>(89,vacio,vacio);
VEDArbin<Integer> arb23=new VEDArbin<Integer>(23,vacio,arb89);
VEDArbin<Integer> arb10=new VEDArbin<Integer>(10,vacio,vacio);
VEDArbin<Integer> arb98=new VEDArbin<Integer>(98,arb23,arb10);
System.out.println(arb98.toString());
arb98.exportarComoImagen("ejemploCreacion03.png"); [98:[23:Ø,[89:Ø,Ø]],[10:Ø,Ø]]
VEDArbin<String> vacio=new VEDArbin<String>();
VEDArbin<String> arbA=new VEDArbin<String>("LUZ",vacio,vacio);
VEDArbin<String> arbB=new VEDArbin<String>("FÉ",vacio,arbA);
VEDArbin<String> arbC=new VEDArbin<String>("SOL",arbB,vacio);
System.out.println(arbC.toString());
arbC.exportarComoImagen("ejemploCreacion04.png");
[SOL:[FÉ:Ø,[LUZ:Ø,Ø]],Ø]

3.2. MÉTODOS DE CONSULTA DE LOS ATRIBUTOS DE UN ÁRBOL BINARIO


Código 21: Método para consultar la raíz del árbol binario.
public E getVal() {
return val; // Retornar el valor de la raíz.
}

Código 22: Método para consultar el subárbol izquierdo del árbol binario.
public VEDArbin<E> getIzq() {
return izq; // Retornar el subárbol izquierdo.
}

Código 23: Método para consultar el subárbol derecho del árbol binario.
public VEDArbin<E> getDer() {
return der; // Retornar el subárbol derecho.
}

Código 24: Método para informar si el árbol binario es vacío o no, con complejidad temporal .
public boolean esVacio() {
if (val==null) { // Si la raíz es null:
return true; // Retornar true porque este árbol representa el árbol vacío.
}
else { // Si la raíz no es null:
return false; // Retornar false porque este árbol no representa el árbol vacío.
}
}

Código 25: Método para determinar si un nodo es una hoja o no, con complejidad temporal .
public boolean esHoja() {
if (esVacio()) { // Si este árbol es vacío:
return false; // Decir que este árbol no representa una hoja.
}
else { // Si este árbol no es vacío:
if (izq.esVacio()&&der.esVacio()) { // Si los dos subárboles son vacíos:
return true; // Decir que este árbol sí representa una hoja.
}
else { // Si alguno de los dos subárboles no es vacío:
return false; // Decir que este árbol no representa una hoja.
}
}
}

ESTRUCTURAS DE DATOS 10
3.3. MÉTODOS PARA CALCULAR EL PESO Y LA ALTURA DE UN ÁRBOL BINARIO
Código 26: Función que calcula el peso del árbol.
public int peso() {
if (esVacio()) { // Si este árbol es vacío:
return 0; // El peso del árbol vacío es 0.
}
else { // Si este árbol no es vacío:
// El peso de un árbol no vacío es uno más el peso del subárbol izquierdo más el
// peso del subárbol derecho:
return 1+izq.peso()+der.peso();
}
}

Código 27: Función que calcula la altura del árbol.


public int altura() {
if (esVacio()) { // Si este árbol es vacío:
return 0; // La altura del árbol vacío es 0.
}
else { // Si este árbol no es vacío:
// La altura de un árbol no vacío es uno más el máximo entre la altura del subárbol
// izquierdo y la altura del subárbol derecho:
return 1+Math.max(izq.altura(),der.altura());
}
}

La complejidad temporal de los dos métodos anteriores es porque visitan exactamente


una vez cada uno de los nodos del árbol. Pero ¿por qué el procesamiento de los subárboles
vacíos no afecta la complejidad temporal? Debido a que es el número de nodos del árbol y
que cada nodo puede aportar máximo dos subárboles vacíos, entonces el número total de
subárboles vacíos que puede tener el árbol nunca puede ser mayor que . Dado que los
dos métodos anteriores procesan exactamente una vez cada nodo y cada subárbol vacío,
entonces el número de invocaciones recurrentes es menor o igual que (el número de
nodos) más (el máximo número de subárboles vacíos), lo que resulta en máximo
llamados. Se concluye entonces que la complejidad temporal de ambos algoritmos es .

3.4. MÉTODOS DE BÚSQUEDA SOBRE UN ÁRBOL BINARIO

Para determinar el número de veces que aparece un valor dentro del árbol binario, se deben
inspeccionar todos los nodos del árbol, comparándolos contra el valor buscado.

Código 28: Función que determina la cantidad de ocurrencias de un elemento en el árbol.


public int numeroOcurrencias(Object pObject) {
if (esVacio()) { // Si este árbol es vacío:
// El objeto no está en el árbol porque el árbol vacío no tiene elementos:
return 0;
}
else { // Si este árbol no es vacío:
// Contar cuántas veces está el objeto dado en el subárbol izquierdo:
int contadorIzq=izq.numeroOcurrencias(pObject);
// Contar cuántas veces está el objeto dado en el subárbol derecho:
int contadorDer=der.numeroOcurrencias(pObject);
if (pObject.equals(val)) { // Si el objeto buscado es igual a la raíz del árbol:

ESTRUCTURAS DE DATOS 11
// Retornar uno más el número de ocurrencias en izq y en der:
return 1+contadorIzq+contadorDer;
}
else { // Si el objeto buscado no es igual a la raíz del árbol:
// Retornar el número de ocurrencias en izq y en der:
return contadorIzq+contadorDer;
}
}
}

El algoritmo tiene complejidad temporal porque procesa exactamente una vez cada
nodo.

4. RECORRIDOS SOBRE ÁRBOLES BINARIOS


Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

Un recorrido de un árbol binario es un proceso que visita exactamente una vez cada uno de
los nodos del árbol, en cierto orden determinado. Existen muchas formas de recorrer un
árbol binario, entre estas:

Tabla 29: Resumen de los cuatro principales recorridos sobre árboles binarios.
Recorrido Esquema Descripción
Visita primero la raíz del árbol, luego recorre el subárbol
En preorden val-[izq]-[der] izquierdo en preorden, y después recorre el subárbol derecho
en preorden.
Recorre el subárbol izquierdo en inorden, luego visita la raíz del
En inorden [izq]-val-[der]
árbol, y después recorre el subárbol derecho en inorden.
Recorre el subárbol izquierdo en postorden, luego recorre el
En
[izq]-[der]-val subárbol derecho en postorden, y después visita la raíz del
postorden
árbol.
nivel0-…-nivelh- Visita los nodos nivel por nivel, desde el nivel hasta el nivel
Por niveles 1 , donde cada nivel se recorre de izquierda a derecha.

Todos los recorridos se pueden tratar como listas que enumeran los nodos del árbol en un
orden específico. Observe que los procesos descritos para hallar los recorridos en preorden,
en inorden y en postorden son recursivos, y que el proceso definido para el recorrido por
niveles es iterativo.

Tabla 30: Recorridos de algunos árboles binarios.


Recorrido
Árbol binario
Preorden Inorden Postorden Niveles

ESTRUCTURAS DE DATOS 12
Código 31: Método recursivo que halla el recorrido en preorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> preorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
preorden(lista); // Alimentar la lista con el recorrido en preorden.
return lista; // Retornar la lista.
}
private void preorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
pLista.add(val); // Añadir la raíz del árbol a la lista.
izq.preorden(pLista); // Recorrer el subárbol izquierdo en preorden.
der.preorden(pLista); // Recorrer el subárbol derecho en preorden.
}
}

Código 32: Método recursivo que halla el recorrido en inorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> inorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
inorden(lista); // Alimentar la lista con el recorrido en inorden.
return lista; // Retornar la lista.
}
private void inorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
izq.inorden(pLista); // Recorrer el subárbol izquierdo en inorden.
pLista.add(val); // Añadir la raíz del árbol a la lista.
der.inorden(pLista); // Recorrer el subárbol derecho en inorden.
}
}

Código 33: Método recursivo que halla el recorrido en postorden con complejidad temporal ,
donde es el peso del árbol.
public List<E> postorden() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
postorden(lista); // Alimentar la lista con el recorrido en postorden.
return lista; // Retornar la lista.
}
private void postorden(List<E> pLista) {
if (esVacio()) { // Si este árbol es vacío:
// No hacer ninguna operación.
}
else { // Si este árbol no es vacío:
izq.postorden(pLista); // Recorrer el subárbol izquierdo en postorden.
der.postorden(pLista); // Recorrer el subárbol derecho en postorden.
pLista.add(val); // Añadir la raíz del árbol a la lista.
}
}

ESTRUCTURAS DE DATOS 13
Para recorrer un árbol por niveles tenemos el siguiente algoritmo iterativo con complejidad
temporal :
1. Cree una cola vacía de árboles.
2. Inserte en la cola el árbol cuyo recorrido por niveles se desee hallar.
3. Mientras la cola no sea vacía:
3.1. Elimine el árbol que se encuentra en la cabeza de la cola. Llámese arb a este árbol.
3.2. Si el árbol arb no es vacío:
3.2.1. Visite la raíz del árbol arb.
3.2.2. Inserte en la cola el subárbol izquierdo del árbol arb.
3.2.3. Inserte en la cola el subárbol derecho del árbol arb.

Código 34: Método iterativo que halla el recorrido por niveles con complejidad temporal ,
donde es el peso del árbol.
public List<E> niveles() {
List<E> lista=new ArrayList<E>(); // Crear una lista nueva.
// Crear una nueva cola vacía para realizar el proceso:
Queue<VEDArbin<E>> cola=new LinkedList<VEDArbin<E>>();
cola.offer(this); // Añadir a la cola este árbol.
while (!cola.isEmpty()) { // Mientras la cola no sea vacía:
// Extraer la cabeza de la cola y guardarla en la variable 'actual':
VEDArbin<E> actual=cola.poll();
if (!actual.esVacio()) { // Si el árbol 'actual' no es vacío:
lista.add(actual.val); // Añadir a la lista la raíz del árbol 'actual'.
cola.offer(actual.izq); // Agregar a la cola el subárbol izquierdo de 'actual'.
cola.offer(actual.der); // Agregar a la cola el subárbol derecho de 'actual'.
}
}
return lista; // Retornar la lista.
}

¿Por qué funciona este algoritmo? El primer árbol que se añade a la cola es el árbol raíz, que
pertenece al nivel . Al sacar de la cola este árbol, se añaden a la cola sus subárboles, que
son precisamente los que pertenecen al nivel . Luego, para cada árbol del nivel , se saca de
la cabeza de la cola y se insertan en la cola sus subárboles, que son precisamente los que
pertenecen al nivel . Después, para cada árbol del nivel , se saca de la cabeza de la cola y
se insertan en la cola sus subárboles, que son precisamente los que pertenecen al nivel .
Siguiendo el proceso, descartando en cada paso los subárboles vacíos, se terminarían
visitando todos los nodos del árbol nivel por nivel.

Tabla 35: Algoritmo para hallar manualmente el recorrido en preorden.


Ilustración representativa Algoritmo
Mentalmente, siga el rastro de la silueta del árbol
como muestra la ilustración. Cada vez que toque
un nodo por su parte izquierda, añada su valor a
una lista. Los números dentro de las cajas rojas
muestran el orden en el que se van visitando los
elementos.
Recorrido en preorden del ejemplo:

ESTRUCTURAS DE DATOS 14
Tabla 36: Algoritmo para hallar manualmente el recorrido en inorden.
Ilustración representativa Algoritmo
Mentalmente, siga el rastro de la silueta del árbol
como muestra la ilustración. Cada vez que toque
un nodo por su parte inferior, añada su valor a
una lista. Los números dentro de las cajas rojas
muestran el orden en el que se van visitando los
elementos.
Recorrido en inorden del ejemplo:

Tabla 37: Algoritmo para hallar manualmente el recorrido en postorden.


Ilustración representativa Algoritmo
Mentalmente, siga el rastro de la silueta del árbol
como muestra la ilustración. Cada vez que toque
un nodo por su parte derecha, añada su valor a
una lista. Los números dentro de las cajas rojas
muestran el orden en el que se van visitando los
elementos.
Recorrido en postorden del ejemplo:

Tabla 38: Algoritmo para hallar manualmente el recorrido por niveles.


Ilustración representativa Algoritmo
Mentalmente, nivel por nivel, visite los nodos de
izquierda a derecha, añadiéndolos a una lista. Los
números dentro de las cajas rojas muestran el
orden en el que se van visitando los elementos.
Recorrido por niveles del ejemplo:

5. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO A PARTIR DE SUS


RECORRIDOS
Recurso como proyecto en Eclipse: ArbolesBinarios.zip.

Para poder reconstruir un árbol binario es necesario que todos sus elementos sean distintos,
porque de lo contrario, no se podría determinar de forma única la estructura del árbol.

ESTRUCTURAS DE DATOS 15
Gráfica 39: Cinco árboles binarios con elementos repetidos y con distinta forma, donde todos sus recorridos son iguales.

Además, es necesario tener el recorrido en inorden. El siguiente ejemplo muestra dos árboles
que tienen los mismos recorridos en preorden, en postorden y por niveles, pero diferente
recorrido en inorden:

Tabla 40: Dos árboles binarios tales que todos sus recorridos son iguales, excepto el inorden.
Recorrido
Árbol binario
Preorden Inorden Postorden Niveles

Para poder reconstruir un árbol binario sin ambigüedad, se requiere:


1. Que el árbol no tenga elementos repetidos.
2. Contar con el recorrido en inorden y con algún otro recorrido, ya sea en preorden, en
postorden o por niveles. En otras palabras, se requiere el inorden y el preorden, o el inorden
y el postorden, o el inorden y el recorrido por niveles.

La reconstrucción de árboles es importante ya que nos permite la persistencia de un árbol


binario mediante el almacenamiento del inorden y de otro de sus recorridos. Es decir, para
guardar y cargar un árbol binario basta manipular dos listas: dos de sus recorridos donde uno
de éstos es el inorden.

5.1. RECONSTRUCCIÓN DE UN ÁRBOL BINARIO DADO SU INORDEN Y SU


PREORDEN

Sabiendo que el inorden recorre nodos en la forma


subárbol izquierdo en inorden – raíz – subárbol derecho en inorden
y que el preorden recorre nodos en la forma
raíz – subárbol izquierdo en preorden – subárbol derecho en preorden
podemos utilizar el siguiente algoritmo para reconstruir un árbol binario sin elementos
repetidos dados sus recorridos en inorden y en preorden:

1. Si el inorden y el preorden son vacíos, retorne el árbol vacío.


2. Si el inorden y el preorden no son vacíos:

ESTRUCTURAS DE DATOS 16
2.1. Sea n el número de elementos del inorden, que coincide con el número de elementos
que tendrá el árbol que vamos a reconstruir.
2.2. Sea nuevoVal el primer elemento del recorrido en preorden, que es precisamente la raíz
del árbol.
2.3. Busque el valor nuevoVal en el inorden (sabemos que no aparece más de una vez porque
el árbol no tiene elementos repetidos). Sea pos la posición donde se encontró nuevoVal en el
inorden.
2.4. Aislamos el inorden del subárbol izquierdo, que es la sublista del inorden que va de la
posición 0 a la posición pos-1. Así mismo, aislamos el preorden del subárbol izquierdo, que es
la sublista del preorden que va de la posición 1 a la posición pos.
2.5. Llamamos recursivamente al algoritmo para reconstruir el subárbol izquierdo, según el
inorden y el preorden que aislamos en el paso anterior. Sea nuevoIzq el subárbol
reconstruido.
2.6. Aislamos el inorden del subárbol derecho, que es la sublista del inorden que va de la
posición pos+1 a la posición n-1. Así mismo, aislamos el preorden del subárbol derecho, que
es la sublista del preorden que va de la posición pos+1 a la posición n-1.
2.7. Llamamos recursivamente al algoritmo para reconstruir el subárbol derecho, según el
inorden y el preorden que aislamos en el paso anterior. Sea nuevoDer el subárbol
reconstruido.
2.8. Retorne un nuevo árbol binario cuya raíz sea nuevoVal, cuyo subárbol izquierdo sea
nuevoIzq y cuyo subárbol derecho sea nuevoDer.

Gráfica 41: Diagrama para guiar la reconstrucción de un árbol binario dado su recorrido en inorden y su recorrido en
preorden.
pos+1
pos-1

pos

n-3

n-2

n-1
0

inorden ... ...


Subárbol izquierdo en inorden Subárbol derecho en inorden
pos+1
pos-1

pos

n-3

n-2

n-1
0

preorden ... ...


Subárbol izquierdo en preorden Subárbol derecho en preorden

Tabla 42: Reconstrucción del árbol binario con inorden y con preorden .
Inorden Preorden Árbol Operación
La raíz es porque es el primer elemento del
preorden. En el inorden, a la izquierda del hay
dos elementos, y a la derecha hay cuatro. Por lo
tanto, se deduce que el subárbol izquierdo tiene
dos nodos y que el subárbol derecho tiene
cuatro nodos. Finalmente, aislamos tanto el
subárbol izquierdo como el subárbol derecho en
cada uno de los dos recorridos.

ESTRUCTURAS DE DATOS 17
Sabemos que el subárbol izquierdo tiene
inorden y preorden y que el
subárbol derecho tiene inorden y
preorden . De la misma forma,
reconstruimos ambos subárboles.
Sabemos que el subárbol izquierdo del nodo
tiene inorden y preorden . Con el
mismo procedimiento reconstruimos este árbol.

Note cómo se reconstruyó el árbol binario


usando recursivamente el mismo argumento.

El siguiente algoritmo en Java reconstruye un árbol binario sin elementos repetidos a partir
de su inorden y de su preorden, con complejidad temporal .

Código 43: Rutina que reconstruye un árbol binario dado su inorden y su preorden.
public static <T> VEDArbin<T> reconstruirArbol(List<T> pInorden, List<T> pPreorden) {
if (pInorden.isEmpty()) { // Si el recorrido en inorden es vacío:
return new VEDArbin<T>(); // Retornar el árbol vacío.
}
else { // Si el recorrido en inorden no es vacío:
// Guardar en una variable el número de nodos que tendría el árbol a reconstruir:
int n=pInorden.size();
// Guardar en una variable la raíz del árbol, que está al principio del preorden:
T nuevoVal=pPreorden.get(0);
// Buscar la posición en la que aparece la raíz dentro del inorden:
int pos=pInorden.indexOf(nuevoVal);
// Extraer el inorden del subárbol izquierdo:
List<T> inordenIzq=pInorden.subList(0,pos);
// Extraer el preorden del subárbol izquierdo:
List<T> preordenIzq=pPreorden.subList(1,pos+1);
// Reconstruir recursivamente el subárbol izquierdo:
VEDArbin<T> nuevoIzq=VEDArbin.reconstruirArbol(inordenIzq,preordenIzq);
// Extraer el inorden del subárbol derecho:
List<T> inordenDer=pInorden.subList(pos+1,n);
// Extraer el preorden del subárbol derecho:
List<T> preordenDer=pPreorden.subList(pos+1,n);
// Reconstruir recursivamente el subárbol derecho:
VEDArbin<T> nuevoDer=VEDArbin.reconstruirArbol(inordenDer,preordenDer);
// Retornar el nuevo árbol reconstruido:
return new VEDArbin<T>(nuevoVal,nuevoIzq,nuevoDer);
}
}

Recuerde que el método subList(fromIndex,toIndex) de la interfaz List<E> entrega una


sublista que va desde la posición fromIndex hasta la posición toIndex-1 (¡la posición final no
se incluye!).

Tabla 44: Ejemplos que ilustran cómo reconstruir árboles binarios usando la rutina descrita,
y cómo transformarlos en texto e imagen.
Programa Imagen exportada
List<Integer> in=Arrays.asList(); // Inorden
List<Integer> pre=Arrays.asList(); // Preorden

ESTRUCTURAS DE DATOS 18
VEDArbin<Integer> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion01.png");
List<Integer> in=Arrays.asList(23,89,98,10); // Inorden
List<Integer> pre=Arrays.asList(98,23,89,10); // Preorden
VEDArbin<Integer> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion02.png");
List<String> in=Arrays.asList("FÉ","LUZ","SOL"); // Inorden
List<String> pre=Arrays.asList("SOL","FÉ","LUZ"); // Preorden
VEDArbin<String> arbol=VEDArbin.reconstruirArbol(in,pre);
arbol.exportarComoImagen("ejemploReconstruccion03.png");

EN RESUMEN
Un árbol binario es una estructura de datos recursiva que está compuesta por un valor, llamado raíz,
y por dos árboles, llamados subárbol izquierdo y subárbol derecho.
Una hoja es un nodo sin hijos. Un nodo interno es un nodo que no es una hoja.
Una rama es un camino que va de la raíz a una hoja.
El peso ( ) de un árbol binario es el número de nodos que tiene. La altura ( ) de un árbol binario es
uno más la longitud de la rama más larga. Tanto el peso como la altura del árbol vacío se definen
como cero.
El recorrido en preorden visita primero la raíz del árbol, luego recorre el subárbol izquierdo en
preorden, y después recorre el subárbol derecho en preorden.
El recorrido en inorden recorre el subárbol izquierdo en inorden, luego visita la raíz del árbol, y
después recorre el subárbol derecho en inorden.
El recorrido en postorden recorre el subárbol izquierdo en postorden, luego recorre el subárbol
derecho en postorden, y después visita la raíz del árbol.
El recorrido por niveles visita los nodos nivel por nivel, desde el nivel hasta el nivel , donde
cada nivel se recorre de izquierda a derecha.
Para poder reconstruir un árbol binario sin ambigüedad, se requiere: 1. que el árbol no tenga
elementos repetidos, y 2. contar con el recorrido en inorden y con algún otro recorrido, ya sea en
preorden, en postorden o por niveles.

PARA TENER EN CUENTA


Es necesario practicar a través del desarrollo de algunos de los ejercicios propuestos para fortalecer
los conceptos tratados en esta lectura.

ESTRUCTURAS DE DATOS 19

También podría gustarte