Tabla de contenidos
El objetivo de este ejercicio es crear su primer árbol binario enlazado y familiarizarse con sus métodos básicos: inserción, extracción, recorridos (pre-order, in-order y post-order), comparación de árboles y búsqueda.
De acuerdo a la definición recursiva de árbol, un árbol binario es, o bien vacío o consta de un elemento raíz, con un nodo que contiene exactamente dos árboles hijo (subárbol izquierdo y subárbol derecho).
Según esta definición se necesitan dos clases para definir un árbol binario:
La que define el tipo de datos del árbol binario en sí mismo
Y la que define sus nodos.
La primera representa al árbol y define su API pública, la segunda permite su implementación mediante enlaces o referencias entre árboles y no debe ser parte de la API pública.
A continuación se muestra la interfaz BTree
que usará
en esta práctica.
public interface BTree<E> { static final int LEFT = 0; static final int RIGHT = 1; boolean isEmpty(); E getInfo() throws BTreeException; BTree<E> getLeft() throws BTreeException; BTree<E> getRight() throws BTreeException; void insert(BTree<E> tree, int side) throws BTreeException; BTree<E> extract(int side) throws BTreeException; String toStringPreOrder(); String toStringInOrder(); String toStringPostOrder(); String toString(); // pre-order int size(); int height(); boolean equals(BTree<E> tree); boolean find(BTree<E> tree); }
Añada comentarios a la interfaz explicando para qué sirven sus atributos. Explique también qué hace y para qué sirve cada método, sus argumentos de entrada y sus valores de retorno. Explique también en qué condiciones se lanzan excepciones.
Suponga que trabaja para el departamento de desarrollo de una agencia de viajes. La agencia oferta una serie de vuelos a diferentes ciudades del mundo. En cada ciudad el cliente puede elegir entre dos destinos diferentes, aunque por motivos climatológicos, algunos de estos viajes están cerrados.
Suponga también que su departamento ya dispone de una
implementación de la interfaz del apartado anterior, y se llama
LBTree
(Linked Binary Tree). Le piden hacer una
aplicación para gestionar la oferta de viajes anteriormente
explicada.
Responda (en papel) a las siguientes preguntas sobre la aplicación, suponiendo que se tiene un árbol de viajes como el de la siguiente figura:
¿Cúal es el tamaño del árbol cuya raíz es Madrid?
¿Cúal es la lista de elementos del árbol cuya raíz es Madrid (en pre-order)?
¿Cúal es el tamaño del árbol cuya raíz es Moscú?
¿Cúal es la lista de los elementos del árbol cuya raíz es Moscú (en post-order)?
Cuando haya respondido a las preguntas por escrito, sigua leyendo.
Programe (también en papel) el método main de esta aplicación que crea el árbol de la figura e imprime por salida estándar las respuestas a las preguntas anteriores.
Puede suponer que los elementos almacenados en el árbol son
String
s, que contienen el nombre de las ciudades y que la clase
LBTree tiene dos constructores: LBTree(E info)
y
LBTree()
.
Preste especial atención al manejo de excepciones y a introducir correctamente unos árboles en otros para que el resultado sea como el de la figura.
A continuación se muestra un ejemplo de ejecución de esta aplicación:
Warning: SPOILERS BELLOW! ; java Cities Madrid tree size = 11 Madrid tree = Madrid Paris Brussels Hanoi Kiev Berlin Lima Moscow Prague Seoul Taipei Moscow tree size = 4 Moscow tree = Seoul Taipei Prague Moscow
Programe la clase LBTree<E>
que implemente la
interfaz BTree
, la clase LBNode<E>
que la acompaña y la clase BTreeException
.
Empieze por la clase BTreeException
, que hereda de
Exception
y simplemente tiene un constructor que
recibe un mensaje, de tipo String.
A continuación programe la clase LBNode<E>
partiendo de la siguiente plantilla:
class LBNode<E> { /* your attributes here */ LBNode(E info, BTree<E> left, BTree<E> right) { /* your code here */ } E getInfo() { /* your code here */ } void setInfo(E info) { /* your code here */ } BTree<E> getLeft() { /* your code here */ } void setLeft(BTree<E> left) { /* your code here */ } BTree<E> getRight() { /* your code here */ } void setRight(BTree<E> right) { /* your code here */ } }
Termine por programar la clase LBTree<E>
, que
debe tener dos constructores:
LBTree()
: crea un árbol vacío, esto es un árbol
con la raíz a null
.
LBTree(E info)
: crea un árbol de tamaño 1, con un
nodo que almacena info
y que contiene dos árboles
vacíos.
Puede probar su práctica con la clase LBTreeTest.java, que
realiza pruebas exhaustivas de cada uno de los métodos de la clase
LBTree
.
Para entender los errores que le descubrirá esta clase, tendrá primeramente que entender cómo funciona el ćodigo de las pruebas.
A continuación se muestra un ejemplo de ejecución de esta clase,
suponiendo que se dispone de una implementación correcta de
LBTree
:
; java LBTreeTest testing isEmpty: OK testing getInfo: OK testing insert: OK testing getLeft: OK testing getRight: OK testing extract: OK testing toStringPreOrder: OK testing toStringInOrder: OK testing toStringPostOrder: OK testing toString: OK testing size: OK testing height: OK testing equals: OK testing find: OK
Volviendo al ejemplo de la agencia de viajes. Suponga que otro
departamento de su empresa usa una implementación diferente de
BTree
, que ellos llaman ABTree
, ya que
sus nodos (que ellos llaman ABnode
) utilizan un array
para almacenar los subárboles, en lugar de usar referencias como
usted.
La raíz de sus árboles, es por tanto una referencia a
ABNode
, que además se llama "wurzel" (en lugar de
"root", ya que este otro departamento está en Alemania).
Responda a las siguientes preguntas razonadamente:
¿Podrá insertar un nuevo árbol de tipo ABTree
con
unas cuantas ciudades nuevas, que le pase el otro
departamento, a la izquierda de Bruselas en su árbol
LBTree
que comenzaba en Madrid?
Si consigue insertarlo, ¿podría buscar un subárbol mixto (con
nodos LBNode
y nodos ABnode
) en el
árbol resultado de la inserción?
Estudiar otro tipo de árboles más genéricos que los binarios, proponiendo un ejemplo concreto de aplicación: los sistemas de ficheros. Se pretende que los alumnos aprendan a generalizar las operaciones que han visto en el caso de árboles binarios para aplicarlas al caso de árboles N-arios.
Hasta ahora hemos trabajado con árboles binarios, pero no son el
único tipo
de árbol existente. En algunas ocasiones necesitamos
árboles
más flexibles que nos permitan tener, por cada nodo, un número
N de
nodos hijo
que no tiene por qué ser exactamente dos, y que puede
ser diferente para
cada
nodo. Esta estructura de datos es lo que se
conoce como árbol N-ario y se
muestra en la
figura 1
. Cada nodo del árbol contiene una referencia a la información que se
quiere almacenar (
info
), y un conjunto de referencias a los nodos hijo (
children
). Para acceder a todos los nodos del árbol tan sólo necesitamos una
referencia a su nodo raíz (
root
), tal y como ocurría en el caso de árboles binarios.
Figura 1. Representación gráfica de un árbol N-ario
En este ejercicio vamos a ver un ejemplo en el que se necesitan árboles N-arios: los sistemas de ficheros. Supongamos que tenemos el siguiente sistema de ficheros (también conocido como árbol de directorios):
C: |_Archivos de programa |_Eclipse |_Java |_Mis documentos |_Imagenes |_Musica |_Videos |_ProgSis |_Proyecto |_Modulo1 |_Modulo2
Tal y como su nombre indica, todos los
directorios o carpetas (en
nuestro caso, para simplificar, vamos a ignorar los
archivos) se
organizan en forma de árbol: existe un nodo raíz (C:)
que contiene
varias carpetas, cada una de las cuales contiene a su
vez más
carpetas, y así sucesivamente. Para crear y manejar este sitema de ficheros,
vamos a
tomar la estructura genérica mostrada en la
figura 1
, y la vamos a particularizar para adaptarla al caso que nos ocupa.
Los nodos de la imagen serán nuestras carpetas. Cada carpeta está
representada mediante un objeto de tipo
Folder
. Cada uno de estos objetos posee dos atributos:
name
es un atributo de tipo
String
que almacena el nombre de la carpeta
subdirectories
es un atributo de tipo
ArrayList
que almacena los subdirectorios (objetos de tipo
Folder
) que contiene la carpeta.
Para representar el sistema de ficheros, haremos uso de una clase
FileSystem
que desempeña el papel de árbol, puesto que es la que contiene una
referencia al nodo raíz (atributo
root
de tipo
Folder
), desde el cual se puede acceder al resto de carpetas (nodos) del
sistema de ficheros.
La figura 2 representa el sistema de ficheros del ejemplo previo representado mediante los objetos Java que vamos a manejar.
Figura 2. Representación gráfica de un sistema de ficheros modelado con objetos Java
Para facilitar la realización de este ejercicio, se proporciona parte
de la
clase
Folder
, así como la estructura de la clase
FileSystem
, que se pueden descargar en los siguientes enlaces
:
En primer lugar, se va a practicar el uso de
objetos de la clase ArrayList
, para
lo que se pide implementar los siguientes
métodos de la clase Folder
:
public Folder addSubdirectory(String folderName)
: añade una nueva
carpeta, con nombre
folderName
, a la colección de subdirectorios y devuelve una
referencia a la misma.
public void printSubdirectories()
: imprime en pantalla el nombre de
todos los subdirectorios de la
carpeta en el formato: subdirectorio1
subdirectorio2 ...
subdirectorioN. Sólo se imprimirán los
subdirectorios, sin
tener en
cuenta los subdirectorios que pudiera tener cada uno de éstos.
Antes de seguir avanzando asegúrate de que los métodos funcionan
correctamente. Para ello, crea una clase
FileSystemTest.java
para comprobar el funcionamiento.
Una vez tenemos claro cómo funcionan los objetos de tipo
Folder
y cómo
recorrer el objeto de la clase
ArrayList
que contiene los subdirectorios
(nodos hijo) de cada carpeta, nos
centraremos en el sistema de ficheros en
sí. Para
ello se pide
implementar los siguientes métodos de la clase
FileSystem
(además de realizar las pruebas necesarias para comprobar su correcto
funcionamiento con la clase
FileSystemTest
que creaste previamente):
public Folder searchFolder(Folder root, String folderName)
: busca
la carpeta cuyo nombre es
folderName
en todo el árbol. Si la carpeta existe, se
devuelve una referencia a la misma, o
null
en caso contrario.
Pista: este método es recursivo ya que, para
cada carpeta, hay que
recorrer
todos sus subdirectorios, los
subdirectorios de éstos y así
sucesivamente
hasta cubrir todo el
árbol. Tal vez sea útil utilizar un método auxiliar
private Folder searchFolder(Folder root, String folderName)
, donde root
será el nodo raíz de cada
subárbol sobre el que se vaya a hacer la búsqueda de la
carpeta especificada.
public Folder addNewFolder(String parentFolderName, String
newFolderName)
: crea una nueva carpeta de nombre
newFolderName
, y la añade como
subdirectorio de la carpeta cuyo nombre es
parentFolderName
. Si ya
existe una carpeta en el sistema de ficheros con el nombre
newFolderName
,
o si la carpeta
parentFolderName
no existe, no se debe hacer nada y se
devuelve
null
. En caso contrario, se crea y añade la nueva carpeta
y se devuelve
una referencia a la misma.
public void printFileSystem()
: imprime la estructura del sistema de
ficheros con el siguiente
formato:
C: |_Archivos de programa |_Eclipse |_Java |_Mis documentos |_Imagenes |_Musica |_Videos |_ProgSis |_Proyecto |_Modulo1 |_Modulo2
Pista: este método es también recursivo. Además, el número de
espacios en blanco
antes de cada nombre tiene mucho que ver con el
nivel del árbol en el
que se
encuentra la carpeta. Tal vez pueda ser
útil crear un método auxiliar
private void printFileSystem(Folder root, int level)
con el que se harán las llamadas recursivas.