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 Tree: implementation

Objective

Teaching the student how to implement and use binary trees.

Exercise

This exercise will use the recursive definition of a Binary Tree in order to design a Java class that implements such Binary tree. According with the definition, a Binary Tree can be both empty or consisting of a root element and, at most, two child trees (left and right).

Note

Remember that two classes, BTree and BNode, are required in order to represent properly the empty tree.

  • Build a BTree class based on the previous recursive definition of the Binary Tree. Implement a constructor without parameters that initializes an empty tree. Program an additional constructor that receives just a single parameter, the information (Object class type) to be stored in the root node of the tree.

  • Build a BNode class based on the previous recursive definition, which encapsulates the information to be stored in the node as well as the references to the left and right children. Implement a constructor that receives just a single parameter, the information to be stored, (Object class type). Program also the corresponding accessor methods (get and set).

  • Implement a BTreeException exception's class with a constructor that receives a message (a string of characters) as parameter.

Exercise Section1.2.  Insertion and deletion of elements into a binary tree

Objective

Student's training in the use of insertion methods for inserting and deleting in binary trees.

Exercise

Given a integer binary tree, implement two methods that allow inserting a subtree into the tree and to extract a subtree from it.

  • Implement the void insert(BNode tree, int side) throws BTreeException method in the class BNode that inserts the subtree passed as parameter at the corresponding side of the tree. If there is already a subtree at that side, the method will throw a BTreeException with the corresponding error message.

    Declare, in the BNode class, the required LEFT_SIDE and RIGHT_SIDE class constants. If an incorrect side is introduced, or if there already is a non-empty tree in the selected side, a BTreeException must be thrown.

  • Implement the BNode extract(int side) throws BTreeException method that extracts and delete the corresponding subtree according to the parameter side. In case that side is not correct, the method will throw a BTreeException with the corresponding error message.

Exercise Section1.3.  Tre-traversal over a binary trees.

Objective

To practice the tree traversals over binary trees.

Exercise

Given the BTree binary tree from the previous exercise, program all tree-traversals (Pre-Order, In-Order, Post-Order) that allows to make the tree traversals recursively.

  • Implement the void printPreOrder() method that traverses the tree in pre-Order printing the content of each node on console.

  • Implement the void printInOrder() method that traverses the tree in in-Order printing the content of each node on console.

  • Implement the void printPostOrder() method that traverses the tree in post-Order printing the content of each node on console.

Exercise Section1.4.  Height of a tree

Objective

To study in depth the use of tree data structures.

Exercise

Given a binary tree, BTree, implement a method that returns its height.

Exercise Section1.5.  Size of a tree

Objective

To study in depth the use of tree data structures.

Exercise

Given a binary tree, BTree, implement a method that returns the number of nodes that it has.

Exercise Section1.6.  Using a Binary Tree

Objective

Teaching the student how to use binary trees.

Exercise

In this exercise, you must use the BTree and BNode classes that you have programmed previously. You must implement the TongueTwister class, whose main method should build a binary tree according to the next structure:

               Tres
                |
       --------------------
       |                  |
    tristes             tigres
       |
  ------------
  |          |
  |          |
comían     trigo
  • Print the binary tree using the methods you have programmed in the previous exercise. Test that the order of all elements appeared on console is correct.

  • Make all necessary changes to the structure of the binary tree, so that printing it in pre-order, the following phrase appears on console: "Tres tristes tigres comían trigo"

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

Exercise Section2.2.  Exam problem to work with binary trees.

Objective

To study a possible application of binary trees to solve simple expressions.

Exam problem

An expression is a particular combination of operators and operands that are evaluated to obtain a particular result. The most popular way to write an expression is one that is known as "infix" in which the order of the previous combination is as follows: first operand, operator and second operand. Next, an example of an infix expression is shown (without parentheses to be easier to understand).

a-b*c+d

However, there is another kind of notation of the previous expression, known as "postfix" in which, the order of the combination changes: first operand, second operand and operator, that is, the operator is located at the end of the combination. According to this rule, the initial infix expression is transformed in the next equivalent postfix one:

abc*-d+

Postfix expressions have many advantages when subsequent processing as, for example, they don't require the use of parentheses to be evaluated and this can be performed by simple algorithms as will be seen in this problem. Having in mind that any infix expression has its equivalent postifx one, this fact provides a great flexibility and agility when trying to set up and solve the problem of evaluation of expressions.

Another major advantage of postfix expressions is that they can be represented as an Abstract Syntax Tree (AST). An AST is a binary tree whose leaves contain the same characters from the operands (a, b, c, d. ..) and the remaining nodes contain the characters from the operators (+,-,*,/). The AST binary tree of the former postfix expression is as follows:

Using this representation of the postfix expression as an AST binary tree, the evaluation of it is reduced to make a simple recursive POSTORDER traversal algorithm and using an auxiliary data structure to store the intermediate results from the porcessing of the nodes in the course of it. Both for the creation and the evaluation of the AST binary tree, TWO AUXILIARY STACKS will be needed: one of binary nodes and another of decimal values(float) that will allow to create the structure of the nodes of the tree and to store the intermediate results of the operations of the evaluation of it. Finally, to evaluate numerically the expression, it will be required an aditional table that will store the numerical values of the operands of the expression, for which it will be used an auxiliary hash table.

Operator interface contains all possible operators involved in the expressions (to simplify the problem, it is only considered binary operators)

public interface Operator {

  public static final char SUM = '+';
  public static final char SUBTRACTION = '-';
  public static final char PRODUCT = '*';
  public static final char DIVISION = '/';

}

PostfixExprEvaluator class implements the previous interface and contains the associated AST binary tree expression together with the necessary data structures to create the tree and evaluate the postfix expression. In this case, they will be two auxiliary data STACKS to store both nodes and intermediate values. The class declaration is as follows:

public class PostfixExprEvaluator implements Operator {

  Node postfixASTTree = null; // The root node of the AST postfix expression tree

  Stack stackNodes = null; // Auxiliary stack to store the nodes of the AST tree

  Stack stackResults = null; //Auxiliary stack to store the intermediate results

Note

The class object that implements a stack is that provided by the JDK (java.util.Stack) and the methods to stack and unstack data are declared as follows:

public Object push(Object item); // Stack data on top of the stack

public Object pop(); // Unstack data from the top of the stack

Node class represents the AST tree binary node and it stores each character(char) of the postfix expression that can be either an operator(+,-,*,/) or an operand (a, b, c, d...):

class Node {
    char value = '\0'; // The character (either an operand or a operator)
    
    Node left = null;  // The reference to the left child node of the binary tree
    Node der = null; // The reference to the right child node of the binary tree
Exercise 1: Creation of the Abstract Syntax Tree (AST).

To create the AST binary tree that will store the postfix expression, a STACK of binary nodes (Node) is needed to build the binary tree as its elements are processed. The next figure shows the general process:

The process to build the AST binary tree is as follows:

  • For each of the characters in the postfix expression do the following:

    • If the character is an operand(variable a, b, c, d. ..), create a new binary node without children that will contain the character and push it on the STACK.

    • If the character is an operator(+,-,*,/), create a new binary node as follows:

      • Extract two binary nodes from the STACK.

        • The first element popped from the STACK will be the RIGHT child of the new binary node.

        • The second element popped from the STACK will be the LEFT child of the new binary node.

      • The data that will be stored at the new binary node will be the character that represents the operator

      • Once created the new binary node, push it on the STACK.

  • Finally, return the root node of the AST binary tree created which is precisely what is obtained by unstacking the only remaining binary node that is on the STACK.

Implement the following method from PostfixExprEvaluator class that permits to create the AST binary tree from a postfix expression according to the previous process:

Node createPostfixTree( String sPostfix ) throws Exception

Exercise 2: Tree Traversal and evaluation of the Abstract Syntax Tree (AST).

A postfix expression can be easily evaluated by the use of a POSTORDER tree traversal of the associated AST binary tree that has been implemented in the previous exercise. It will be also required to have two auxiliary data structures: a STACK and a DICTIONARY of data, as shown in the figure below:

Note

The DICTIONARY of data contains the integer values of operands(a,b,c,d....). To achieve this, it has been decided to use a Hashtable object that is provided by the JDK(java.util.Hashtable) and that permits to map a number of keys with its values. In this case, the key will be the operand's asociated character. To get the integer value associated to an operand, use the following method of the class where key is the associated character of it.

public Object get(Object key)  //Returns the value to which the specified key is mapped in this hashtable. 

The process for the evaluation of the AST binary tree is based on making a POSTORDER tree traversal of it performing the following process for each binary node of the tree:

  1. If the node to be evaluated is an operand (a,b,c....)

    • Stack on the results' STACK the integer value of the operand that is obtained previously from the DICTIONARY of data.

  2. If the node to be evaluated is a binary operator (+,-,*,/)

    • Unstack two values from the results' STACK, calculate the result of applying the operator to those values and, finally, stack the result again on the STACK.

Implement the following RECURSIVE method of the Node class that implements the POSTORDER tree traversal of the AST binary tree according to the previous process:

void processNode( Hashtable values, Stack stackResults ) throws Exception