Universidad Carlos III de Madrid

Ingeniería de Telecomunicación

Enero-Mayo 2010 / January-May 2010

Trees

Lab Section1.  Session 2 (lab): Trees (I)

Exercise Section1.1. Binary Trees

The goal of this exercise is to create your first linked binary tree, and get to know its methods: insertion, extraction, traverse the tree (pre-order, in-order, post-order) as well as comparison of trees and searches.

If you recall the recursive definition of a tree, a binary tree is empty or consists of a root element, with a node containing exactly two children trees (left subtree and right subtree).

Following this definition, you need two classes to define a binary tree:

  1. The class defining the binary tree datatype itself.

  2. The class defining its nodes.

The first one represents the tree and defines the public API. The second class allows the implementation through links or references among trees, and must not be part of the public API.

Section 1. BTree Interface

The BTree interface that will be used in this assignment is shown below.

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);
}

Add comments to the interface explaining what the attributes are for. Explain also what each method does and what it is useful for, the input arguments and the return values. Explain as well under which conditions exceptions are thrown.

Solution
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);
}

Section 2. Use Example

Suppose you work for the development department of a travel agency. The agency offers several flights to different cities of the world. In each city, the client can choose between two different destinations. However, because of weather issues, some of these flights are closed.

Suppose also that your department already has an implementation of the interface described in the previous section, called LBTree (LinkedBinaryTree). You are asked to create an application to manage the flight offers explained before.

Answer (in a piece of paper) the following questions regarding the applications. Suppose that you have a flights tree like the following one:

  1. What is the size of the tree which root is Madrid?

  2. What is the elements list of the tree which root is Madrid (in pre-order)?

  3. What is the size of the tree which root is Moscow?

  4. What is the elements list of the tree which root is Moscow (in post-order)?

Once you have answered the questions, keep on reading.

Write (once again, in a piece of paper) the main method of this application. This main method creates the tree shown in the picture above, and prints in the standard output the answers to the previous questions.

You can suppose that the elements stored in the tree are Strings, which contain the name of the cities. You can also suppose that the LBTree class has two constructors: LBTree(E info) and LBTree().

Pay special attention to the management of the exceptions, an also on how to make the tree insertion, so that the result is the one in the figure.

You can see an example of the execution of the application below:

















     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 
Solution
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());
    }
}

Section 3. The LBTree and LBNode classes

Write the LBTree<E> class, which implements the BTree interface. Code also the LBNode<E> class needed by LBTree<E> class, as well as the BTreeException class.

Start with the BTreeException class. This class extends from Exception, and it simply has a constructor with an only input argument, of String type.

Next, write the LBNode<E> class starting from the following template:

  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 */
    }
}

Finally, write the LBTree<E> class, which must have two constructors:

  1. LBTree(): creates an empty tree, i.e., a tree which root is null.

  2. LBTree(E info): creates a tree of size 1, with a node that stores info, a two empty subtrees.

You can test your solution with the LBTreeTest.java class, which performs thorough tests with every method of the LBTree class.

In order to understand the errors shown by this class, you need to understand first how the testing code works.

Next, an execution example of this class is shown, supposing that there already exists a working implementation of 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
Solution
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;
            }
        }
    }
}

Section 4. Compatibility with other binary trees

Let's go back to the travel agency example. Suppose that another department of your business uses a different implementation of BTree, called ABTree. In this implementation, the nodes (called ABNode) use an array to store the subtrees, instead of using references like in our implementation.

Therefore, their trees root is a reference to an ABNode, which is besides called "wurzel" (instead of "root"), since this department is in Germany.

Answer the following questions:

  1. Could you insert a new tree of type ABTree with some new cities, which comes from the other deparment, to the left of Brussels in your LBTree that started in Madrid?

  2. If you could, could you search for a mixed subtree (with LBNode and ABNode nodes) in the tree resulting from the previous insertion?

Solution

The answer to both questions is yes. That's because our LBTree class inserts, extracts and searches generic trees that comply with BTree, without any hypothesis on how the trees are internally organized.

This is not a merit of our implementation. The merit comes from the BTree interface itself, which , for instance, makes us return generic BTree trees as a result of an extraction, instead of returning our own LBTree.

Homework Section2.  Homework

Exercise Section2.1.  N-ary trees and file systems

Objective

To study another kind of more generic trees than binary ones, by proposing an application example: file systems. The students are supposed to learn how to generalize the operations with binary trees in order to apply them to the case of N-ary trees.

N-ary trees and file systems

Up to now we have been working with binary trees, but they are not the only kind of trees we can find. Sometimes we need more flexible trees that allow us to have, for each node, N children nodes, where N has not to be exactly two and can be a different value for each node. This data structure is known as N-ary tree, and it is shown in Figure 1. As you can see, each tree node contains a reference to the information stored in it (info), as well as a set of references to the children nodes (children). In order to access every tree node we just need a reference to the root node (root), as in the case of binary trees.

Figure 1.  Graphical representation of a N-ary tree

Graphical representation of a N-ary tree

In this exercise we are going to see an example in which N-ary trees are necessary: file systems. Let's suppose that we have the following file system (also known as directory tree):

C:
  |_Program Files
    |_Eclipse
    |_Java
  |_My Documents
    |_Images
    |_Music
    |_Videos
  |_ProgSis
    |_Project
      |_Module1
      |_Module2

As its name suggests, every directory or folder (in our case we are going to simplify the problem by ignoring the files) is stored following a tree structure: there is a root folder (C:) which contains many folders, each one of the containing many more and so on. In order to create and manage a file system, we are going to take the generic data structure shown in Figure 1, and we are going to make it specific to the case we are studying. The nodes in the image will be our folders. Each folder will be represented by an object of the Folder class. This class has two attributes:

  • name is an attribute of type String which stores the folder's name

  • subdirectories is an attribute of type ArrayList which stores the subdirectories (objects of type Folder) the folder contains.

In order to represent the file system, we will use the FileSystem class, which plays the role of tree since it is in charge of storing the reference to the root folder (root attribute of type Folder).

Figure 2 represents the sample file system shown before by using the Java objects we will be dealing with during the exercise.

Figure 2.  Graphical representation of a file system modeled with Java objects

Graphical representation of a file system modeled with Java objects


Exercise

In order to make the exercise easier, we provide part of the Folder class, as well as the structure of the FileSystem class. Both files can be downloaded from the following links:

First of all you will practice the use of ArrayList objects (if you do not know how to use them, check the API). In order to do that you have to implement the following methods of Folder class:

  1. public Folder addSubdirectory(String folderName) : adds a new folder, which name is folderName , to the set of subdirectories and return a reference to the new folder.

  2. public void printSubdirectories() : prints in the screen the name of every subdirectory contained by the folder, with the following format: subdirectory1 subdirectory2 ... subdirectoryN. Only direct subdirectories (children nodes) will be printed, ignoring the subdirectories that can be contained in each one of them.

Before we move on, make sure that the methods you just implemented work as expected. In order to do that, create a FileSystemTest.java class and check if they behave as supposed to.

Once we have clear how Folder objects behave and how to traverse an object of ArrayList class, we can start dealing with the file system itself. You have to implement the following methods of the FileSystem class (besides making the necessary tests in order to check the proper working of each one with the FileSystemTest class you created earlier):

  1. public Folder searchFolder(Folder root, String folderName) : search the folder which name is folderName in the whole tree. If the folder exists, it returns a reference to it, or null otherwise. (Hint: this method is recursive since, for each foder, you have to search in its subdirectories, in the subdirectories of these ones and so on until you find the folder or you have checked every node in the tree. It might be useful to use an auxiliary method private Folder searchFolder(Folder root, String folderName) , where root is the root folder of each subtree over which the search is going to be applied).

  2. public Folder addNewFolder(String parentFolderName, String newFolderName): creates a new folder which name is newFolderName, and adds it to the subdirectories set of the folder with name parentFolderName. If there is already a folder in the tree with the name newFolderName or if the folder parentFolderName does not exist, then the method does nothing and null is returned. Otherwise, the new folder is created and added, and a reference to it returned.

  3. public void printFileSystem() : prints in screen the file system structure with the following format:

    C:
     |_Program Files
       |_Eclipse
       |_Java
     |_My Documentos
       |_Images
       |_Music
       |_Videos
     |_ProgSis
       |_Project
         |_Module1
         |_Module2
    

    (Hint: this method is also recursive. Moreover, the number of white spaces before each name has much to do with the tree level where the folder is stored. It might be useful to use an auxiliary method private void printFileSystem(Folder root, int level) with which the recursive calls will be made.

Solution
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();
    }
}