Universidad Carlos III de Madrid

Grado en Ingeniería Telemática/Grado en Ingeniería de Sistemas de Comunicaciones

Enero-Mayo 2014

Árboles

Lab Section1. Sesión 2 (laboratorio): Árboles (I)

Exercise Section1.1. Árboles binarios

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:

  1. La que define el tipo de datos del árbol binario en sí mismo

  2. 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.

Apartado 1. La interfaz BTree

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.

Solución
public interface BTree<E> {
    // Each non-empty tree has two children, left and right.  This
    // constants will be used to tell one from the other, for example,
    // when inserting or extracting. Thanks to this we don't need to
    // duplicate the insert and extract methods into insertLeft,
    // insertRight, extractLeft and extractRight.
    //
    // The proper way to do this is by using an enum instead of static
    // final ints.
    static final int LEFT = 0;
    static final int RIGHT = 1;

    // Returns if the tree is empty.
    boolean isEmpty();

    // Return the info stored in the root of the tree, its left child
    // and its right child.
    //
    // BTreeExceptions are thrown if the tree is empty.
    E getInfo() throws BTreeException;
    BTree<E> getLeft() throws BTreeException;
    BTree<E> getRight() throws BTreeException;

    // Insert a tree at the specified child (LEFT or RIGHT).
    //
    // A BTreeException is thrown if the side is not LEFT or RIGHT or if
    // the tree where the insertion is to be made is empty
    void insert(BTree<E> tree, int side) throws BTreeException;

    // Returns and extract the specified child (LEFT or RIGHT)
    //
    // A BTreeException is thrown if the side is not LEFT or RIGHT or if
    // the tree where the extraction is to be made is empty
    BTree<E> extract(int side) throws BTreeException;

    // Generates a textual representation of the tree.
    //
    // toString defaults to pre-order.
    String toStringPreOrder();
    String toStringInOrder();
    String toStringPostOrder();
    String toString(); // pre-order

    // Returns the size of the tree (its number of nodes).
    // 0 for empty trees
    int size();
    // Returns the height of the tree (number of jumps between the root
    // and its deepest leaf).
    // -1 for empty trees.
    int height();

    // Returns if this tree is equal to another tree.
    //
    // Two trees are equal if their nodes hold equal information and if
    // they are distributed in the same way.
    boolean equals(BTree<E> tree);
    // Tells if a subtree equal to "tree" is stored in this tree.
    boolean find(BTree<E> tree);
}

Apartado 2. Ejemplo de uso

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:

  1. ¿Cúal es el tamaño del árbol cuya raíz es Madrid?

  2. ¿Cúal es la lista de elementos del árbol cuya raíz es Madrid (en pre-order)?

  3. ¿Cúal es el tamaño del árbol cuya raíz es Moscú?

  4. ¿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 Strings, 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 
Solución
public class Cities {

    public static void main(String[] args) {

        BTree<String> madrid = new LBTree<String>("Madrid");
        BTree<String> paris = new LBTree<String>("Paris");
        BTree<String> berlin = new LBTree<String>("Berlin");
        BTree<String> brussels = new LBTree<String>("Brussels");
        BTree<String> lima = new LBTree<String>("Lima");
        BTree<String> moscow = new LBTree<String>("Moscow");
        BTree<String> hanoi = new LBTree<String>("Hanoi");
        BTree<String> prague = new LBTree<String>("Prague");
        BTree<String> kiev = new LBTree<String>("Kiev");
        BTree<String> seoul = new LBTree<String>("Seoul");
        BTree<String> taipei = new LBTree<String>("Taipei");

        try {
            prague.insert(seoul, BTree.LEFT);
            prague.insert(taipei, BTree.RIGHT);
            moscow.insert(prague, BTree.LEFT);
            berlin.insert(lima, BTree.LEFT);
            berlin.insert(moscow, BTree.RIGHT);
            hanoi.insert(kiev, BTree.RIGHT);
            brussels.insert(hanoi, BTree.LEFT);
            paris.insert(brussels, BTree.LEFT);
            madrid.insert(paris, BTree.LEFT);
            madrid.insert(berlin, BTree.RIGHT);
        } catch (BTreeException e) {
            System.err.println("ERROR: " + e);
            return;
        }

        System.out.println("Madrid tree size = " + madrid.size());
        System.out.println("Madrid tree = " + madrid);
        System.out.println("Moscow tree size = " + moscow.size());
        System.out.println("Moscow tree = " + moscow.toStringPostOrder());
    }
}

Apartado 3. La clase LBTree y la clase LBNode

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:

  1. LBTree(): crea un árbol vacío, esto es un árbol con la raíz a null.

  2. 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
Solución
public class BTreeException extends Exception {
    public BTreeException(String msg) {
        super(msg);
    }
}
class LBNode<E> {
    private E info;
    private BTree<E> left;
    private BTree<E> right;

    LBNode(E info, BTree<E> left, BTree<E> right) {
        this.left = left;
        this.right = right;
        this.info = info;
    }

    E getInfo() {
        return info;
    }

    void setInfo(E info) {
        this.info = info;
    }

    BTree<E> getLeft() {
        return left;
    }

    void setLeft(BTree<E> left) {
        this.left = left;
    }

    BTree<E> getRight() {
        return right;
    }

    void setRight(BTree<E> right) {
        this.right = right;
    }
}
public class LBTree<E> implements BTree<E> {
    private LBNode<E> root;

    public LBTree() {
        root = null;
    }

    public LBTree(E info) {
        root = new LBNode<E>(info, new LBTree<E>(), new LBTree<E>());
    }

    public boolean isEmpty() {
        return (root == null);
    }

    public E getInfo() throws BTreeException {
        if (isEmpty()) {
            throw new BTreeException("empty trees do not have info");
        }
        return root.getInfo();
    }

    public BTree<E> getLeft() throws BTreeException {
        if (isEmpty()) {
            throw new BTreeException("empty trees do not have a left child");
        }
        return root.getLeft();
    }

    public BTree<E> getRight() throws BTreeException {
        if (isEmpty()) {
            throw new BTreeException("empty trees do not have a right child");
        }
        return root.getRight();
    }

    public void insert(BTree<E> tree, int side) throws BTreeException {
        if (isEmpty()) {
            throw new BTreeException("cannot insert on an empty tree");
        }
        if (side == BTree.LEFT) {
            root.setLeft(tree);
        } else if (side == BTree.RIGHT) {
            root.setRight(tree);
        } else {
            throw new BTreeException("Invalid side argument");
        }
    }

    public BTree<E> extract(int side) throws BTreeException {
        BTree<E> retval;
        try {
            if (side == BTree.LEFT) {
                retval = getLeft();
                root.setLeft(new LBTree<E>());
            } else if (side == BTree.RIGHT) {
                retval = getRight();
                root.setRight(new LBTree<E>());
            } else {
                throw new BTreeException("Invalid side argument");
            }
        } catch (BTreeException e) {
            throw new BTreeException("cannot extract from an empty tree");
        }
        return retval;
    }

    public int size() {
        try {
            return 1 + getLeft().size() + getRight().size();
        } catch (BTreeException e) {
            return 0;
        }
    }

    public String toStringPreOrder() {
        try {
            return getInfo().toString() + " " +
                getLeft().toStringPreOrder() +
                getRight().toStringPreOrder();
        } catch (BTreeException e) {
            return "";
        }
    }

    public String toStringInOrder() {
        try {
            return getLeft().toStringInOrder() +
                getInfo().toString() + " " +
                getRight().toStringInOrder();
        } catch (BTreeException e) {
            return "";
        }
    }

    public String toStringPostOrder() {
        try {
            return getLeft().toStringPostOrder() +
                getRight().toStringPostOrder() +
                getInfo().toString() + " ";
        } catch (BTreeException e) {
            return "";
        }
    }

    public String toString() {
        return toStringPreOrder();
    }

    public int height() {
        try {
            int leftHeight = getLeft().height();
            int rightHeight = getRight().height();
            return 1 + Math.max(leftHeight, rightHeight);
        } catch (BTreeException e) {
            return -1;
        }
    }

    public boolean equals(BTree<E> tree) {
        try {
            return (getInfo().equals(tree.getInfo()) &&
                    getLeft().equals(tree.getLeft()) &&
                    getRight().equals(tree.getRight()));
        } catch (BTreeException e) {
            if (this.isEmpty() && tree.isEmpty()) {
                return true;
            } else {
                return false;
            }
        }
    }

    public boolean find(BTree<E> tree) {
        if (this.equals(tree)) {
            return true;
        } else {
            try {
                if (getLeft().find(tree)) {
                    return true;
                } else {
                    return (getRight().find(tree));
                }
            } catch (BTreeException e) {
                return false;
            }
        }
    }
}

Apartado 4. Compatibilidad con otros árboles binarios

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:

  1. ¿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?

  2. Si consigue insertarlo, ¿podría buscar un subárbol mixto (con nodos LBNode y nodos ABnode) en el árbol resultado de la inserción?

Solución

La respuesta a las dos preguntas es "Sí", ya que nuestra clase LBTree inserta, extrae y busca árboles genéricos que cumplan la interfaz BTree, no haciendo ninguna suposición acerca de cómo están organizados esos árboles internamente.

Esto no es un mérito de nuestra implementación, es mérito del propio interfaz BTree, que nos obliga, por ejemplo, a retornar árboles genéricos BTree cuando extraémos, en lugar de nuestros propios árboles LBTree.

Homework Section2. Actividades para casa

Exercise Section2.1. Árboles N-arios y sistemas de ficheros

Objetivo

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.

Árboles N-arios y sistemas de ficheros

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

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

Representación gráfica de un sistema de ficheros modelado con objetos Java


Ejercicio

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 :

  1. 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.

  2. 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):

  1. 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.

  2. 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.

  3. 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.

Solución
import java.util.ArrayList;

/**
 * Class representing a folder.
 * 
 * @author Dpt. of Telematic Engineering, University Carlos III of Madrid
 * 
 */
public class Folder {

    /** Name of the folder */
    private String name;

    /**
     * ArrayList containing the subdirectories stored by this folder
     */
    private ArrayList<Folder> subdirectories;

    /**
     * Constructor
     */
    public Folder() {
        this("default folder");
    }

    /**
     * Constructor
     * 
     * @param name
     *          Name of the folder.
     */
    public Folder(String name) {
        this.name = name;
        this.subdirectories = new ArrayList<Folder>();
    }

    /**
     * Get the name of the folder.
     * 
     * @return a String reference to the name of the folder.
     */
    public String getName() {
        return this.name;
    }

    /**
     * Set the name of the folder.
     * 
     * @param name
     *          New name of the folder.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Get the vector of subdirectories.
     * 
     * @return a reference to the vector containing the subdirectories.
     */
    public ArrayList<Folder> getSubdirectories() {
        return this.subdirectories;
    }

    /**
     * Add a new folder as subdirectory of this folder.
     * 
     * @param folderName
     *          Name of the new folder.
     * @return a reference to the new folder.
     */
    public Folder addSubdirectory(String folderName) {
        Folder newFolder = new Folder(folderName);
        subdirectories.add(newFolder);
        return newFolder;
    }

    /**
     * Print in screen the subdirectories of the folder with the format:
     * subdirectory1 subdirectory2 ... subdirectoryN
     */
    public void printSubdirectories() {
        for (int i = 0; i < subdirectories.size(); i++) {
            System.out.print(subdirectories.get(i).getName() + " ");
        }
        System.out.println();
    }
}
/**
 * Class representing the file system.
 * 
 * @author Dpt. of Telematic Engineering, University Carlos III of Madrid
 * 
 */
public class FileSystem {

    /** File system (tree) root */
    private Folder root;

    /**
     * Constructor.
     */
    public FileSystem() {
        this("default");
    }// end constructor

    /**
     * Constructor.
     * 
     * @param rootFolderName
     *          Name of the root folder
     */
    public FileSystem(String rootFolderName) {
        root = new Folder(rootFolderName);
    }// end constructor

    /**
     * Search the folder specified as parameter in the file system, and return a
     * reference to the folder if it exists or null otherwise.
     * 
     * @param folderName
     *          Name of the folder to search.
     * @return a reference to the folder if it is in the file system, or null
     *         otherwise.
     */
    public Folder searchFolder(String folderName) {
        // It calls the auxiliary search method
        return searchFolder(root, folderName);
    }// end searchFolder

    /**
     * Search the folder specified as parameter in the file system, and return a
     * reference to the folder if it exists or null otherwise. This method is
     * recursive and is applied over each subtree by specifying the root of that
     * subtree in each case.
     * 
     * @param rootFolder
     *          Root of each subtree over which the search method is applied.
     * @param folderName
     *          Name of the folder to be searched.
     * @return a reference to the folder being searched if it is in the file
     *         system, or null otherwise.
     */
    private Folder searchFolder(Folder rootFolder, String folderName) {
        // Reference to the folder we are searching for
        Folder searchedFolder = null;
        // Boolean to indicate that we have found the folder
        boolean found = false;

        if (rootFolder != null) { // Check if the tree is not empty
            if (rootFolder.getName().equals(folderName)) {
                // If the current folder is the one being searched,
                // save it in the variable searchedFolder...
                searchedFolder = rootFolder;
            } else {
                // ...else, apply the search method over each subtree
                // which root is each of the subdirectories of the
                // current folder, until it is found
                for (int i = 0; i < rootFolder.getSubdirectories().size() && !found; i++) {
                    searchedFolder = searchFolder(rootFolder.getSubdirectories().get(i),
                                                  folderName);
                    if (searchedFolder != null) {
                        // if we have found the folder, we stop searching
                        found = true;
                    }
                }
            }
        }
        return searchedFolder;
    }// end searchFolder

    /**
     * Add a new folder of the specified name, as a subdirectory of the folder
     * specified in the first parameter. If there exists a folder with name
     * newFolderName in the file system, or if parentFolderName does not exist,
     * the method does nothing and returns null. Otherwise it returns a reference
     * to the new folder.
     * 
     * @param parentFolderName
     *          Name of the parent folder.
     * @param newFolderName
     *          Name of the new folder.
     * @return a reference to the new folder, or null if there exists a folder
     *         with newFolderName in the file system of if the parent folder does
     *         not exist.
     */
    public Folder addNewFolder(String parentFolderName, String newFolderName) {
        // Search the parent folder
        Folder parentFolder = searchFolder(parentFolderName);
        // Search if there exists a folder with newFolderName name
        Folder newFolder = searchFolder(newFolderName);

        // If the parent folder exists and no newFolderName exists in the file
        // system...
        if (parentFolder != null && newFolder == null) {
            // ...then add the directory...
            newFolder = parentFolder.addSubdirectory(newFolderName);
        } else { // ...otherwise...
            // ...return null and show some message in screen.
            newFolder = null;
            if (parentFolder == null) {
                System.out.println("The folder cannot be added because "
                                   + "the parent folder \"" + parentFolderName + "\" does not exist.");
            } else {
                System.out.println("The folder cannot be added because "
                                   + "there is already another folder with the same name");
            }
        }
        return newFolder;
    }// end addNewFolder

    /**
     * Print the file system structure in screen.
     */
    public void printFileSystem() {
        // Call an auxiliary recursive method.
        printFileSystem(0, root);
    }// end printFileSystem

    /**
     * Print the file system structure in screen. This method is recursive, and is
     * called over each subtree by specifying the root of the corresponding
     * subtree.
     * 
     * @param level
     *          Tree level where the root of the subtree is.
     * @param root
     *          Root folder of the subtree over which the method is applied.
     */
    private void printFileSystem(int level, Folder root) {
        // Check if the current root is not null
        if (root != null) {
            if (level > 0) {
                // If level is greater than 0 (the root is not
                // the root of whole file system)...
                for (int i = 0; i < level; i++) {
                    // ...print as many white spaces as levels...
                    System.out.print("  ");
                }
                // ...and print the symbol representing the directory...
                System.out.print("|_");
            }
            // ...print the name of the folder
            System.out.println(root.getName());
            for (int j = 0; j < root.getSubdirectories().size(); j++) {
                // Take each subdirectory as root of a subtree, and
                // apply the method over that subtree
                printFileSystem(level + 1, root.getSubdirectories().get(j));
            }
        }
    }// end printFileSystem
}
/**
 * Class for testing purposes.
 * 
 * @author Dpt. of Telematic Engineering, University Carlos III of Madrid
 * 
 */
public class FileSystemTest {

    /**
     * Testing main.
     */
    public static void main(String[] args) {
        /*
         * First, we check the proper working of Folder class
         */
        // Create several folder objects representing
        // different folders
        Folder folderC = new Folder("C");

        // Check the getName method
        System.out.println(folderC.getName());

        // Check the setName method
        folderC.setName("C:");
        System.out.println(folderC.getName());

        // Check the printSubdirectories when there are
        // no subdirectories added
        folderC.printSubdirectories();

        // Add one subdirectory
        folderC.addSubdirectory("Program Files");

        // Check the printSubdirectories when there is
        // only one subdirectory added
        folderC.printSubdirectories();

        // Add some more subdirectories
        folderC.addSubdirectory("My Documents");
        folderC.addSubdirectory("Windows");

        // Check the printSubdirectories when there are
        // several subdirectories added
        folderC.printSubdirectories();

        /*
         * Once we have checked the proper working of the Folder class, we test the
         * FileSystem class.
         */
        // Create the file system
        FileSystem fs = new FileSystem("C:");
        fs.printFileSystem();

        // Add a new folder not contained in the file system
        fs.addNewFolder("C:", "Program Files");
        fs.printFileSystem();

        // Add a new folder
        fs.addNewFolder("Program Files", "Java");
        fs.printFileSystem();

        // Add a new folder as subdirectory of a folder
        // which is not in the file system
        fs.addNewFolder("Windows", "MyFolder");

        // Add several folder, and see if they are
        // printed as expected
        fs.addNewFolder("C:", "My Documents");
        fs.addNewFolder("My Documents", "Images");
        fs.addNewFolder("My Documents", "Video");
        fs.addNewFolder("My Documents", "Music");
        fs.addNewFolder("My Documents", "Text Documents");
        fs.addNewFolder("Music", "Old Music");
        fs.printFileSystem();
    }
}