Binary Trees, Part 2 As an ADT, binary trees have operations that add and remove nodes, implement the three traversal methods we discussed in the last lecture, operations that display a node’s ancestors or descendants, and calculates the tree’s height, and we can even test to see if a tree is balanced. Binary Trees can be implemented using an array, but we will focus only on a reference based implementation. Each node in the binary tree will consist of 3 things: Value A value A link to the nodes left subtree. A link to the node’s right subtree. The tree itself will consist of a “link” to the root of the tree. The following is a class representing a node in a binary tree: public class BinaryTreeNode { private int value; private BinaryTreeNode leftChild; private BinaryTreeNode rightChild; public BinaryTreeNode(int aValue) { value = aValue; leftChild = null; rightChild = null; } public BinaryTreeNode(int aValue, BinaryTreeNode left, BinaryTreeNode right){ value = aValue; leftChild = left; rightChild = right; }// end constructor public int getValue() { // returns the value stored in any tree node return value; } public void setValue(int aValue) { // sets the value in a tree node value = aValue; } public BinaryTreeNode getLeft(){ // returns a reference to the node’s left subtree return leftChild; } public BinaryTreeNode getRight() { // returns a reference to the node’s right subtree return rightChild; } public void setLeft(BinaryTreeNode left) { // sets a node’s left subtree leftChild = left; } public void setRight(BinaryTreeNode right) { // sets a node’s right subtree rightChild = right; } }// end Node The data members for a binary tree are: public class BinaryTree { private BinaryTreeNode root; public BinaryTree () { root = null; } // end default constructor public BinaryTree(int newValue) { // creates a tree with a single node that is the root root = new BinaryTreeNode(newValue); } }// end Binary Tree Class Some basic tree operations, that are valid for either unordered binary trees, or binary search trees are: + isEmpty(): Boolean, //returns true of root is null public boolean isEmpty() { return (root == null); } + makeEmpty() // if the root node is not null, sets it to null freeing all nodes in the tree. public void makeEmpty() { root = null; } + setRoot(in value: nodeValueType) // Creates a node and attaches it as the root node of a binary tree // unlinking any others that might exist public void setRoot(int aValue) { root = new BinaryTreeNode( aValue); } + getRoot (): BinaryTreeNode // returns a reference to node pointed to by root. // Note this should be implemented with care, once a client program // has a reference to node, then the tree can be directly modified // and may be modified incorrectly public BinaryTreeNode getRoot() { return root; } +traverseInorder() // does an inorder traversal of the tree, displaying the value in each node. // To preserve the encapsulation of the tree, client programs will // be provided with a non recursive method, which in turn // invokes private a recursive method public void traverseInorder() { inOrder( this.root ); } private static void inOrder( BinaryTreeNode currentRoot) { if (currentRoot != null) { inOrder( currentRoot.getLeft() ); System.out.println( currentRoot.getValue() ); inOrder ( currentRoot.getRight() ); }// end if } // end inOrder Traversal +traversePreOrder() // does a preorder traversal of the tree, displaying the value in each node // public void traversePreorder() { preOrder( this.root ); } private static void preOrder( BinaryTreeNode currentRoot) { if (currentRoot != null) { System.out.println( currentRoot.getValue() ); preOrder( currentRoot.getLeft() ); preOrder ( currentRoot.getRight() ); }// end if } // end preOrder Traversal + traversePostOrder() // does a postOrder traversal of the tree, displaying the value in each node public void traversePostorder() { postOrder( this.root ); } private static void postOrder( BinaryTreeNode currentRoot) { if (currentRoot != null) { postOrder( currentRoot.getLeft() ); postOrder ( currentRoot.getRight() ); System.out.println( currentRoot.getValue() ); }// end if } // end inOrder Traversal +find (in value: nodeValueType) : BinaryTreeNode // returns a reference to the node containing the specified value // if the value is not in the tree null is returned. root M A H T Z L When discussing unordered binary trees we will assume that there are no duplicate values in the tree. To locate a node in an UNORDERED binary tree, we begin with the root of a tree or subtree: if “root” is null, we quit and return null; else if the “root” node contains the value we are looking for, we return a reference to “root” else we search the “root’s” left sub-tree, making it’s left child the new “root” of our search. if the previous search returns “null” the value was not in the left subtree, so the right subtree is searched, making the right child the new “root”. return the result of the final search. To protect the root of our tree, we declare a public method “find” and a private method which accesses the root: public BinaryTreeNode find(int searchValue) { return findNode(this.root, searchValue); } private static BinaryTreeNode findNode(BinaryTreeNode current, int searchValue) { BinaryTreeNode nodeFound = null; if( current == null) return null; else if (current.getValue() == searchValue) return current; else {// search left subtree, then if needed the right subtree nodeFound = findNode(current.getLeft(), searchValue); if (nodeFound == null) nodeFound = findNode(current.getRight(), searchValue); } return nodeFound; }// end found root M P H T D L Searching a Binary Search Tree: The algorithm to search for a value in a BST is simpler than that for an unordered tree If the “current root “ is null the value is not in the tree, return null. if the node in the “current root” of the subtree being searched equals the search value, we return a reference to that node. Otherwise, if the search value is less than the value in the “current root” we search again with the left child. Otherwise, if the search value is greater than the value in “current root” we search again with the right child. private static BinaryTreeNode findNode(BinaryTreeNode current, int searchValue) { //This method does a recursive search of a Binary search tree to // locate a node containing a specific value. If the value is // present in the tree, a reference to that node is returned // otherwise null is returned if the value is not in the tree // “current” represents the root of the subtree currently being searched BinaryTreeNode nodeFound = null; // if the "current root" is null, the node isn't in the tree if( current == null) return null; else if (current.getValue() == searchValue) nodeFound = current; else if (current.getValue() > searchValue) nodeFound = findNode(current.getLeft(), searchValue); else nodeFound = findNode(current.getRight(), searchValue); return nodeFound; }// end find node + height() : int // Calculates and returns the height of a binary tree The height of a binary tree is the number of nodes between the root, and leaf on its longest path. For example: M M Q H H T T D L D height=4 height = 3 A M Q H T D L Y height = 5 J U The height of any tree is the greater of the height of its left subtree or right subtree plus one. We calculate the height of each subtree, determine the greatest and add 1. public int height(){ return calcHeight(this.root); } private int calcHeight(BinaryTreeNode current) { int heightLeft=0, heightRight=0; if ( current== null) return 0; else{ heightLeft = calcHeight(current.getLeft()); heightRight = calcHeight(current.getRight()); if (heightLeft >= heightRight) return 1 + heightLeft; else return 1 +heightRight; }// end else } // end method Balanced trees: A tree is balanced if the height of it’s subtrees differ by at most one, AND BOTH OF ITS SUBTREES ARE BALANCED. This is especially important for binary search trees (BST). When searching an unordered binary tree of N nodes, we can not eliminate any nodes from consideration, until all have been searched, so at worst we will need to view all N nodes before finding a value or determining it is not there (like a linear search). A binary search tree approximates the efficiency of a binary search on an ordered array IF IT IS balanced, and may still be more efficient than searching an unordered binary tree in many cases if it is not. If the tree is balanced, then with each decision to search the left or right subtree, we are eliminating approximately ½ of the remaining nodes in the tree. If the tree is unbalanced, then the search time can approach that of a linear search, ie: M H D T height=4 A Note: There are techniques for balancing an unbalanced binary tree, but they are beyond the scope of time remaining in this class. For an unbalanced tree the time to search can become nearly the same as the linear search on a unsorted array. public Boolean isBalanced(){ return balancedTree(root); } private boolean balancedTree(BinaryTreeNode currentRoot) { int heightLeft=0, heightRight=0; if(currentroot == null) return true; heightLeft = calcHeight(curentroot.getLeft()); heightRight = calcHeight(currentroot.getRight()); if (Math.abs(heightRight - heightLeft) <=1 && this.balancedTree(currentroot.getLeft()) && this.balancedTree(currentroot.getRight()) ) return true; else return false; } Inserting Values into an unordered Binary Tree Adding a node a regular unordered binary tree is a bit complex we must name the parent node by specifying the value in the parent node, and specify where the new node is to be inserted (as the right or left child). +attachLeft ( in newItem: nodeValueType, in parentNode: nodeValueType) // creates a new node, and attaches it as the left child of the node containing the value // “parentNode”. public void attachLeft(int aValue, int parent) { BinaryTreeNode temp = this.findNode(root, parent); if (temp == null) System.out.println("The node containing " + parent + " was not found, insert aborted"); else if (temp.getLeft() != null) System.out.println("The node containing " + parent + " already has a left subtree."); else // add the node temp.setLeft(new BinaryTreeNode(aValue)); }// end attach Left +attachRight (in newItem: nodeValueType, in parentNode: nodeValueType) // creates a new node, and attaches it as the right child of the node containing the value // “parentNode”. public void attachRight(int aValue, int parent) { BinaryTreeNode temp = this.findNode(root, parent); if (temp == null) System.out.println("The node containing " + parent + " was not found, insert aborted"); else if (temp.getRight() != null) System.out.println("The node containing " + parent + " already has a right subtree."); else // add the node temp.setRight(new BinaryTreeNode(aValue)); }// end attach Right look at sample program TestBtree.java Inserting Nodes into a Binary Search Tree Remember a binary search tree (BST) is an ordered binary tree. In a binary search tree, each node in any Node’s left subtree contains a value less than the value stored in the root, and each node in the right subtree contains a value that is greater than the value stored in the root. M Q H T D L The data members of a BST are the same as the data members for a regular binary tree. Only the insertion, search, and deletion algorithms change. Although the basic operations discussed above are appropriate for BST’s the insertion algorithm alters as follows: +insert(in value: nodeValueType, in root: BinaryTreeNode) // Creates a node and attaches it into the binary tree. // 1. The same value can not be stored in two different nodes. // 2. If the value in the “root” of the tree or subtree is greater than the new “value” // a) If the “root’s” left child is null, a node is created and the new value inserted. // b) if the “root’s” left child is Not null, call insert again with the left subtree. // 3. If the value in the “root” of the tree or subtree is less than the new “value” // a) If the “root’s” right child is null, a node is created and the new value inserted. // b) if the “root’s” right child is Not null, call insert again with the right subtree. To illustrate this algorithm, insert the values K, R, H, E, F, M, Z into an empty binary tree. 1. The tree is initially empty, so the first value “K” is created as root. K 2. To add the next value R, the algorithm begins at “root”. “R” is greater than “K”, root’s right child is “null”, create a node and add it as the right child of “root”. K R 3. To insert “H”, begin at root, “H” is less than “K”, the left child of root is null, create a node and add it as the left child of root. K R H 4. To insert “E”, begin at root, “E” is less than “K”, the left child of “K” is not null, call insert with “K’s” left child: “H”. “E” is less than “H”, node “H” has no left child, create a node and attach it as “H’s” left child. K H E R 5. To insert “F”, start at root, “F” is less than “K”, “K’s” left child is not null, call insert with “K’s” left child “H”. “F” is less than “H”, “H’s” left child is not null, call insert with “H’s” child “E”. “F” is greater than “E”, “E’s” right child is null, create a node and insert it as “E’s” right child. K R H E F 6. To insert “M”, begin at root. “M” is greater than “K”, “K’s” right child is not null, call insert with “K’s” right child “R”. “M” is less than “R”, “R’s” left child is null, insert “M” as “R’s” left child. K R H M E F 7. To insert “Z”, begin at root. “Z” is greater than “K”, call insert with “K’s” right child “R”. “R” is less than “Z”, “R’s” right child is null, add “Z” as the right child of “R”. K R H M E F Z public void insert(int newValue) { // public insertion method that either sets the root // or calls the recursive insertion method to add the node if (this.root == null) this.root = new BinaryTreeNode(newValue); else insertNode(this.root, newValue); } private static void insertNode(BinaryTreeNode current, int newValue){ // This method either inserts a new node into the tree, or // displays an error if the value already exists in the tree. // if the value of the "current root" of the subtree being // traced equals the value to insert, display an error if (current.getValue() == newValue){ System.out.println("insert cancelled, value is already in tree"); return; } else { // if the new value is less than that in the "current root" // if the left child of the current root is null add // the new node there otherwise call insert again with // the left subtree if (current.getValue() > newValue){ if(current.getLeft() == null) current.setLeft( new BinaryTreeNode(newValue) ); else insertNode(current.getLeft(), newValue); }// end left else { // if the new value is greater than that in the "current root" // if the right child of the current root is null add // the new node there otherwise call insert again with // the right subtree if (current.getValue() < newValue) { if (current.getRight() == null) current.setRight( new BinaryTreeNode(newValue) ); else insertNode(current.getRight(), newValue); } } // right }// end else not equal }// end insertNode Deleting Values from a Binary Search Tree: Deleting values is somewhat more complex, as with any linked data structure we must locate both the node to be removed, and that node’s predecessor (or parent) To locate a nodes parent: // private method, that given a search value finds the parent of the node // containing the value. If the value is not in the tree a null // is returned as the parent. private static BinaryTreeNode findParent(BinaryTreeNode current, int value) { // "current" represents the root of the subtree being processed BinaryTreeNode nodeFound = null; if( current == null) nodeFound = null; else if (value < current.getValue()) { if (current.getLeft()!= null && current.getLeft().getValue() == value) nodeFound= current; else nodeFound = findParent (current.getLeft(), value); }// not left else {// check right if (current.getRight() != null && current.getRight().getValue() == value) nodeFound= current; else nodeFound = findParent(current.getRight(), value); }// end check right return nodeFound; }// end findParent When deleting a node there are 3 special cases: 1) The node is a leaf: if the node is a leaf, we simply unlink it. K R H E M F O If we are asked to delete the node containing “F”, we locate it and it’s parent “E”, and simply unlink it. K R H E M O 2) The node has 1 child: If the node has either a left or right child, we simply replace it with it’s child K R H E M F O If we are asked to delete the node containing “E”, we locate it and it’s parent “H”, and replace it with it’s child “F” K R H F M O K R H E If we are asked to delete the node containing “R”, we locate it and it’s parent “K”, and replace it with it’s child M M F O K H M E O F 3) The node has two children: If the node has two children then we replace it with it’s “inorder” successor. We find the node in the tree that has a value closest to the one being removed, and use it to replace the node being deleted. The inorder successor is the leftmost node on the node to be deleted’s right subtree, and will always either be a leaf, or a node with a right subtree. K R H T E M Z G O A F If we remove node “E”, we replace it with the leftmoste node on it’s right subtree, which is F K R H T F M Z G A O K R H T F M Z G O A If we delete R, we replace it with node T K T H Z F M G A O Algorithmically, we find the leftmost node on the right subtree, and then return the value stored in that node. We then call “delete” again to delete the original node from the tree, and then store the retrieved value into the original physical node. public void delete (int value) { BinaryTreeNode parent, nodeToDelete ; char leftRight; // check to see if we are deleting root if (root.getValue() == value) { System.out.println("node to delete contains " + root.getValue()); // if root is only node in tree, just set the root to null if (root.getLeft() == null && root.getRight() == null) this.root = null; // if root has 1 child else if (root.getLeft() == null || root.getRight() == null) { if (root.getLeft() != null) this.root = root.getLeft(); else this.root = root.getRight(); }// end root has one child // else root has 2 children else { int tempValue = findSuccessor(root.getRight()); this.delete(tempValue); root.setValue(tempValue); }// end root has two children }// end deleting root else { // NOT deleting root // find the parent of the node to be deleted parent = findParent( this.root, value); if (parent == null) System.out.println("Value not in tree"); else { System.out.println("The parent of " + value + " is " + parent.getValue() ); // determine if the node to be deleted is the parents left or right subtree if (parent.getLeft() != null &&parent.getLeft().getValue() == value) { leftRight = 'L'; nodeToDelete = parent.getLeft(); }//left child else { leftRight = 'R'; nodeToDelete = parent.getRight(); }// right Child System.out.println("node to delete contains " + nodeToDelete.getValue()); // is node to delete a leaf if (nodeToDelete.getRight() == null && nodeToDelete.getLeft() == null) { if (leftRight == 'L') parent.setLeft(null); else parent.setRight(null); }// end leaf else if (nodeToDelete.getLeft() == null || nodeToDelete.getRight()==null){ // node has one child BinaryTreeNode temp; // get child from nodeToDelete if (nodeToDelete.getLeft() != null) temp= nodeToDelete.getLeft(); else temp = nodeToDelete.getRight(); if (leftRight == 'L') parent.setLeft(temp); else parent.setRight(temp); }// end one child else { // two children int successorValue = findSuccessor(nodeToDelete.getRight()); System.out.println("The inorder successor of " + value + " is " + successorValue); this.delete(successorValue); nodeToDelete.setValue(successorValue); } }// end else found }// end not deleting root }// end delete private static int findSuccessor(BinaryTreeNode current){ if (current.getLeft() == null) return current.getValue(); else return findSuccessor(current.getLeft()); }// end find successor