We'll use a simple application to demonstrate that trees can be much faster than lists:
1. for each word w
2. w' = reverse (w)
3. if w' is a word
4. count = count + 1
5. endif
6. endfor
7. return count
// Build the data structure and put all the words in it.
1. DS = create new data structure
2. for each word w
3. add w to DS
4. endfor
// Now check each possible reversal.
5. for each word w
2. w' = reverse (w)
3. if DS.contains (w')
4. count = count + 1
5. endif
6. endfor
7. return count
Next, let's use Java's tree data structure and compare:
import java.util.*;
public class WordReversals {
public static void main (String[] argv)
{
// Fetch the dictionary.
String[] words = WordTool.getDictionary ();
// Compare a tree data structure with ArrayList and LinkedList.
findReversalsUsingTree (words);
findReversalsUsingArrayList (words);
findReversalsUsingLinkedList (words);
}
static void findReversalsUsingTree (String[] words)
{
long startTime = System.currentTimeMillis();
// Count such words.
int count = 0;
// First put all words into a tree.
TreeSet wordSet = new TreeSet ();
for (int i=0; i < words.length; i++) {
wordSet.add (words[i]);
}
// Now perform the search for reversals.
for (int i=0; i < words.length; i++) {
String reverseStr = reverse (words[i]);
if (wordSet.contains (reverseStr)) {
count ++;
System.out.println (words[i]);
}
}
// How much time has elapsed?
long timeTaken = System.currentTimeMillis() - startTime;
System.out.println ("Using a tree: count=" + count + " timeTaken=" + timeTaken);
}
static void findReversalsUsingArrayList (String[] words)
{
// ... similar except that we use an ArrayList ...
}
static void findReversalsUsingLinkedList (String[] words)
{
// ... similar except that we use a LinkedList ...
}
static String reverse (String str)
{
// ... Reverse a string. This method is used above.
}
}
Recall from linked lists:
class ListItem {
String data; // The data to be stored.
ListItem next; // A pointer to the next node in the list.
}
class ListItem {
String data; // The data to be stored.
ListItem next; // A pointer to the next node in the list.
ListItem prev; // Points to previous item in list.
}
Consider this program that builds a linked structure: (source file)
class Node {
String data; // The data to be stored.
Node left; // Two pointers.
Node right;
}
public class StrangeStructure {
public static void main (String[] argv)
{
// Step 1:
Node root = new Node ();
root.data = "Ewok";
// Step 2:
root.right = new Node ();
root.right.data = "Gungan";
// Step 3:
root.left = new Node ();
root.left.data = "Aqualish";
// Step 4:
root.left.left = new Node ();
root.left.left.data = "Amanin";
// Step 5:
root.left.right = new Node ();
root.left.right.data = "Cerean";
}
}
We'll start our discussion of trees by looking at some simple examples:





Henceforth, we will be interested only in ordered binary trees.
Search in a binary tree:
1. Start with the root.
2. if the given value is equal to the current node's value
3. return found
4. elseif given value < node's value
5. explore left subtree (recursively)
6. else
7. explore right subtree (recursively)
8. endif
Algorithm: recursiveSearch (node, element)
// Check to see if the given element is in the node we're examining.
1. if element = node.value
2. return found
3. endif
// Otherwise, search the appropriate subtree (left or right)
4. if element < node.value
5. return recursiveSearch (node.left, element)
6. else
7. return recursiveSearch (node.right, element)
8. endif
Insertion of new elements:







Let's now look at implementation:
public class BinaryTreeInt {
public void add (int k)
{
// ... Add an integer to the tree ...
}
public int size ()
{
// ... return the number of items added thus far ...
}
public boolean contains (int k)
{
// ... search for k in the tree ...
}
}
public static void main (String[] argv)
{
// Make an instance of the tree.
BinaryTreeInt tree = new BinaryTreeInt ();
// Add stuff.
tree.add (7);
tree.add (9);
tree.add (3);
// ...
// Do a search.
if ( tree.contains (11) ) {
System.out.println ("Tree contains 11");
}
}
class TreeNode {
int data;
TreeNode left; // Pointer to the left child.
TreeNode right; // Pointer to the right child.
}
Thus, the tree consisting of elements '7', '3' and '9' looks like:

In more detail (with sample memory addresses):

import java.util.*;
// Each node of the tree is an instance of the class TreeNode.
class TreeNode {
int data;
TreeNode left; // Pointer to the left child.
TreeNode right; // Pointer to the right child.
}
public class BinaryTreeInt {
TreeNode root = null; // Root of the tree.
int numItems = 0; // We'll keep track of how many elements we've added so far.
public void add (int k)
{
// If empty, create new root.
if (root == null) {
root = new TreeNode (); // Note: root.left and root.right are initialized to null
root.data = k;
numItems ++;
return;
}
// Search to see if it's already there.
if ( contains (k) ) {
// Handle duplicates.
return;
}
// If this is a new piece of data, insert into tree.
recursiveInsert (root, k);
numItems ++;
}
void recursiveInsert (TreeNode node, int k)
{
// Compare input data with data in current node.
if (k < node.data) {
// It's less. Go left if possible, otherwise we've found the correct place to insert.
if (node.left != null) {
recursiveInsert (node.left, k);
}
else {
node.left = new TreeNode ();
node.left.data = k;
}
}
// Otherwise, go right.
else {
// It's greater. Go right if possible, otherwise we've found the correct place to insert.
if (node.right != null) {
recursiveInsert (node.right, k);
}
else {
node.right = new TreeNode ();
node.right.data = k;
}
}
}
public int size ()
{
return numItems;
}
public boolean contains (int k)
{
if (numItems == 0) {
return false;
}
return recursiveSearch (root, k);
}
boolean recursiveSearch (TreeNode node, int k)
{
// If input string is at current node, it's in the tree.
if (k == node.data) {
// Found.
return true;
}
// Otherwise, navigate further.
if (k < node.data) {
// Go left if possible, otherwise it's not in the tree.
if (node.left == null) {
return false;
}
else {
return recursiveSearch (node.left, k);
}
}
else {
// Go right if possible, otherwise it's not in the tree.
if (node.right == null) {
return false;
}
else {
return recursiveSearch (node.right, k);
}
}
}
}
What do we need to change to create a binary tree for strings?
class TreeNode {
String data; // Changed from int to String
TreeNode left;
TreeNode right;
// ...
}
public class BinaryTreeString {
public void add (String data)
{
// ...
}
public boolean contains (String str)
{
// ...
}
}
public class BinaryTreeString {
public void add (String data)
{
// ...
}
public boolean contains (String str)
{
if (numItems == 0) {
return false;
}
return recursiveSearch (root, str);
}
boolean recursiveSearch (TreeNode node, String str)
{
if ( str.compareTo (node.data) == 0 ) {
// Found.
return true;
}
// Otherwise, navigate further.
if ( str.compareTo (node.data) < 0 ) {
// Go left if possible, otherwise it's not in the tree.
if (node.left == null) {
return false;
}
else {
return recursiveSearch (node.left, str);
}
}
else {
// Go right if possible.
// ... similar to above ...
}
}
}
Let us compare search in lists vs. trees:
The height of a full tree:
Balanced trees:
Back to the analysis:
| Data structure | Insertion | Search |
| LinkedList | O(1) | O(n) |
| ArrayList | O(n)? | O(n) |
| SortedList | O(n) | O(log(n)) |
| BinaryTree (balanced) | O(log(n)) | O(log(n)) |
About the Map ADT:
public class BinaryTree {
public void add (String data)
{
// ...
}
public boolean contains (String str)
{
// ...
}
}
This data structure stores strings
public class BinaryTree {
public void add (String key, Object value)
{
// ...
}
public boolean contains (String key)
{
// ...
}
public Object getValue (String key)
{
// ...
}
}
Thus, there are three operations:
Consider an example:


An example with trees:

public class BinaryTreeMapExample {
public static void main (String[] argv)
{
// Create an instance of the map data structure.
BinaryTreeMap tree = new BinaryTreeMap ();
// Add name and fierceness-rating (1-10)
tree.add ("Ewok", 3);
tree.add ("Aqualish", 6);
tree.add ("Gungan", 2);
tree.add ("Amanin", 8);
tree.add ("Jawa", 6);
tree.add ("Hutt", 7);
tree.add ("Cerean", 4);
int rating = tree.getValue ("Hutt");
System.out.println ("Rating for Hutt: " + rating);
}
}
Let's examine the code for BinaryTreeMap: (source file)
import java.util.*;
class TreeNode {
String key; // The key-value pair.
int value;
TreeNode left; // The usual left-child, right-child pointers.
TreeNode right;
}
public class BinaryTreeMap {
TreeNode root = null;
int numItems = 0;
public void add (String key, int value)
{
// If empty, create new root.
if (root == null) {
root = new TreeNode ();
// Store both key and value:
root.key = key;
root.value = value;
numItems ++;
return;
}
// Search to see if it's already there.
if ( contains (key) ) {
// Handle duplicates.
return;
}
// If this is a new piece of data, insert into tree.
recursiveInsert (root, key, value);
numItems ++;
}
void recursiveInsert (TreeNode node, String key, int value)
{
// Compare input key with key in current node: comparisons are only with keys.
if ( key.compareTo (node.key) < 0 ) {
// It's less. Go left if possible, otherwise we've found the correct place to insert.
if (node.left != null) {
recursiveInsert (node.left, key, value);
}
else {
node.left = new TreeNode ();
node.left.key = key; // Store both key and value.
node.left.value = value;
}
}
// Otherwise, go right.
else {
// It's greater. Go right if possible, otherwise we've found the correct place to insert.
if (node.right != null) {
recursiveInsert (node.right, key, value);
}
else {
node.right = new TreeNode ();
node.right.key = key; // Store both key and value.
node.right.value = value;
}
}
}
public int size ()
{
return numItems;
}
public boolean contains (String str)
{
if (numItems == 0) {
return false;
}
TreeNode node = recursiveSearch (root, str);
if (node == null) {
return false;
}
return true;
}
public int getValue (String key)
{
if (numItems == 0) {
return -1;
}
TreeNode node = recursiveSearch (root, key);
if (node == null) {
return -1;
}
return node.value;
}
TreeNode recursiveSearch (TreeNode node, String key)
{
// If input key is at current node, it's in the tree.
if ( key.compareTo (node.key) == 0 ) {
// Found.
return node;
}
// Otherwise, navigate further.
if ( key.compareTo (node.key) < 0 ) {
// Go left if possible, otherwise it's not in the tree.
if (node.left == null) {
return null;
}
else {
return recursiveSearch (node.left, key);
}
}
else {
// Go right if possible, otherwise it's not in the tree.
if (node.right == null) {
return null;
}
else {
return recursiveSearch (node.right, key);
}
}
}
} //end-BinaryTreeMap
Note:
Sometimes we wish to store a more complex value
=>
An object, for instance.

class TribeInfo {
String name;
int fierceness;
String planet;
// Constructor.
public TribeInfo (String name, int fierceness, String planet)
{
this.name = name;
this.fierceness = fierceness;
this.planet = planet;
}
}
public class BinaryTreeMapExample2 {
public static void main (String[] argv)
{
// Create an instance of our new object-version of a binary-tree map.
BinaryTreeMap2 tree = new BinaryTreeMap2 ();
// Put some key-value pairs inside.
TribeInfo info = new TribeInfo ("Ewok", 3, "Endor");
tree.add ("Ewok", info);
info = new TribeInfo ("Aqualish", 6, "Ando");
tree.add (info.name, info);
info = new TribeInfo ("Gungan", 2, "Naboo");
tree.add (info.name, info);
info = new TribeInfo ("Amanin", 8, "Maridun");
tree.add (info.name, info);
info = new TribeInfo ("Jawa", 6, "Tatooine");
tree.add (info.name, info);
info = new TribeInfo ("Hutt", 7, "Varl");
tree.add (info.name, info);
info = new TribeInfo ("Cerean", 4, "Cerea");
tree.add (info.name, info);
// Note: a cast is needed for conversion from Object to TribeInfo
// even though we know that a TribeInfo instance will be returned.
TribeInfo tInfo = (TribeInfo) tree.getValue ("Hutt");
System.out.println ("Info for Hutt: " + tInfo);
}
}
Let's now take a look at implementing the map: (source file)
import java.util.*;
class TreeNode {
String key;
Object value; // The value is now a generic object.
TreeNode left;
TreeNode right;
}
public class BinaryTreeMap2 {
TreeNode root = null;
int numItems = 0;
public void add (String key, Object value)
{
// ...
}
public int size ()
{
// ...
}
public boolean contains (String key)
{
// ...
}
public Object getValue (String key)
{
// ...
// Return value is an Object (the value)
}
}
Note:
What is an Object?
class MyOwnVeryObject { // A silly little object
int k;
public String toString ()
{
return ("k=" + k);
}
}
public class TestObject {
public static void main (String[] argv)
{
// Create an instance of the class defined above and set a value for one of the members.
MyOwnObject x = new MyOwnObject ();
x.k = 5;
// Invoke the toString() method:
System.out.println (x);
// Since MyOwnObject is also an Object, an Object variable can point to it.
Object obj = x;
// Invoke the toString() method: this calls the toString() method in x.
System.out.println (obj);
// Cast down from an Object variable into a MyOwnObject variable.
MyOwnObject y = (MyOwnObject) obj;
print (y);
// Casting can occur in a method call too.
print (x);
}
static void print (Object obj)
{
System.out.println (obj);
}
}
// Cast from return type (Object) into tInfo's type (TribeInfo).
TribeInfo tInfo = (TribeInfo) tree.getValue ("Hutt");
An alternative to handling keys and values separately is to create a single object that packages key-and-value:
public class KeyValuePair {
String key;
Object value;
public KeyValuePair (String s, Object v)
{
key = s;
value = v;
}
}
import java.util.*;
class TreeNode {
KeyValuePair kvp; // A tree node now stores the Key-Value pair as an object.
TreeNode left; // The usual pointers.
TreeNode right;
}
public class BinaryTreeMap3 {
public void add (KeyValuePair kvp)
{
// ...
}
public boolean contains (String key)
{
// ...
}
public KeyValuePair getKeyValuePair (String key)
{
// ...
}
}
Let's re-work our earlier map example to use KeyValuePair's: (source file)
class TribeInfo {
String name;
int fierceness;
String planet;
public TribeInfo (String name, int fierceness, String planet)
{
this.name = name;
this.fierceness = fierceness;
this.planet = planet;
}
} //end-TribeInfo
public class BinaryTreeMapExample3 {
public static void main (String[] argv)
{
// Create an instance.
BinaryTreeMap3 tree = new BinaryTreeMap3 ();
// Put some key-value pairs inside.
TribeInfo info = new TribeInfo ("Ewok", 3, "Endor");
KeyValuePair kvp = new KeyValuePair ("Ewok", info);
tree.add (kvp);
info = new TribeInfo ("Aqualish", 6, "Ando");
kvp = new KeyValuePair (info.name, info);
tree.add (kvp);
// This is more compact: create the instance in the method argument list.
info = new TribeInfo ("Gungan", 2, "Naboo");
tree.add ( new KeyValuePair (info.name, info) );
info = new TribeInfo ("Amanin", 8, "Maridun");
tree.add ( new KeyValuePair (info.name, info) );
info = new TribeInfo ("Jawa", 6, "Tatooine");
tree.add ( new KeyValuePair (info.name, info) );
info = new TribeInfo ("Hutt", 7, "Varl");
tree.add ( new KeyValuePair (info.name, info) );
// A little harder to read, but even more compact:
tree.add ( new KeyValuePair ("Cerean", new TribeInfo ("Cerean", 4, "Cerea") ) );
KeyValuePair kvpResult = tree.getKeyValuePair ("Hutt");
System.out.println ("Info for Hutt: " + kvpResult);
}
}
A map can also be implemented with a linked list:
public class OurLinkedListMap {
public void add (KeyValuePair kvp)
{
// ...
}
public boolean contains (String key)
{
// ...
}
public KeyValuePair getKeyValuePair (String key)
{
// ...
}
}
public boolean contains (String key)
{
if (front == null) {
return false;
}
// Start from the front and walk down the list. If it's there,
// we'll be able to return true from inside the loop.
ListItem listPtr = front;
while (listPtr != null) {
// Note: listPtr.kvp is the KeyValuePair instance.
if ( listPtr.kvp.key.equals(key) ) {
return true;
}
listPtr = listPtr.next;
}
return false;
}
Key ideas:


More details:
Storing strings:
Let's look at pseudocode:
Algorithm: insert (key, value)
Input: key-value pair
// Compute table entry:
1. entry = key.hashCode() mod numBuckets
2. if table[entry] is null
// No list present, so create one
3. table[entry] = new linked list;
4. table[entry].add (key, value)
5. else
6. // Otherwise, add to existing list
7. table[entry].add (key, value)
8. endif
Algorithm: search (key)
Input: search-key
// Compute table entry:
1. entry = key.hashCode() mod numBuckets
2. if table[entry] is null
3. return null
4. else
5. return table[entry].search (key)
6. endif
Finally, our implementation in Java: (source file)
public class OurHashMap {
int numBuckets = 100; // Initial number of buckets.
OurLinkedListMap[] table; // The hashtable.
int numItems; // Keep track of number of items added.
// Constructor.
public OurHashMap (int numBuckets)
{
this.numBuckets = numBuckets;
table = new OurLinkedListMap [numBuckets];
numItems = 0;
}
public void add (KeyValuePair kvp)
{
if ( contains (kvp.key) ) {
return;
}
// Compute hashcode and therefore, which table entry (list).
int entry = Math.abs(kvp.key.hashCode()) % numBuckets;
// If there's no list there, make one.
if (table[entry] == null) {
table[entry] = new OurLinkedListMap ();
}
// Add to list.
table[entry].add (kvp);
numItems ++;
}
public boolean contains (String key)
{
// Compute table entry using hash function.
int entry = Math.abs(key.hashCode()) % numBuckets;
if (table[entry] == null) {
return false;
}
// Use the contains() method of the list.
return table[entry].contains (key);
}
public KeyValuePair getKeyValuePair (String key)
{
// Similar to contains.
int entry = Math.abs(key.hashCode()) % numBuckets;
if (table[entry] == null) {
return null;
}
return table[entry].getKeyValuePair (key);
}
}
Analysis:
| Data structure | Insertion | Search |
| LinkedList | O(1) | O(n) |
| ArrayList | O(n)? | O(n) |
| SortedList | O(n) | O(log(n)) |
| BinaryTree (balanced) | O(log(n)) | O(log(n)) |
| Hashtable | O(1) | O(1) |
Caveats:
What we haven't covered in hashing: