CSCI 151 - Lab 5 Binary Trees

Due by 6PM on Sunday, April 17

In this lab you will implement a recursive binary tree structure and associated methods. The purpose of this lab is to:

Part 1 - Unpacking the zip file

You should get the starter code from Lab5.zip. As with previous labs, you want to unzip this into your csci151 folder. It makes a folder named "Lab5"; change the name of this folder to "Lab5<your last name> and open up an Eclipse project named Lab5 with this as your project folder. You will find the following files:

The BinaryTree, EmptyTree, and ConTree classes create a binary tree data structure. You will be adding methods to each of these classes. The TreeLoader class has a method loadTreeFromFile that you need to implement in Part 2. The The other three files create a GUI interface; you should not need to change them.

BinaryTree

As you may recall from class, a binary tree is either:

  1. empty, or
  2. a root node of data with a left and right (sub)tree.

The starter code creates three tree classes:

  1. an abstract class BinaryTree<T> to represent any binary tree,
  2. a subclass EmptyTree<T> of BinaryTree<T> to represent any empty binary tree. Note that we are using this class and NOT null to represent empty trees.
  3. a subclass ConsTree<T> of BinaryTree<T> to represent any non-empty binary tree. This class should contain fields appropriate for a non-empty binary tree.
In this way, any binary tree you create will either be an EmptyTree or a ConsTree. All three classes have a fully implemented toString() method that will allow trees to be drawn by the GUI interface.

TreeApp

You should be able to run the TreeApp program. It doesn't do much yet. There is a Load File button that brings up a file picker that allows you to choose from various tree files that have been supplied, but nothing happens when you choose a file because the loadTreeFromFile() method at this point just returns a new EmptyTree. You will fix that in Part 2.

This application's GUI frame has two text areas for output, and a group of buttons for user commands. After you implment the loadTreeFromFile() method, when a file is loaded a binary tree object will be created and the tree displayed in the left area of the frame. The area on the right side of the frame is used for the output the methods you will implement in Part 3.

Part 2 - Loading a Binary Tree from a File

Your task for this part is to implement the loadTreeFromFile() method in the TreeLoader class. We'll walk you through it.

The tree files for this lab list the nodes of the tree in postorder sequence, one node per line. Each line contains a string, which is the data value stored in the node, and two tag bits. The first tag bit indicates whether or not this node has a left child or not. (1=yes, 0=no). The second tag bit indicates whether the node has a right child or not.

For example, tree7:

 

is represented as

    32 0 0
    74 0 1
    29 0 0
    63 1 0
    18 0 0
    83 1 1
    34 1 1

The information in the file is sufficient to uniquely determine a binary tree. The tree can be constructed from the file using the following algorithm:

  1. Create an empty stack of binary trees (i.e. Stack<BinaryTree<String>>)
  2. While there is another line of input in the file
    1. read a line (containing data and two tags)
    2. if the right tag is 1, pop an element from the stack and call it right
    3. if the left tag is 1, pop an element from the stack and call it left
    4. create a new binary tree with data as its root and left and right as its subtrees (if they exist)
    5. push the new tree onto your stack

When you exit the while loop after having read all of the input, the stack should contain one element. This element is the the tree represented by the file, so return it. If the stack is empty, the tree has no nodes, so the tree must be an EmptyTree.

NOTE: the input file is left then right, but the algorithm uses the right value first, then the left. This is not a typo.

You need to implement this algorithm in the loadTreeFromFile method, with the following method signature:

    public BinaryTree<String> loadTreeFromFile(String filename)

The method should create a Scanner object to read strings from the file whose name is given as an argument. It should create a Stack of BinaryTree<String> objects to use as a work area. You can use Java's standard Scanner and Stack classes.

NOTE: Use Java generics in your code when declaring and instantiating a tree or stack. (For example, use BinaryTree<String> and Stack<BinaryTree<String>>.)

Test out your program, using the TreeApp program.

Part 3 - Implementing Your Binary Tree

Each of the methods listed below appears in the BinaryTree class as an abstract method. Eclipse has given default implementations of them in ConsTree and EmptyTree. Your task for each method is to provide correct implementations in ConsTree and EmptyTree and test that they work using the provided tree files.

Note: You do not need any loops in any of these methods.

Here is the complete list of methods for you to implement.

public boolean isEmpty()
Returns true if the tree is empty. (You should know this based on the class itself.)

public int height()
Returns the height of the tree. An empty tree has height -1, a single node has height 0.

public int nodeCount()
Returns the number of nodes in the tree.

public int leafCount()
Returns the number of leaves in the tree.

public int levelCount(int level)
Returns the number of nodes at a given level in the tree. Level 0 is the level of the root node, level 1 are the (up to 2) children of the root, and so on.

public int diameter()
Returns the diameter (or width) of the tree. This is the length of the longest path through the tree between two nodes without repeating any nodes along the path. There are three possible cases:
  1. The longest path is between two nodes in the left subtree.
  2. The longest path is between two nodes in the right subtree.
  3. The longest path is between one node in the left subtree and one node in the right subtree. In the first two cases, the root of the tree is not on the longest. (It couldn’t be because otherwise the left/right child would appear in the path twice.) Thus the diameter of the whole tree must be the diameter of the subtree.

In the third case, the longest path must include the root and therefore must be from the deepest leaf on the left to the deepest leaf on the right. We can compute that as the height of the left subtree plus the height of the right subtree plus 2. (We have to add 2 because the path from the left child → root → right child has length 2.)


public BinaryTree<T> mirrorImage()
Returns a new tree which looks like the mirror image of the given tree (mirrored around the root node, so left becomes right and right becomes left.) As in the clone() method for Collections, the new tree should contain all new nodes, not sharing any with the original tree. However, the data objects in the tree should be the same objects as those in the original tree; those objects should not be cloned. (This is also known as a shallow copy.)

public BinaryTree<T> pare()
Returns a new tree which is a copy of the current tree but omitting the children of any node which has fewer than two children. Starting at the root of the tree, if the current node has fewer than two children, return a new leaf node containing a shallow copy of the data. If the node has two children, return a new node containing a shallow copy of the data whose children are the result of calling pare on the left and right subtrees.

For example, if we pare tree7.txt the tree changes from Tree 7 to Pared tree. Nodes 74 and 63 have one child each and thus the whole subtree corresponding to those children will be removed leaving the result on the right

Paring tree9.txt will remove every node except for the root because the root only has a single child.

Paring any tree a second time should not change the tree since all nodes will have either zero or two children. (This is called a full binary tree.)


public BinaryTree<T> cutLeaves()
Returns a new tree which is a copy of the current tree with all of the leaf nodes removed. As with mirrorImage() and pare(), the copy of the data should be shallow. Cutting the leaves of tree7.txt removes nodes 32, 29, and 18, leaving this.

Cut tree.

Unlike pare(), cutLeaves() can be performed multiple times and each time will reduce the number of nodes in the tree until the tree is empty.


public int weightBalanceFactor()
Returns the “weight-balance factor” of the tree. The weight-balance factor of a binary tree is a measure of how well-balanced it is; that is, how evenly its nodes are distributed between the left and right subtrees of each node.

Define the weight-balance factor of a binary tree as follows: it is the maximum value of the absolute value of (number of nodes in left subtree minus number of nodes in right subtree) for all nodes in the tree. Whew! That takes some parsing!

Note: This is a different measurement from the Height-Balance property of AVL trees.

Here is an annotated example showing the WBF of all the nodes. (When you click the Weight Balance Factor button, you won’t see these annotations.) Weight-balance factor

  • The leaves all have a WBF of 0.
  • Node 56’s WBF is 1 because its right subtree contains 1 node and its left contains 0.
  • Node 23’s WBF is 2 for the same reasons.
  • Node 44’s WBF is 0 because its left and right subtrees contain the same number of nodes and have WBFs of 0.
  • Node 18’s WBF is 2 even though its left subtree contains 3 nodes and its right subtree contains 3 nodes because its left subtree has a WBF of 2.
  • Node 21’s WBF is 6 because its left subtree contains one node and its right subtree contains 7 nodes.

public void preOrderElements(List<T> list)
Adds all of the elements of the tree to list using a preorder traversal of the tree.

public void postOrderElements(List<T> list)
Adds all of the elements of the tree to list using a postorder traversal of the tree.

public void inOrderElements(List<T> list)
Adds all of the elements of the tree to list using an inorder traversal of the tree.

Note: The last three methods take a list as a parameter and append to it, rather than creating a new list and returning it: this is to avoid creating multiple lists. This also lets you implement these methods recursively.

Part 4 - handin

Look through your programs and make sure you've included your name at the top of ConsTree.java, EmptyTree.java and TreeLoader.java, the three files you edited.

Include in your submission a file named README. The contents of the README file should include the following:

  1. Your name and your partner's name if you worked with someone
  2. A statement of the Honor Pledge
  3. Any known problems with your classes or program

As usual, make a zipped copy of you project folder (which should be Lab5<your last name>) and hand it in on Blackbard as Lab 5.

Last Modified: November, 2021