A Binary Tree ADT Fields The definition of
A Binary Tree ADT
Fields • The definition of a binary tree pretty much requires the following fields: Object value; Binary. Tree left. Child; Binary. Tree right. Child; • I also wanted to have this additional field: Binary. Tree 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, left. Child, right. Child, 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 Binary. Tree get. Left. Child() public void set. Left. Child(Binary. Tree child) public Binary. Tree get. Right. Child() public void set. Right. Child(Binary. Tree child) public Binary. Tree get. Parent() public void set. Parent(Binary. Tree parent) • We don’t want this one—why not? – Have to add parameter to say which child it will be – We can use set. Left. Child or set. Right. Child instead public Object get. Value() public void set. Value(Object value) 4
Header nodes With a header: Binary. Tree. Header my. Tree Without a header: Binary. Tree my. Tree • 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 Binary. Tree(Object value, Binary. Tree left. Child, Binary. Tree right. Child) {. . . } • In addition, we need a no-argument constructor – This is a requirement for serializable objects – public Binary. Tree() { this(null, null); } • Here’s a third constructor that I found convenient: – public Binary. Tree(Object value) { this(value, null); } 6
public void set. Left. Child(Binary. Tree child) • Here is the obvious code for this method: public void set. Left. Child(Binary. Tree new. Child) { left. Child = new. Child; } • Is there anything wrong with this code? • Hint: yes 7
Naive set. Left. Child • 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 set. Left. Child does not preserve the validity of the data structure After: new child parent left child right child 8
public void set. Left. Child(Binary. Tree child) • Here is the improved code for this method: public void set. Left. Child(Binary. Tree new. Child) { left. Child. parent = null; left. Child = new. Child; new. Child. parent = this; } Null. Pointer. Exception • 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 set. Left. Child(Binary. Tree child) • Here is even more improved code for this method: public void set. Left. Child(Binary. Tree new. Child) { left. Child. parent = null; left. Child = new. Child; if (new. Child != null) { new. Child. parent = this; } Null. Pointer. Exception } • Do you see any more problems? • What if the left child was originally null? 10
public void set. Left. Child(Binary. Tree child) • Here is yet another version of this method: public void set. Left. Child(Binary. Tree new. Child) { if (left. Child != null) { left. Child. parent = null; } left. Child = new. Child; if (new. Child != null) { new. Child. parent = this; } } • Now is there anything wrong? • What if the new child already has a parent? 11
public void set. Left. Child(Binary. Tree child) • And yet again: • public void set. Left. Child(Binary. Tree new. Child) { if (left. Child != null) { left. Child. parent = null; } left. Child = new. Child; if (new. Child != null) { if (new. Child. parent != null) { new. Child. parent. left. Child = null; } new. Child. parent = this; } } • Now is there anything wrong? 12
public void set. Left. Child(Binary. Tree child) • Bad assumption: it was previously a left child • public void set. Left. Child(Binary. Tree new. Child) { if (left. Child != null) { left. Child. parent = null; } left. Child = new. Child; if (new. Child != null) { if (new. Child. parent. left. Child == new. Child) new. Child. parent. left. Child = null; else new. Child. parent. right. Child = null; } new. Child. 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 set. Left. Child(Binary. Tree new. Child) { left. Child = new. Child; } We might have felt this method is “silly” and not bothered with it 15
Taking stock • Are the constructors and mutators (set. Xxx 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 get. Xxx 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 (left. Child==null && right. Child==null), but this is so commonly needed that we might as well supply the methods: – public boolean is. Root() – public boolean is. Leaf() • 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 set. Left. Child, detach is pretty trivial: public void detach() { if (parent != null) { if(parent. left. Child == this) parent. left. Child = null; else parent. right. Child = 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 set. Left. Child(Binary. Tree new. Child) { if (left. Child != null) left. Child. detach(); if (new. Child != null) new. Child. detach(); left. Child = new. Child; if (new. Child != null) new. Child. parent = this; } 20
Serialization methods • In case the binary tree needs to be serialized, I added the following two methods: • public static Binary. Tree load(String file. Name) throws IOException • public void save(String file. Name) throws IOException • We also need to note this in the class definition: public class Binary. Tree implements Serializable { 21
Other input-output methods • It’s always a good idea to write the following method (simplifies debugging): public String to. String() • 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
to. String public String to. String() { if (is. Leaf()) return value. to. String(); String. Buffer buffer = new String. Buffer(); buffer. append("[" + value + ", "); root if (left. Child == null) buffer. append("null"); left else buffer. append(left. Child. to. String()); subtree buffer. append(", "); if (right. Child == null) buffer. append("null"); right else buffer. append(right. Child. to. String()); subtree buffer. append("]"); return buffer. to. String(); } 23
print() • to. String 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) root private void print(String indent) { System. out. println(indent + value); if (is. Leaf()) return; left subtree right subtree } if (left. Child == null) { System. out. println(indent + " " + "null"); } else { left. Child. print(indent + " "); } if (right. Child == null) { System. out. println(indent + " " + "null"); } else { right. Child. print(indent + " "); } 25
Final comments • I didn’t think of everything when I first wrote set. Left. Child—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
- Slides: 27