A Binary Tree ADT

Download Report

Transcript A Binary Tree ADT

A Binary Tree ADT
Fields
• The definition of a binary tree pretty much requires
the following fields:
Object value;
BinaryTree leftChild;
BinaryTree rightChild;
• I also wanted to have this additional field:
BinaryTree parent;
• I’m trying to define a general purpose binary tree, and
this might be needed in some applications
• Should these fields be public, private, or
somewhere in between?
2
Maintaining control
• It is the responsibility of any class to ensure the
safety and validity of its objects
• Is there any real harm in letting the fields of a
node in a binary tree be public?
– In other words, how easily can the binary tree be
damaged?
– I claim it’s quite easy, as I will demonstrate shortly
• In my design, leftChild, rightChild, and parent
are private, and I provide getters and setters
• For consistency, the value field should also be
private—however, this is not a validity issue
3
Getters and setters
• Here are the methods we have decided on so far:
public BinaryTree getLeftChild()
public void setLeftChild(BinaryTree child)
public BinaryTree getRightChild()
public void setRightChild(BinaryTree child)
public BinaryTree getParent()
public void setParent(BinaryTree parent)
• We don’t want this one—why not?
– Have to add parameter to say which child it will be
– We can use setLeftChild or setRightChild instead
public Object getValue()
public void setValue(Object value)
4
Header nodes
With a header:
BinaryTreeHeader myTree
Without a header:
BinaryTree myTree
• I don’t like to use a header node for a binary tree,
because that gets in the way of treating the
subtrees as binary trees in their own right
5
Constructors
• There is an obvious three-argument constructor
that we need:
– public BinaryTree(Object value,
BinaryTree leftChild,
BinaryTree rightChild) { ... }
• In addition, we need a no-argument constructor
– This is a requirement for serializable objects
– public BinaryTree() {
this(null, null, null);
}
• Here’s a third constructor that I found convenient:
– public BinaryTree(Object value) {
this(value, null, null);
}
6
public void setLeftChild(BinaryTree child)
• Here is the obvious code for this method:
public void setLeftChild(BinaryTree newChild) {
leftChild = newChild;
}
• Is there anything wrong with this code?
• Hint: yes
7
Naive setLeftChild
• Remember that, in
this design, a node
also has a link to
its parent
Before:
new child
parent
left child
right child
• This version of
setLeftChild
does not
preserve the
validity of the
data structure
After:
new child
parent
left child
right child
8
public void setLeftChild(BinaryTree child)
• Here is the improved code for this method:
public void setLeftChild(BinaryTree newChild) {
leftChild.parent = null;
leftChild = newChild;
newChild.parent = this;
}
NullPointerException
• Now is this code correct?
• Is it reasonable to set the left child to null?
– A binary tree can be empty, so yes
• What happens if we try to do so?
9
public void setLeftChild(BinaryTree child)
• Here is even more improved code for this method:
public void setLeftChild(BinaryTree newChild) {
leftChild.parent = null;
leftChild = newChild;
if (newChild != null) {
newChild.parent = this;
}
NullPointerException
}
• Do you see any more problems?
• What if the left child was originally null?
10
public void setLeftChild(BinaryTree child)
• Here is yet another version of this method:
public void setLeftChild(BinaryTree newChild) {
if (leftChild != null) {
leftChild.parent = null;
}
leftChild = newChild;
if (newChild != null) {
newChild.parent = this;
}
}
• Now is there anything wrong?
• What if the new child already has a parent?
11
public void setLeftChild(BinaryTree child)
• And yet again:
• public void setLeftChild(BinaryTree newChild) {
if (leftChild != null) {
leftChild.parent = null;
}
leftChild = newChild;
if (newChild != null) {
if (newChild.parent != null) {
newChild.parent.leftChild = null;
}
newChild.parent = this;
}
}
• Now is there anything wrong?
12
public void setLeftChild(BinaryTree child)
• Bad assumption: it was previously a left child
•
public void setLeftChild(BinaryTree newChild) {
if (leftChild != null) {
leftChild.parent = null;
}
leftChild = newChild;
if (newChild != null) {
if (newChild.parent != null) {
if (newChild.parent.leftChild == newChild)
newChild.parent.leftChild = null;
else
newChild.parent.rightChild = null;
}
newChild.parent = this;
}
}
• Now is there anything wrong?
13
How much is enough?
• What if the new child is already a node elsewhere in
the binary tree?
• Do we need to search the tree to find out?
– This could be a somewhat expensive search—O(n)
– All our previous modifications have been O(1), that is,
constant time
• I think that this is a problem only if the new child is
an ancestor of the node it is to be added to
–
–
–
–
This is an O(log n) search, if the tree is balanced
Is this worth doing?
It’s a judgment call—how safe does our code need to be?
The answer depends on what the code is for
14
Getters and setters
•
•
Getters and setters are annoying to write, especially
when they don’t seem to add any value to the code
There are two purposes:
1. To prevent careless or malicious access to the object
•
You’ve just seen an example of this
2. To preserve flexibility, in case you might want to change
the object some time in the future
•
For example, if we did not originally have the parent link, the
following code would have been enough:
public void setLeftChild(BinaryTree newChild) {
leftChild = newChild;
}
We might have felt this method is “silly” and not bothered with it
15
Taking stock
• Are the constructors and mutators (setXxx
methods) adequate to construct any binary tree?
– Yes, provided you start from the root and build the
binary tree by working downwards
– However, there isn’t much support for changing an
existing binary tree
• Can we access all the data in the tree?
– Yes, with the getXxx methods
– However, it might be nice to provide convenience
methods for testing if we are at the root or a leaf
16
Convenience accessor methods
• It’s easy enough to test if we are at a root
(parent==null) or at a leaf (leftChild==null &&
rightChild==null), but this is so commonly
needed that we might as well supply the methods:
– public boolean isRoot()
– public boolean isLeaf()
• Besides, using these methods makes the code
more readable
17
Changing the binary tree
• I’ve seen some pretty complicated methods for
doing things in the binary tree
• The kind of changes that are needed in any given
program are probably very problem-specific
• What I think we need is not a collection of
complicated methods, but some very simple
methods we can put together in complex ways
• Here’s what I have:
/** Breaks the connection between this node and its
* parent.
*/
public void detach() { ... }
18
detach()
• After all we’ve been through with setLeftChild,
detach is pretty trivial:
public void detach() {
if (parent != null) {
if(parent.leftChild == this)
parent.leftChild = null;
else
parent.rightChild = null;
parent = null;
}
}
19
Using existing methods
• It is frequently advantageous to use some of the
methods of a class when implementing other
methods
public void setLeftChild(BinaryTree newChild) {
if (leftChild != null) leftChild.detach();
if (newChild != null) newChild.detach();
leftChild = newChild;
if (newChild != null) newChild.parent = this;
}
20
Serialization methods
• In case the binary tree needs to be serialized, I added
the following two methods:
• public static BinaryTree load(String fileName)
throws IOException
• public void save(String fileName) throws
IOException
• We also need to note this in the class definition:
public class BinaryTree implements Serializable {
21
Other input-output methods
• It’s always a good idea to write the following
method (simplifies debugging):
public String toString()
• Because a binary tree isn’t terribly easy to read
when it’s shown linearly, I also wrote the following
method to give me a nicely indented tree:
public void print()
22
toString
public String toString() {
if (isLeaf()) return value.toString();
StringBuffer buffer = new StringBuffer();
buffer.append("[" + value + ", ");
root
if (leftChild == null) buffer.append("null");
left
else buffer.append(leftChild.toString());
subtree
buffer.append(", ");
if (rightChild == null) buffer.append("null");
right
else buffer.append(rightChild.toString());
subtree
buffer.append("]");
return buffer.toString();
}
23
print()
• toString is handy for producing condensed,
single-line output, but doesn’t show the shape of
the binary tree
• To keep track of indentation, we need either a
global variable (bad) or a parameter (OK)
• I don’t want the user to have to supply this
parameter, so:
public void print() {
print("");
}
private void print(String indent) { ... }
24
print(String indent)
private void print(String indent) {
System.out.println(indent + value);
root
if (isLeaf()) return;
left subtree
right subtree
}
if (leftChild == null) {
System.out.println(indent + " " + "null");
}
else {
leftChild.print(indent + " ");
}
if (rightChild == null) {
System.out.println(indent + " " + "null");
}
else {
rightChild.print(indent + " ");
}
25
Final comments
• I didn’t think of everything when I first wrote
setLeftChild—my first version was pretty sloppy
– Maybe there are still some problems I’ve overlooked
– No program is ever perfect
– Corrections are, as always, welcome
• I took some care because this is intended as an
example of ADT design
– For just using it in a particular (small) program, I
wouldn’t have been so fussy
– However, even for just one program, using setters and
getters is always a good idea
– It’s impressively hard to tell beforehand how much a
program is going to be used
26
The End
27