¡Descarga Arboles java estructuras de datos dinamicos y más Resúmenes en PDF de Estructuras de Datos y Algoritmos solo en Docsity!
ESTRUCTURAS DE DATOS ÁRBOLES 143
TEMA 4.
ÁRBOLES
4.1. CONCEPTOS GENERALES.
Un árbol es una estructura de datos ramificada (no lineal) que puede representarse
como un conjunto de nodos enlazados entre sí por medio de ramas. La información
contenida en un nodo puede ser de cualquier tipo simple o estructura de datos.
Los árboles permiten modelar diversas entidades del mundo real tales como, por
ejemplo, el índice de un libro, la clasificación del reino animal, el árbol genealógico de un
apellido, etc.
La figura 4.1. muestra un ejemplo de estructura en árbol (la numeración de los nodos
es arbitraria). Se entiende por “topología” de un árbol a su representación geométrica.
1
2 3 4
5 6 7 8 9
10 11 12
Figura 4.1. Ejemplo de árbol.
144 ÁRBOLES ESTRUCTURAS DE DATOS
Una definición formal es la siguiente:
Un árbol es una estructura de datos base que cumple una de estas dos condiciones:
Es una estructura vacía, o
Es un nodo de tipo base que tiene de 0 a N subárboles disjuntos entre sí.
Al nodo base, que debe ser único, se le denomina raíz y se establece el convenio de
representarlo gráficamente en la parte superior.
En un árbol se representa una relación jerárquica a partir del nodo raíz en sentido
vertical descendente, definiendo niveles
1
. El nivel del nodo raíz es 1.
Desde la raíz se puede llegar a cualquier nodo progresando por las ramas y
atravesando los sucesivos niveles estableciendo así un camino. En la figura 4.1. el nodo 7
está a nivel 3 y la secuencia de nodos 4, 8 y 11 constituye un (sub)camino.
Se dice que un nodo es antecesor de otro cuando ambos forman parte de un camino y
el primero se encuentra en un nivel superior (numeración más baja) al del segundo
(numeración más alta). En el ejemplo anterior el nodo 4 es antecesor del 11. Por el
contrario, el nodo 11 es descendiente del 4.
La relación entre dos nodos separados de forma inmediata por una rama se denomina
padre/hijo. En el ejemplo de la figura 4.1., el nodo 5 es hijo del nodo 2 y, recíprocamente,
el nodo 2 es padre del nodo 5. En un árbol un padre puede tener varios hijos pero un hijo
solo puede tener un padre.
Se denomina grado al número de hijos de un nodo. Por ejemplo, en la figura 4.1. el
nodo 4 tiene grado 3 y el nodo 7 tiene grado 0.
Se dice que un nodo es hoja cuando no tiene descendientes (grado 0).
Se establecen los siguientes atributos para un árbol:
Altura / profundidad / nivel: La mayor altura / profundidad / nivel de sus nodos. La
altura del árbol de la figura 4.1. es 4 (la alcanzan sus nodos 10, 11 y 12).
Amplitud / Anchura: El número de nodos del nivel más poblado. En el ejemplo, 5
(nivel 3).
Grado: el mayor de los grados de los nodos. En el ejemplo, 3 (nodos 1 y 4).
1
Los términos “altura” o “profundidad” son sinónimos del de nivel.
146 ÁRBOLES ESTRUCTURAS DE DATOS
4.2. ÁRBOLES BINARIOS.
Se definen como árboles de grado 2. Esto es, cada nodo puede tener dos, uno o
ningún hijo. Al tratarse como mucho de dos hijos, cada uno de ellos puede identificarse
como hijo izquierdo o hijo derecho.
4.2.1. Implementación física.
El gráfico de un árbol es una representación conceptual cuya implementación física
admite diversas posibilidades condicionadas, en primer lugar, por el dispositivo de
almacenamiento del mismo (memoria principal o memoria externa). A los efectos del curso
nos ocuparemos exclusivamente de la memoria principal en donde puede optarse por dos
filosofías principales:
Estructuras de datos estáticas, normalmente matrices.
Estructuras de datos dinámicas
En cualquier caso, la representación física de un árbol requiere contemplar tres
componentes:
La clave (simple o compuesta).
Dos punteros, indicando las ubicaciones respectivas de los nodos hijo izquierdo e
hijo derecho
2
Ejemplo. La figura 4.3. representa un árbol binario que podría implementarse
físicamente según se ilustra en las figuras 4.4. (estructura estática) ó 4.5. (estructura
dinámica).
15
25 20
10 5
45
Figura 4.3. Ejemplo de árbol binario.
Clave 15 20 5 25 45 10 Hijo izquierdo 3 5 4 * * * Hijo derecho 1 2 * * * *
Figura 4.4. Ejemplo de árbol binario. Implementación estática.
2
Deberá establecerse un convenio para indicar la inexistencia de hijo(s).
ESTRUCTURAS DE DATOS ÁRBOLES 147
Puntero al árbol
raíz 20 15
25 * *
10 * * 5 *
45 * *
Figura 4.5. Ejemplo de árbol binario. Implementación dinámica.
4.2.2. Algoritmos básicos con árboles binarios
3
Para la utilización de árboles binarios es necesario definir las clases NodoArbol y
Arbol siguiendo la sintaxis siguiente:
public class NodoArbol {
public NodoArbol (int dato) { clave = dato;
iz = null;
de = null; }
public int clave; public NodoArbol iz, de;
}
public class Arbol {
public NodoArbol raiz;
public Arbol () { raiz = null;
}
}
4.2.2.1.Recorridos.
Se entiende por recorrido el tratamiento realizado para acceder a los diferentes nodos
de un árbol. El recorrido puede afectar a la totalidad de los nodos del árbol (recorrido
completo), por ejemplo si se desea conocer el número de nodos, o finalizar anticipadamente
en función de determinada/s circunstancia/s, por ejemplo al encontrar el valor de una clave
determinada.
En cualquier caso, el recorrido se puede realizar basándose en las siguientes
modalidades:
3
Los siguientes algoritmos se han desarrollado en Java considerando la implementación del árbol por medio
de una estructura dinámica. A efectos de la realización de prácticas se utiliza la sintaxis del lenguaje Java.
ESTRUCTURAS DE DATOS ÁRBOLES 149
o Postorden. Se desciende recursivamente por la rama izquierda, al alcanzar el
final de dicha rama, se retorna y se desciende por la rama derecha. Cuando se
alcanza el final de la rama derecha, se procesa la clave. La exploración en
postorden del árbol de la figura 4.3.a. daría el siguiente resultado: 25, 10, 45, 5,
Este recorrido es el menos utilizado. Se utiliza para liberar memoria, o bien
cuando la información de los niveles más profundos del árbol afecta a la
información buscada.
En general, el algoritmo de recorrido en postorden es el siguiente:
// Escribe las claves del árbol binario en postorden. static void postOrden (NodoArbol arbol) { if (arbol != null) { postOrden (arbol.iz); // Izquierda postOrden (arbol.de); // Derecha System.out.print (arbol.clave + " ") ; // Nodo } } public void postOrden () { postOrden (raiz); }
Ejemplo de algoritmo de recorrido en profundidad: desarrollar un método estático
(sumaClaves) que obtenga la suma de las claves del árbol
4
static int sumaClaves (NodoArbol arbol) {
int resul;
if (arbol != null) resul = arbol.clave + sumaClaves (arbol.iz) + sumaClaves (arbol.de); else resul = 0; return resul;
}
static int sumaClaves (Arbol a) {
return sumaClaves (a.raiz);
}
Como ejercicio se propone al alumno la realización de un algoritmo que cuente los
nodos que tiene un árbol, utilizando uno de los tres recorridos en profundidad
propuestos.
4
Puede realizarse indistintamente con cualquiera de las modalidades de recorrido, en la solución propuesta se
hace un recorrido en preorden.
150 ÁRBOLES ESTRUCTURAS DE DATOS
Recorrido en amplitud. Implica un acceso a las claves recorriendo cada nivel de
izquierda a derecha y descendiendo al siguiente. Por ejemplo, la exploración en
amplitud del árbol de la figura 4.3.a. daría el siguiente resultado: 15, 25, 20, 10, 5,
Para la implementación del recorrido en amplitud se requiere la utilización de la
técnica iterativa así como de la disponibilidad de una estructura de datos auxiliar: TAD cola
de nodos de árbol:
class NodoCola {
NodoArbol dato; NodoCola siguiente; // Constructores NodoCola (NodoArbol elemento, NodoCola n) { dato = elemento; siguiente = n; }
}
Por ejemplo, el algoritmo siguiente permite obtener un listado de las claves de un
árbol binario recorrido en amplitud:
static void listarAmplitud (NodoArbol arbol) {
NodoArbol p; Cola c = new TadCola ();
p = arbol;
if (p != null)
c.encolar (p); while (! c.colaVacia ()) {
p = c.desencolar (); System.out.print (p.clave + " ");
if (p.iz != null) c.encolar (p.iz);
if (p.de != null)
c.encolar (p.de); }
} public void listarAmplitud () {
listarAmplitud (raiz);
}
La idea, básicamente, consiste en encolar en la cola de punteros las direcciones
(referencias) de los posibles hijos de cada nodo procesado (su dirección se encuentra en la
variable p ).
A continuación, en cada iteración se extrae el primer elemento de la cola que se
corresponde con el nodo actual del árbol. Si su hijo izquierdo y/o su hijo derecho son
distintos de null se encolan, y el proceso terminará cuando la cola se encuentre vacía.
152 ÁRBOLES ESTRUCTURAS DE DATOS
public void juntar (int dato, Arbol a1, Arbol a2) { if (a1.raiz == a2.raiz && a1.raiz != null) { System.out.println ("no se pueden mezclar, t1 y t2 son iguales") ; return; } // Crear el nodo nuevo raiz = new NodoArbol (dato, a1.raiz, a2.raiz) ; // Borrar los árboles a1 y a if (this != a1) a1.raiz = null; if (this != a2) a2.raiz = null; }
En el siguiente ejemplo, se muestra como generar el árbol de la figura 4.6:
public static void main (String [] args) { Arbol a1 = new Arbol (1) ; Arbol a3 = new Arbol (3) ; Arbol a5 = new Arbol (5) ; Arbol a7 = new Arbol (7) ; Arbol a2 = new Arbol (); Arbol a4 = new Arbol (); Arbol a6 = new Arbol ();
a2.juntar (2, a1, a3) ; a6.juntar (6, a5, a7) ; a4.juntar (4, a2, a6) ;
}
Figura 4.6. Ejemplo de árbol binario.
4.2.2.4.Tratamiento de hojas.
En este tipo de algoritmos se trata de evaluar la condición de que un nodo del árbol
sea una hoja:
(arbol.iz == null) && (arbol.de == null).
Por ejemplo, el siguiente método de tipo entero ( cuentaHojas ) devuelve como
resultado el número de hojas del árbol que se le entrega como argumento.
4
2 6
1 3 5 7
ESTRUCTURAS DE DATOS ÁRBOLES 153
static int cuentaHojas (NodoArbol arbol) {
int resul = 0;
if (arbol != null) if ((arbol.iz == null) && (arbol.de == null))
resul = 1; else resul = cuentaHojas (arbol.iz) + cuentaHojas (arbol.de);
return resul;
}
static int cuentaHojas (Arbol a) {
return cuentaHojas (a.raiz);
}
4.2.2.5. Procesamiento con constancia del nivel.
Determinados tratamientos requieren conocer el nivel en que se encuentran los nodos
visitados, para lo cual es necesario conocer el nivel actual.
Recorrido en profundidad.
Cuando se va a realizar un tratamiento en profundidad, pasamos como argumento
(por valor) el nivel actual. En cada llamada recursiva (por la derecha o por la izquierda) se
incrementa el nivel en una unidad. La inicialización de dicho argumento (a 1) se realiza en
la primera llamada (no recursiva).
Por ejemplo, el siguiente método ( clavesNiveles ) recorre un árbol en preorden
5
mostrando en la pantalla el valor de las diferentes claves así como el nivel en que se
encuentran.
static void clavesNiveles (NodoArbol arbol, int n) { if (arbol != null) { System.out.println ("Clave: " + arbol.clave + " en el nivel: " + n); clavesNiveles (arbol.iz, n+1); clavesNiveles (arbol.de, n+1); } } static void clavesNiveles (Arbol a) { clavesNiveles (a.raiz, 1); }
Recorrido en amplitud.
El algoritmo explicado para el recorrido en amplitud en el apartado 3.2.2.2., permite
recorrer el árbol en amplitud, sin embargo, no se tiene constancia del nivel en que se
encuentran los nodos a los que se accede.
En caso de que esto sea necesario debería modificarse, bien la estructura de la cola
añadiendo el nivel, o bien el algoritmo visto anteriormente de manera que su estructura sea
una iteración anidada en dos niveles.
5
La modalidad de recorrido es indiferente.
ESTRUCTURAS DE DATOS ÁRBOLES 155
(controlado por medio de la variable contador ) es necesario conocer a priori su amplitud
(variable actual ) y según se va explorando dicho nivel se cuenta el número de hijos del
siguiente nivel (variable siguiente ). Al iniciar el proceso del siguiente nivel se pasa a la
variable actual el último valor encontrado en la variable siguiente.
A continuación se muestra como ejemplo una variante del ejemplo anterior.
static void listarAmplitudNiveles (NodoArbol arbol) {
NodoArbol p; Cola c = new TadCola ();
int actual, siguiente, contador, altura;
altura = 0; siguiente = 1;
p = arbol;
if (p != null ) c.encolar (p);
while (!c.colaVacia()) { actual = siguiente;
siguiente = 0; contador = 1;
altura++; while (contador <= actual) {
p = c.desencolar ();
System.out.println ("clave: " + p.clave + " nivel: " + altura); contador++;
if (p.iz != null) { c.encolar (p.iz);
siguiente++;
} if (p.de != null) {
c.encolar (p.de); siguiente++;
} }
}
} public void listarAmplitudNiveles (){ listarAmplitudNiveles (raiz); }
156 ÁRBOLES ESTRUCTURAS DE DATOS
4.2.3. Ejemplo.
A continuación se desarrolla un método que intenta verificar si dos árboles son
iguales o no.
static boolean iguales (NodoArbol a, NodoArbol b) {
boolean resul ;
if ((a == null) && (b == null)) resul = true;
else if ((a == null) || (b == null)) resul = false;
else if (a.clave == b.clave)
resul = iguales(a.iz, b.iz) && iguales (a.de, b.de); else resul = false;
return resul; }
static boolean iguales (Arbol a1, Arbol a2) {
return iguales (a1.raiz, a2.raiz); }
158 ÁRBOLES ESTRUCTURAS DE DATOS
En caso de no buscar una clave determinada el recorrido del árbol binario de
búsqueda no ofrece ninguna particularidad respecto al árbol binario genérico, pudiendo
realizarse dicho recorrido en cualquier modalidad: orden central, preorden o postorden, así
como en amplitud.
No obstante, cuando se desea recuperar el conjunto de claves del árbol ordenadas, el
recorrido deberá ser en orden central (de izquierda a derecha para secuencia ascendente y
de derecha a izquierda para secuencia descendente).
A continuación se muestra como ejemplo el método booleano esBusqueda que
devuelve true si el arbol binario , que se pasa como argumento, es de búsqueda y false en
caso contrario. Consiste en verificar que la secuencia de claves es ascendente, por lo que
habrá que hacer un recorrido en orden central (de izquierda a derecha) y comparar el valor
de cada clave con la anterior ( ant ) que, debidamente inicializada, deberá pasarse como
argumento en las sucesivas llamadas recursivas. Es necesario identificar el primer elemento
de la lista (no tiene elemento anterior con el que comparar la clave), con una variable
auxiliar booleana ( primero ), inicializada a true desde fuera de el método.
static class Busqueda {
static int ant; static boolean primero = true;
static boolean esBusqueda (NodoArbol arbol) { boolean resul;
if (arbol == null) resul = true; else { resul = esBusqueda (arbol.iz); if (primero) primero = false; else if (arbol.clave <= ant) resul = false; if (resul) { ant = arbol.clave; resul = esBusqueda(arbol.de); } } return resul; }
}
static boolean esBusqueda (Arbol a) {
return Busqueda.esBusqueda(a.raiz); }
ESTRUCTURAS DE DATOS ÁRBOLES 159
4.3.2. Algoritmos de modificación.
4.3.2.1. Inserción.
Se trata de crear un nuevo nodo en la posición que le corresponda según el criterio de
árbol binario de búsqueda. A continuación se muestra el algoritmo.
static NodoArbol insertar (NodoArbol arbol, int dato) { NodoArbol resul = arbol; if (arbol != null) if (arbol.clave < dato) arbol.de = insertar (arbol.de, dato); else if (arbol.clave > dato) arbol.iz = insertar (arbol.iz, dato); else System.out.println ("la clave ya existe"); else resul = new NodoArbol (dato); return resul; } public void insertar (int dato) { raiz = insertar (raiz, dato); }
4.3.2.2. Eliminación.
La eliminación de un nodo en un árbol binario de búsqueda implica una
reorganización posterior del mismo con el objeto de que una vez eliminado el nodo el árbol
mantenga su naturaleza de búsqueda. Para ello se procede de la manera siguiente:
Figura 4.8. Árbol binario de búsqueda
Primero localizamos el nodo a eliminar. Una vez encontrado, tenemos tres posibles
casos:
1. Tenemos que borrar un nodo hoja: procedemos a borrarlo directamente. Por ejemplo, si
en el árbol de la figura 4.11, queremos borrar la clave 13, el resultado sería la figura
Figura 4.9. Borrado de un nodo hoja en un árbol binario de búsqueda
15
10 20
2 12 17 25
1 7 13 18 23
15
10 20
2 12 17 25
1 7 18 23
ESTRUCTURAS DE DATOS ÁRBOLES 161
static class Eliminar { static NodoArbol eliminar2Hijos (NodoArbol arbol, NodoArbol p) { NodoArbol resul; if (arbol.de != null) { resul = arbol; arbol.de = eliminar2Hijos (arbol.de, p); } else { p.clave = arbol.clave; resul = arbol.iz; } return resul; } static NodoArbol eliminarElemento (NodoArbol arbol, int elem) { NodoArbol p;
if (arbol != null) if (arbol.clave > elem) arbol.iz = eliminarElemento (arbol.iz, elem); else if (arbol.clave < elem) arbol.de = eliminarElemento (arbol.de, elem); else { p = arbol; if (arbol.iz == null) arbol= arbol.de; else if (arbol.de == null) arbol = arbol.iz; else arbol.iz = eliminar2Hijos (arbol.iz, p); } else System.out.println ("la clave buscada no existe"); return arbol; } } public void eliminar (int elem) { raiz = Eliminar.eliminarElemento (raiz, elem); }
4.3.3. Ejemplo.
Dado un árbol binario de búsqueda y una lista enlazada por medio de punteros,
implementar un algoritmo en Java que, recibiendo al menos un árbol y una lista ordenada
ascendentemente, determine si todos los elementos del árbol están en la lista.
Implementaremos un método booleano con el arbol y la lista como argumentos.
Como la lista está ordenada ascendentemente, y el árbol binario es de búsqueda,
realizaremos un recorrido del árbol en orden central, parando en el momento en que
detectemos algún elemento que existe en el árbol, pero no en la lista.
162 ÁRBOLES ESTRUCTURAS DE DATOS
static boolean estaContenido (NodoArbol arbol, NodoLista lista) {
boolean seguir, saltar;
if (arbol == null) seguir = true; else { seguir = estaContenido (arbol.iz, lista); if (seguir && (lista != null)) if (arbol.clave < lista.clave) seguir = false; else { saltar = true; while ((lista != null) && saltar) if (arbol.clave == lista.clave) saltar = false; else lista = lista.sig; if (!saltar) seguir = estaContenido (arbol.de, lista.sig); else seguir = false; } else seguir = false; } return seguir;
}
static boolean estaContenido (Arbol a, Lista l) {
return estaContenido (a.raiz, l.inicio);
}