Binary Trees, Part 2

advertisement
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
Download