You are on page 1of 27

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; Im 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 its 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 privatehowever, 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 dont want this onewhy 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 dont 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); }

Heres 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

Naive setLeftChild
Remember that, in this design, a node also has a link to its parent This version of
setLeftChild

Before: new child left child

parent right child

does not preserve the validity of the data structure

After: new child left child

parent right child

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 searchO(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? Its a judgment callhow 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 dont seem to add any value to the code There are two purposes:
1. To prevent careless or malicious access to the object
Youve 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 isnt 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


Its 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


Ive 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 Heres what I have:
/** Breaks the connection between this node and its * parent. */ public void detach() { ... }
18

detach()
After all weve 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


Its always a good idea to write the following method (simplifies debugging):
public String toString()

Because a binary tree isnt terribly easy to read when its 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 doesnt show the shape of the binary tree To keep track of indentation, we need either a global variable (bad) or a parameter (OK) I dont 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; 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

left subtree

right subtree

Final comments
I didnt think of everything when I first wrote setLeftChildmy first version was pretty sloppy
Maybe there are still some problems Ive 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 wouldnt have been so fussy However, even for just one program, using setters and getters is always a good idea Its impressively hard to tell beforehand how much a program is going to be used

26

The End

27

You might also like