Binary Nodes

a. Every node can have up to two children: a left child and a right child

Types of Binary Nodes

a. Root: the first node

b. Leaf: a node with no children

c. Interior Nodes: nodes that are neither the root or a leaf

 


Traversals of Binary Trees

Mechanisms

In general, there isn’t an explicit order for traversing binary tree arrangements. Here are three example mechanisms:

1. Pre-order traversal

In this traversal method, the program visits each node before visiting any child nodes. Usually, it goes to a node first, then each node in its left subtree, and then each node in its right subtree.

2. In-order traversal

In this traversal method, the program first traverses the left subtree, then visits the root, and then traverses the right subtree.

3. Post-order traversal

In this traversal method, the program first traverses the left subtree, then the right subtree, and lastly, visits the root node.

 

Traversal Example

Traversing a binary tree requires creating an iterator that traverses all subtrees. Consider the following binary tree.

\(\require{color}\)

An InOrder traversal visits the nodes in the following order:

\[\begin{gather}4 & \mathbf{10} & 12 & \mathbf{15} & 18 & \mathbf{22} & 24 & {\color{navy}\mathbf{25}} & 31 & \mathbf{35} & 44 & \mathbf{50} & 66 & \mathbf{70} & 90\end{gather}\]

 


Binary Search Tree

Unlike the BinaryTree, the BinarySearchTree provides one iterator method: an in-order traversal. When implementing a BinarySearchTree, we’re implementing an OrderedStructure: methods that accept and return values compared to one another. We assume the data is Comparable, and the natural order is sufficient. We can use alternative Comparators for elements that do not directly implement compareTo.

Definition

A binary search tree is a binary tree structure with the following attributes:

a. The left subtree of a node holds only nodes with values lesser than the node

b. The right subtree of a node has only nodes with values greater than the node

c. Both the left and right subtrees must also be a binary search tree

 

Example. The below is an example of a complete Binary Search Tree with 4 Levels:

 

  • Levels. The root and each interior node have two children –a right and left child; hence, there are \(4\) full levels.
  • Nodes. The number of nodes can be calculated by \(2^{n}-1\) where \(n\) represents the number of full levels; hence, there are \(15\) nodes.

 


Types of Binary Search Trees

I. Complete Binary Trees

  • Complete Binary Trees give excellent time complexities of \(\mathbb{\bf O}\left(\log2\left(n\right)\right)\), which helps for find and insert methods in a Java data structure.
  • The number of nodes can be calculated with, \(\mathbb{\bf n}\text{ full levels:}\left(2^\mathbb{\bf n}-1\right) \text{ nodes}\).

 

II. “Real” Binary Trees

  • It’s possible to have a non-complete binary tree, which means not every parent node has two children nodes.
  • In the worst case, our binary tree could be a LinkedList with \(\mathbb{\bf O}\left(n\right)\) complexities.

 


Java Code

1. Main.java

Main method to demonstrate binary tree and binary tree node

a. creates an empty BinaryTree

b. creates random integers to insert into the tree

c. prints out the tree’s nodes and order

d. prints out the tree

 

import java.util.concurrent.ThreadLocalRandom;
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    
    // create an empty BinaryTree
    BinaryTree<Integer> myTree = new
    BinaryTree<Integer>();
    
    // instantiate new scanner
    Scanner keyboard = new Scanner(System.in);
    
    // create random integers and insert them into myTree
    for (int i=0; i< 5; i++){
      
      // generate random number in range [-15, 50]
      System.out.print("Enter item #"+i+": ");
      int randValue = keyboard.nextInt();
      
      // insert each number into myTree
      myTree.insert(randValue); }
    
    // System.out.println(myTree);
    
    System.out.print("The nodes of tree are (inorder): ");
    System.out.println(myTree.inorder());
    System.out.print("The breadth order of the tree is: ");
    System.out.println(myTree);
  }
}

2. BinaryNode.java

Simple BinaryNode method using a generic that extends Comparable

a. uses a generic that extends Comparable .compareTo()

b. each node contains three objects: data, a left child, and a right child

c. alternate constructor creates a new leaf in binary tree and getters return objects

d. insert(E item) method recursively inserts item as a new leaf

e. boolean method checks if the Left or Right child is empty

f. inorder method recursively traverses the nodes

  • if there is a nonempty left child, check there first
  • if there is a nonempty right child, check there last

 

public class BinaryNode<E extends Comparable> {
  
  // each node contains three objects
  private E  Data;
  private BinaryNode<E> leftchild;
  private BinaryNode<E> rightchild;

  // alternate constructor: creates a new leaf in binary tree
  public BinaryNode(E item){
    Data = item;
    leftchild = null;
    rightchild = null; }
  
  // getters: return objects
  public E getData(){
    return Data; }
  public BinaryNode<E> getLeft(){
    return leftchild; }
  public BinaryNode<E> getRight(){
    return rightchild; }
  
  //  recursively insert item as a new leaf
  public void insert(E item){
    int compareV = item.compareTo(Data);
    
    // ignore items already in tree
    if (compareV ==0) { 
      return; }
    
    // left child
    else if (compareV < 0){
      if (leftchild == null){
        leftchild = new BinaryNode<E>(item); }
      else {
        leftchild.insert(item); }}
    
    // right child
    else {
      if (rightchild == null){
        rightchild = new BinaryNode<E>(item);
        return; }
      else {
        rightchild.insert(item); }}
  }
  
  // boolean checks whether Left or Right child is empty
  public boolean isLeftChildEmpty(){
    return leftchild == null; }
  public boolean isRightChildEmpty(){
    return rightchild == null; }
  
  // recursive inorder traversal of the nodes
  public String inorder(){
    String Result = " ";
    
    // if there is a nonempty left child, check there first
    if (!isLeftChildEmpty()){
      Result = Result + leftchild.inorder(); }
    // now print value at the node
    Result = Result + " " + Data;
    
    // if there is a nonempty right child, check there last
    if (!isRightChildEmpty()){
      Result = Result + " " + rightchild.inorder(); }
    return Result; }
  }

3. BinaryTree.java

Simple BinaryTree method using a generic E that extends Comparable

a. uses a generic E to extend Comparable .compareTo()

b. default constructor establishes tree’s root node

c. insert(E item) method inserts an item into the tree as a binary node

  • if tree is empty, create a binary node as the root containing an item
  • insert item as a node using the order relation

d. inorder method prints the tree in order

  • calls recursive BinaryNode inorder() and returns roots

e. ToString method prints tree in a breadth first order

 

import java.util.LinkedList;
import java.util.Queue;

public class BinaryTree<E extends Comparable> {
  
  private BinaryNode<E> root;
  
  // default constructor
  public BinaryTree(){
    root = null; }
  
  
  // insert an item into the tree as a binary node
  public void insert(E item) {
    
    // if tree is empty, create a binary node as the root containing an item
    if (root == null) {
      root = new BinaryNode<E>(item);
      return; }
    
    // insert item as a node using the order relation
    root.insert(item); }
  
  
  // print tree in inorder
  public String inorder(){
    String Result = "";
    if (root == null){
      return Result; }
    
    // call recursive BinaryNode inorder()
    return root.inorder(); }
  

  // print tree in a breadth first order
  public String toString(){
    Queue<BinaryNode<E>> myQueue = new LinkedList<BinaryNode<E>>();
    String Result = " ";
    
    if (root == null) return Result;
    myQueue.add(root);
    
    while (!myQueue.isEmpty()){
      BinaryNode<E> current = myQueue.remove();
      Result = Result + " " + current.getData();
      if (!current.isLeftChildEmpty()){
        myQueue.add(current.getLeft()); }
      if (!current.isRightChildEmpty()){
        myQueue.add(current.getRight()); }}
    return Result; }
  
}

 


Summary

The binary search tree is the result of inserting new values. The method puts each new data point on a leaf; hence, the internal nodes remain unchanged, thereby making the structure reasonably static. Insufficient data allocation will cause a degraded tree structure to negatively affect the method’s performance.

The binary search tree requires an order on the nodes of the binary tree. At each node in the search method, the program decides to go left or right. If the tree is short and relatively balanced, these decisions will eliminate many of the remaining candidate values.

 

Time Complexity

Each operation of a Binary Search Tree has a worst-case time complexity proportional to the tree’s height. Checking leaves, adding leaves, or removing roots are some of the most time-consuming operations. Therefore, for logarithmic behavior, we want the tree to be as short as possible.

i. If we insert values using descending order, the tree is left-skewed.

ii. If we insert values using ascending order, the tree is right-skewed.

iii. The program performs better if we shuffle the values beforehand:

  • the tree becomes shorter and more balanced
  • the expected insertion time is \(O(\log n)\)

 

import java.io.FileReader;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;


public class Main {

    // declaration of ArrayList
    private static ArrayList<Lilly> Flowers;

    public static void main(String[] args) {

        // How many data points do you want
        // formatted input from FileReader using Scanner
        System.out.print("How large of an array do you want to sort? ");
        Scanner keyboard = new Scanner(System.in);
        int FileSize = keyboard.nextInt();

        // create data set
        Flowers = CreateData(FileSize);

        // sort data set
        ArrayList<Lilly> MFlowers = MergeSort.MergeSort(Flowers); // Merge Sort
        ArrayList<Lilly> NFlowers = BubbleSort.Bubble(Flowers); // Bubble Sort
        ArrayList<Lilly> IFlowers = InsertionSort.InsertionSort(Flowers); // Insertion Sort

        System.out.println();
        System.out.println("For list size of " + Flowers.size());
        System.out.println("     Bubble sort number of steps: " + BubbleSort.getBcount());
        System.out.println("     Merge sort number  of steps: " + MergeSort.getMcount());
        System.out.println("     Insertion sort number of steps:  " + InsertionSort.getICount());
    }

    // **************
    // Utility for debugging
    public static void Dump(ArrayList<Lilly> tFlowers){
        for( Lilly XXX : tFlowers){
            System.out.println(XXX.toString());
        }
    }

    // ******************
    // Provided method to read from dataset and fill an ArrayList with lilly objects
    // Read from the dataset fisherIris.csv
    // Randomly chooses n samples from dataset
    // such that n is the user defined number of data points
    private static ArrayList<Lilly> CreateData(int testSize){

        ArrayList<Lilly> FisherData = ReadFromFile("fisherIris.csv");
        System.out.println();
        System.out.print("Fisher data file loaded ...");
        return GenerateTestSet(FisherData, testSize);
    }

    // *******************
    // Generate simple test cases:
    // Create the test file of size tsize from the Fisher Iris data set
    private static ArrayList<Lilly> GenerateTestSet(ArrayList<Lilly> fisher, int tsize){

        ArrayList<Lilly> testSet = new ArrayList<Lilly>();
        for (int i=0; i< tsize; i++){

            // correct way to generate random number in range [0, fisher.size()-1]  in Java
            int randfisher = ThreadLocalRandom.current().nextInt(0,fisher.size());

            testSet.add( fisher.get(randfisher) );
        }

        System.out.println(" test file of size " + tsize + " created.");
        return testSet;
    }

    // ****************
    // Provided method to read from dataset and fill an ArrayList with lilly objects
    // Open external input file to be read with Scanner class
    private static ArrayList<Lilly> ReadFromFile(String fname){

        // Arraylist that will hold Fisher Iris data set
        // this data set will be read in from the comma separated file fname
        ArrayList<Lilly> basefile = new ArrayList<Lilly>();

        Scanner DataFile;
        try{
            DataFile = new Scanner(new FileReader(fname));
            DataFile.useDelimiter(",");

            // throw away first line
            DataFile.nextLine();

        } catch(Exception e){
            System.out.println("Something is horribly wrong with input file");
            System.exit(22);
            return null;
        }

        double sl, sw, pl, pw;
        String category;

        // Opened file for input on Scanner DataFile
        while (DataFile.hasNext()) {
            try {
                sl = DataFile.nextDouble();
                sw = DataFile.nextDouble();
                pl = DataFile.nextDouble();
                pw = DataFile.nextDouble();
                category = DataFile.nextLine();
            } catch (Exception e) {
                System.out.println("Fisher data is corrupted");
                System.exit(33);
                return null;
            }

            basefile.add(new Lilly(sl, sw, pl, pw, category));
        }

        DataFile.close();

        return basefile;

    }

}

References

[1]
Bailey, D. A. (2003). Java structures: Data structures in Java for the principled programmer. McGraw-Hill, Boston, Mau.
[2]
Gallardo, R., Gordon, J., Hommel, S., Kannan, S. and Zakhour, S. (2015). The Java tutorial a short course on the basics. Addison-Wesley, Upper Saddle River, NJ.