| data values of all nodes in the left subtree of node n |
< | data value at node n |
< | data values of all nodes in the right subtree of node n |
root,For example, some facts about the above tree:
children,
leaf (node with no children),
interior node (non-leaf),
a node's level or depth,
a tree's height (maximum depth),
a nodes's height (height of the subtree at that node),
a perfect tree (all interior nodes have two children & all leaves at same level)
node 20, the root, is at level 0,
it has height 3,
it has two children: nodes 15 and 30
node 25 is at level 2,
it has height 1, and is a perfect subtree
node 17 is a leaf at level 3 and has height 0
< 0 means e1 is less that e2
e1.compareTo(e2) = 0 means that e1 equals e2
> 0 means that e1 is greater than e2
Alternatively, a Comparator<E> object
can be used to define the relation between two elements.
A Comparator<E> object,
cmp, is one which defines the compare function.
Using such an object we can defined the relation between two elements:
< 0 means e1 is less that e2
cmp.compare(e1,e2) = 0 means that e1 equals e2
> 0 means that e1 is greater than e2
Comparator<Integer> cmp = new Comparator<Integer>() {
public int compare(Integer lhs, Integer rhs) {
String lhs_str = lhs.toString();
String rhs_str = rhs.toString();
return lhs_str.compareTo(rhs_str);
}
};
If you've never seen this construction before, the object cmp
is created using an anonymous inner class.
Comparator is an interface which
requires the definition of the function compare.
Java
creates a class which implements Comparator on the fly and makes
cmp an instance of this class.
The class is local to the main1
function and is never given a name.
Here is the code:
Class Name: SetAdapter package: setThen insert the following content ( click to show ) Create the Java Class:
Class Name: NavSetAdapter package: setThen insert the following content ( click to show )
Class Name: SearchTreeSet package: tsetThen insert the following content ( click to show )
import tset.SearchTreeSet;
private class Node {
E data;
Node left, right;
Node(E data, Node left, Node right) {
this.data = data;
this.left = left;
this.right = right;
}
}
The data members of our class are these:
private Node root = null; private int size = 0; private Comparator<? super E> cmp = null;
new TreeSet<SomeComparableElementType>();or a Comparator must be passed to the constructor as in:
Comparator<SomeElement> my_cmp
= new Comparator<SomeElement>() {
public int compare(SomeElement lhs, SomeElement rhs) {
//...
}
}
new TreeSet<SomeElementType>(my_cmp);
In the former case the internal private data member cmp
is the default null value.
In the latter case, we would have
cmp = my_cmpmaking cmp a non-null value. In either case, the private helper function, myCompare, does the right thing:
private int myCompare(E lhs, E rhs) {
if (cmp == null) {
// no Comparator passed in constructor,
// assume element type is Comparable
return ((Comparable)lhs).compareTo(rhs);
} else {
// use the Comparator to compare elements
return cmp.compare(lhs, rhs);
}
}
public class SearchTreeSet<E extends Comparable<? super E>> ...which implies that its element type must be Comparable! This implementation unnecessarily restricts the element types a search tree can hold, a restriction not made by the java.util.TreeSet class in which we can pass a Comparator for a non-Comparable class.
boolean add(E elt);This is commonly implemented as a recursive helper function:
boolean add(Elt, Node n);called at the root:
add(elt, root);The recursive helper traverses the tree looking for an occurence of elt. If found, return true, otherwise, add a new Node to the tree containing elt and return false. At issue is the fact that a "found/not found" boolean value must be returned and pointer in the tree must be reset if not found.
private boolean add(E elt, Node & n) // n is a "reference" parameter
{
if (n == null) {
n = new Node(elt, null, null);
return false;
}
int comp = myCompare(elt, n.data);
if (comp < 0) {
return add(elt, n.left);
} else if (comp > 0) {
return add(elt, n.right);
} else {
return true;
}
}
If we had the ability to create the above function, we would simply call:
boolean found = add(elt, root);If necessary, root would be reset by the call to this add function and the return value would indicate whether the inserted value was already found. Unfortunately, since reference parameters do not exist in Java, we must work harder to create this effect. The first step is to ignore the boolean return value and concentrate getting the Node reference parameter replaced. We write (as in the textbook):
private Node add(E elt, Node n) {
if (n == null) {
return new Node(elt, null, null);
}
int comp = myCompare(elt, n.data);
if (comp < 0) {
n.left = add(elt, n.left);
} else if (comp > 0) {
n.right = add(elt, n.right);
}
return n;
}
Given this version of add, the call must be written:
root = add(elt, root);Of the parameter-passing choices mentioned above, this is of the first style. You can see this style repeated in the statements:
n.left = add(elt, n.left); n.right = add(elt, n.right); return n;The "proof" that this Java-based algorithm works is by induction. Assuming root is the top of a binary search tree, The claim is that the statement
n = add(elt, n);correctly inserts e into the search tree rooted at Node n
Proof:
if n == null, clearly it works
if n != null, and elt is found at n, it works also
otherwise, the algorithm correctly calls one of these two:
n.left = add(elt, n.left) or n.right = add(elt, n.right)
which, by the inductive assumption, must work,
in that case n itself is not changed.
Boolean bool = new Boolean(true);the internal boolean value can never be changed. All the standard java.lang wrapper classes are immutable. Finding a suitable mutable boolean object is somewhat of a challenge. We could easily create what is needed ourselves like this:
public class MyBoolean {
private boolean value;
public MyBoolean( boolean value ) { this.value = value; }
public MyBoolean( ) { this(false); }
public boolean get() { return value; }
public void set(boolean value) { this.value = value; }
}
but we'd like to
use something that is already built into Java. Given that
constraint, the class
java.util.concurrent.atomic.AtomicBoolean;suits our needs, although it is intended for other, more complex, situations involving multiple threads. Its member functions give us just what we need:
AtomicBoolean bool = new AtomicBoolean(); // initial value is optional bool.set( either-true-or-false ); // set value to true or false boolean b = bool.get() // retrieve the internal valueUsing this, we can now rewrite add to achieve our goal:
private Node add(E elt, Node n, AtomicBoolean found) {
if (n == null) {
found.set(false);
return new Node(elt, null, null);
}
int comp = myCompare(elt, n.data);
if (comp < 0) {
n.left = add(elt, n.left, found);
} else if (comp > 0) {
n.right = add(elt, n.right, found);
} else {
found.set(true);
}
return n;
}
The code of the public add function goes like this:
public boolean add(E elt) {
AtomicBoolean found = new AtomicBoolean();
root = add(elt, root, found);
if (!found.get()) {
++size;
}
return !found.get();
}
In particular the public function's return value is the
opposite of the private helper's boolean "return" value.
The public function wants to say "true" if the
element was not found (and therefore inserted), and "false" if
the element was found (and not inserted).
visit the root visit the left subtree recursively visit the right subtree recursively
visit the left subtree recursively visit the root visit the right subtree recursively
visit the left subtree recursively visit the right subtree recursively visit the root
visit the root visit the children of the root visit the children's children ...
inorderOutput(root);
// using the function ...
void inorderOutput(Node n) {
if (n == null)
return;
inorderOutput(n.left); // left
System.out.println(n.data); // root
inorderOutput(n.right); // right
}
Consider the effect on the following binary search tree:
inorder listing: 5 15 17 18 20 22 25 27 30If you're trying to reveal the structure of the tree you also want to add indentation corresponding to a node's level. To do this, modify the code to the following:
inorderOutput(root,0);
// using the function ...
void inorderOutput(Node n, int level) {
if (n == null)
return;
inorderOutput(n.left, level+1);
// print indentation corresponding to current level
System.out.println(n.data);
inorderOutput(n.right, level+1);
}
Assuming that "print indentation corresponding to current level" means
print 3*level blanks, the output would be this:
5
15
17
18
20
22
25
27
30
This yields a pretty good correspondence to the actual tree,
but it is easier to "see" the tree
if one used a reverse-inorder printing
corresponding the "right, root, left:"
reverseInorderOutput(root,0);
// using the function ...
void reverseInorderOutput(Node n, int level) {
if (n == null)
return;
reverseInorderOutput(n.right, level+1);
// print indentation corresponding to current level
System.out.println(n.data);
reverseInorderOutput(n.left, level+1);
}
Compare the output of this function with the actual tree:
30
27
25
22
20
18
17
15
5
|
|
preorder listing: 20 15 5 18 17 30 25 22 27 postorder listing: 5 17 18 15 22 27 25 30 20
public String inorderOutput(); public String preorderOutput(); public String preorderOutput(); public String reverseInorderOutput();Double-check that you have the import line:
import tset.SearchTreeSet;Then add main4 to the Main class, and have main call it:
public static void main4(String[] args) {
SearchTreeSet<Integer> tree = new SearchTreeSet<Integer>();
int num_samples = 10;
Random rand = new Random();
for (int i = 0; i < num_samples; ++i) {
tree.add(rand.nextInt(2 * num_samples));
}
System.out.println("-------- reverse inorder --------");
tree.reverseInorderOutput();
System.out.println("-------- preorder --------");
tree.preorderOutput();
System.out.println("-------- postorder --------");
tree.postorderOutput();
}
n.right = removeMin( n.right );using the recursive function:
Node removeMin( Node n ) {
if (n.left == null)
return n.right;
else {
n.left = removeMin( n.left );
return n;
}
}
In order to capture the minimum
value, we could, as is done in the textbook,
call two operations, one to find the minimum value, a second to
remove it. This is probably acceptable; however it is inefficient
because we are navigating down to the same point in the tree twice.
As an alternative, we will use a wrapper object around a generic type
to capture the minimum value. As before, we could easily define
such a class ourselves, but we prefer what is already written
via the class:
java.util.concurrent.atomic.AtomicReference<E>The modified code should capture the value prior to deleting the node:
private Node removeMin(Node n, AtomicReference<E> save) {
if (n.left == null) {
save.set(n.data);
return n.right;
} else {
n.left = removeMin(n.left, save);
return n;
}
}
After executing:
AtomicReference<E> save = new AtomicReference<E>(); n.right = removeMin( n.right, save );we can obtain the captured value as save.get() and replace Node n's value with it:
n.data = saved.get();
AtomicBoolean found = new AtomicBoolean(); root = remove( elt, root, found );Like add, we navigate down from the root looking for an occurrence of elt. The base case is an empty tree in which we do nothing. If the element is found, then the "hard" case is when the node has both children. At this point we can choose to do either removeMin or removeMax. There are two reasonable ways to proceed:
private Random flipACoin = new Random();
private Node remove(E elt, Node n, AtomicBoolean found) {
if (n == null) {
found.set(false);
return null;
}
int comp = myCompare(elt, n.data);
if (comp < 0) {
n.left = remove(elt, n.left, found);
return n;
}
if (comp > 0) {
n.right = remove(elt, n.right, found);
return n;
}
found.set(true);
if (n.left == null) {
return n.right;
}
if (n.right == null) {
return n.left;
}
// the most difficult situation is upon us!!
AtomicReference<E> save = new AtomicReference<E>();
boolean choose_min = (flipACoin.nextInt(2) == 1);
if (choose_min) {
n.right = removeMin(n.right, save);
}
else {
n.left = removeMax(n.left, save);
}
n.data = save.get();
return n;
}
Other alternatives might be to try to make a "best" choice between
removeMin or removeMax
based on, say, the height of the left and right subtrees. To make
such a modified algorithm efficient you should have an extra
field in the Node class which keeps track of the height of
the subtree at that node; furthermore, adds and removes would have
to maintain this height field correctly.
AtomicBoolean found = new AtomicBoolean();
root = remove(elt, root, found);
if (found.get()) {
--size;
}
return found.get(); // if found, a successful removal
Note the semantic
difference between add and remove
in that finding the element returns false for
add and true for remove.
The actual public remove operation in full is this:
public boolean remove(Object obj) {
if (isEmpty()) {
return false;
}
E elt = (E) obj;
AtomicBoolean found = new AtomicBoolean();
root = remove(elt, root, found);
if (found.get()) {
--size;
}
return found.get();
}
The prototype of remove(Object obj) forces us to cast obj
to the
generic type E. This which will generate an exception
if obj is not of type
E to begin with! Alternatively, you could test
if (obj instanceOf E)and return false if this fails. However Java's java.util.TreeSet implementation appears to prefer the former approach of throwing an exception on type mismatch.
public static void main5(String[] args) {
SearchTreeSet<Integer> tree = new SearchTreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
int num_samples = 10;
Random rand = new Random();
for (int i = 0; i < num_samples; ++i) {
int n = rand.nextInt(2 * num_samples);
list.add(n);
tree.add(n);
}
System.out.println("---------------");
tree.reverseInorderOutput();
System.out.println("---------------");
int pos = rand.nextInt(num_samples);
int num = list.get(pos);
System.out.println("remove: " + num);
tree.remove(num);
System.out.println("---------------");
tree.reverseInorderOutput();
System.out.println("---------------");
}