Table of Contents
Teaching the student how to implement and use binary trees.
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).
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.
Solutions are at the end of the exercercises.
Student's training in the use of insertion methods for inserting and deleting in binary trees.
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.
Solutions are at the end of the exercercises.
To practice the tree traversals over binary trees.
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.
Solutions are at the end of the exercercises.
To study in depth the use of tree data structures.
Given a binary tree, BTree
, implement a method that returns its height.
Solutions are included in the following listing:
public class BTree<E> { /* ********************************************** * * CLASS BNode * ********************************************** */ static class BNode<E> { private E info; private BNode<E> left; private BNode<E> right; BNode(E info) { this(info, null, null); } BNode(E info, BNode<E> l, BNode<E> r) { this.info = info; left = l; right = r; } E getInfo() { return info; } BNode<E> getLeft() { return left; } BNode<E> getRight() { return right; } void setInfo(E info) { this.info = info; } void setLeft(BNode<E> left) { this.left = left; } void setRight(BNode<E> right) { this.right = right; } void insert(BNode<E> tree, int side) throws BTreeException { if (side == LEFT_SIDE) { if (left == null) { left = tree; } else { throw new BTreeException("A non-empty tree already exists"); } } else if (side == RIGHT_SIDE) { if (right == null) { right = tree; } else { throw new BTreeException("A non-empty tree already exists"); } } else { throw new BTreeException("Incorrect side"); } } BNode<E> extract(int side) throws BTreeException { BNode<E> subtree; if (side == LEFT_SIDE) { subtree = left; left = null; } else if (side == RIGHT_SIDE) { subtree = right; right = null; } else { throw new BTreeException("Incorrect side"); } return subtree; } int size() { int size = 1; if (left != null) { size += left.size(); } if (right != null) { size += right.size(); } return size; } int height() { int hl = -1; int hr = -1; if (left != null) { hl = left.height(); } if (right != null) { hr = right.height(); } return 1 + Math.max(hl, hr); } void preorder() { System.out.println(info); if (left != null) { left.preorder(); } if (right != null) { right.preorder(); } } void inorder() { if (left != null) { left.inorder(); } System.out.println(info); if (right != null) { right.inorder(); } } void postorder() { if (left != null) { left.postorder(); } if (right != null) { right.postorder(); } System.out.println(info); } } /* ********************************************** */ public static final int LEFT_SIDE = 0; public static final int RIGHT_SIDE = 1; protected BNode<E> root; public BTree() { root = null; } public BTree(E info) { root = new BNode(info); } public BNode<E> getRoot() { return root; } public void setRoot(BNode<E> root) { this.root = root; } public void insert(BTree<E> tree, int side) throws BTreeException { root.insert(tree.getRoot(), side); } public int size() { int size = 0; if (root != null) { size = root.size(); } return size; } public int height() { int h = -1; if (root != null) { h = root.height(); } return h; } public void preorder() { if (root != null) { root.preorder(); } } public void inorder() { if (root != null) { root.inorder(); } } public void postorder() { if (root != null) { root.postorder(); } } }
public class BTreeException extends Exception { public BTreeException(String mensaje) { super(mensaje); } }
To study in depth the use of tree data structures.
Given a binary tree, BTree
, implement a method that returns the number of nodes that it has.
Solutions are here and at the end of the exercercises:
public class TestNumNodes { public TestNumNodes() { } public static void main(String args[]) { BTree tree; try { tree = new BTree("Tres"); BTree subarbol = new BTree("tristes"); tree.insert(subarbol, BTree.LEFT_SIDE); BTree temporal = new BTree("tigres"); tree.insert(temporal, BTree.RIGHT_SIDE); temporal = new BTree("com\355an"); subarbol.insert(temporal, BTree.LEFT_SIDE); temporal = new BTree("trigo"); subarbol.insert(temporal, BTree.RIGHT_SIDE); } catch (BTreeException ex) { System.out.println(ex.getMessage()); return; } System.out.println("------------------"); System.out.println((new StringBuilder()) .append("Num Nodos Arbol Binario = ").append(tree.size()).toString()); System.out.println("------------------"); } }
Teaching the student how to use binary trees.
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"
Solutions are here and at the end of the exercercises:
public class TongueTwister { public static void main(String args[]) { BTree<String> tongueTwister; BTree.BNode<String> subtree, tmp; try { tongueTwister = new BTree<String>("Tres"); subtree = new BTree.BNode<String>("tristes"); tongueTwister.getRoot().insert(subtree, BTree.LEFT_SIDE); tmp = new BTree.BNode<String>("tigres"); tongueTwister.getRoot().insert(tmp, BTree.RIGHT_SIDE); tmp = new BTree.BNode<String>("comian"); subtree.insert(tmp, BTree.LEFT_SIDE); tmp = new BTree.BNode<String>("trigo"); subtree.insert(tmp, BTree.RIGHT_SIDE); } catch (BTreeException ex) { System.out.println(ex.getMessage()); return; } tongueTwister.preorder(); System.out.println("------------------"); tongueTwister.inorder(); System.out.println("------------------"); tongueTwister.postorder(); System.out.println("------------------"); System.out.println("------------------"); try { subtree = tongueTwister.getRoot().getLeft().extract(BTree.LEFT_SIDE); tongueTwister.getRoot().getRight().insert(subtree, BTree.LEFT_SIDE); subtree = tongueTwister.getRoot().getLeft().extract(BTree.RIGHT_SIDE); tongueTwister.getRoot().getRight().insert(subtree, BTree.RIGHT_SIDE); tongueTwister.preorder(); } catch (BTreeException ex) { System.out.println(ex.getMessage()); } } }
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.
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.
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.
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:
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.
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):
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).
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.
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.
To study a possible application of binary trees to solve simple expressions.
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
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
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
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:
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:
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.
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
Solutions are included in the following listings:
public interface Operator {
public static final char SUM = '+';
public static final char SUBTRACTION = '-';
public static final char PRODUCT = '*';
public static final char DIVISION = '/';
}
import java.util.Hashtable;
import java.util.Stack;
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 right = null; // The reference to the right child node of the binary tree
public Node() {
}
public Node(char value, Node left, Node right) {
this.value = value;
this.left = left;
this.right = right;
}
public Node(char value) {
this(value, null, null);
}
public void processNode(Hashtable values, Stack stackResults)
throws Exception {
// This recursive method realizes a postorder tree traversal to process the
// node
// Firstly, we process the left node recursively
if (left != null) {
left.processNode(values, stackResults);
}
// Secondly, we process the right node recursively
if (right != null) {
right.processNode(values, stackResults);
}
// In the other case, we process the actual node
float result = 0;
// Depending on the Node's value that indicates the corresponding opperation
// the result value is calculated.
switch (value) {
case Operator.SUM:
result = (Float) stackResults.pop() + (Float) stackResults.pop();
break;
case Operator.SUBTRACTION:
float subtrahend = (Float) stackResults.pop();
float minuend = (Float) stackResults.pop();
result = minuend - subtrahend;
break;
case Operator.PRODUCT:
result = (Float) stackResults.pop() * (Float) stackResults.pop();
break;
case Operator.DIVISION:
float divisor = (Float) stackResults.pop();
float dividend = (Float) stackResults.pop();
result = dividend / divisor;
break;
default:
result = (Integer) (values.get(value));
}
// And finally, we store the result at the results' stack
stackResults.push(result);
}
}
import java.util.Stack;
import java.util.Hashtable;
public class PostfixExprEvaluator implements Operator {
Node postfixASTTree = null; // The 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
public PostfixExprEvaluator(String sPostfix) throws Exception {
// Create the AST Tree from the postfix expression
postfixASTTree = createPostfixTree(sPostfix);
}
private Node createPostfixTree(String sPostfix) throws Exception {
int i = 0;
stackNodes = new Stack();
// while there are tokens(char) left to analyze
while (i < sPostfix.length()) {
// Evaluate the token (char)
char token = sPostfix.charAt(i++);
switch (token) {
// If it is an operator
case Operator.SUM:
case Operator.PRODUCT:
case Operator.SUBTRACTION:
case Operator.DIVISION:
// Popping the two operands
Node leftOperand = (Node) stackNodes.pop();
Node rightOperand = (Node) stackNodes.pop();
// Create a new Binary Tree Node
Node binaryOperation = new Node(token, rightOperand, leftOperand);
// Push it on the stack
stackNodes.push(binaryOperation);
break;
// If it is an operand
default:
// Push the new node on the stack
Node operand = new Node(token);
stackNodes.push(operand);
}
}
// Finally, we return the first Node of the stack.
return (Node) stackNodes.pop();
}
public float evaluateExpression(String expression, Hashtable values)
throws Exception {
// Expression's result
float result = 0;
// The results' stack
stackResults = new Stack();
// Create a Binary Tree with the postfix expression
postfixASTTree = createPostfixTree(expression);
// Traverse the tree to calculate the intermediate results
postfixASTTree.processNode(values, stackResults);
// The result value to be returned is located on top of the results' stack
result = (Float) stackResults.pop();
return result;
}
public static void main(String args[]) throws Exception {
Hashtable values = new Hashtable();
values.put('a', 2);
values.put('b', 5);
values.put('c', 3);
values.put('d', 1);
String sPostfix = "abc*-d+";
PostfixExprEvaluator myExpression = new PostfixExprEvaluator(sPostfix);
System.out.println(" Evaluating postfix expression '" + sPostfix
+ "' with values: " + values);
System.out.println(" Result = "
+ myExpression.evaluateExpression(sPostfix, values));
}
}