CS211 Section Notes prepared by: Alvin Law (ajl56@cornell.edu) ********* * Trees * ********* Q: How do you keep a 211 student in the shower all day? A: Give him a bottle of shampoo which says "lather, rinse, repeat." * infinite loops =( * Overview: Welcome to the land of NullPointerException!!! Trees are acceleration structures. They are commonly used to speed up the efficiency of an algorithm. Think about a list again - in order to find a specific element in a list, we have to (at most) visit each node in the list. With a tree on the other hand, we could potentially intelligently skip the majority of nodes when looking for a specific element. This is clear when we consider that each node in a tree has (potentially) more than one child. We take and choose one child to follow, and the other child and his subtree is effectively ignored. Note that this is only possible if the objects in the tree are ordinal, or that they can be arranged in some type of order. We'll analyze the efficiency of trees later on in the course. A binary tree is the most common type of tree. Each node has up to two children. Commonly, the tree is arranged in an ordinal fashion, where all the nodes on the left of a node are less than the parent, and all the nodes on the right of a node are greater than the parent. General trees have the property that each node can have any amount of children connected to it. To implement this, a list is commonly used to store the children of each node. ********************************************************************** Recursion and Trees: Trees have an inherent nature to be recursive. That's because at each node, the problem is the same. Let's look at binary trees for example. At each node we are faced with the same problem in for any problem we want to solve. We can either utilize the node at hand's data, or we can continue looking in the left subtree or the right subtree: our node <~ do we do something now, or / \ in a subtree? left subtree right subtree Since each problem looks identical, this leads to the recursive case for the method or function. The base case is typically encountered at our leaf nodes. We check its children when necessary, and they are null. This leads to a general format for many tree associated methods and functions: returnType function(TreeCell t, other parameters) { if (t == null) { base case goes here } else { recursive case goes here } } Example (from notes) //compute height of tree using post-order walk public static int height(TreeCell t) { // our two base cases if (t == null) return –1; //height is undefined for empty tree if (isLeaf(t)) return 0; // our recursive case else return 1 + Math.max(height(t.getLeft()), height(t.getRight())); } Sometimes the base case is hidden more, but there will always be a check against null. ********************************************************************** Tree Traversal: Consider the following tree: 5 / \ 3 6 / \ \ 1 4 8 \ / \ 2 7 9 In order traversal (nodes in order): 1,2,3,4,5,6,7,8,9 Pre order traversal (nodes at first visit): 5,3,1,2,4,6,8,7,9 Post order traversal (nodes at last visit): 2,1,4,3,7,9,8,6,5 Implementing each traversal for a particular method generally follows a simple framework: returnType inOrder(TreeCell t, other parameters) { inOrder(t.getLeft(), other parameters); do stuff on t inOrder(t.getRight(), other parameters); } returnType preOrder(TreeCell t, other parameters) { do stuff on t preOrder(t.getLeft(), other parameters); preOrder(t.getRight(), other parameters); } returnType postOrder(TreeCell t, other parameters) { postOrder(t.getLeft(), other parameters); postOrder(t.getRight(), other parameters); do stuff on t } ********************************************************************** Methods with trees: // returns the number of nodes in the tree int size(TreeCell t) { if (t == null) // our base case, null trees have 0 nodes return 0; else return 1 + size(t.getLeft()) + size(t.getRight()); } // In this method, our BST has been filled with Integer objects, we // will sum up the values of the tree. This is an example of the // need to cast objects, otherwise the methods called are // unrecognized. int sum(TreeCell t) { if (t == null) // our base case null check - null node adds 0 to sum return 0; else return ((Integer)t.getDatum()).intValue() + sum(t.getLeft()) + sum(t.getRight()); } // returns the number of instances of o in t - the tree need not to be // sorted (if the tree were sorted, however, we could intelligently // rule out parts of the tree from our search - try it! Also, the equals // method should generally be overriden by the datum class. If it isn't, // the class Object's equals method would be called, which would just // compare reference addresses, which usually is not what we want. int instancesOf(TreeCell t, Object o) { if (t == null) // base case return 0; else { if (t.getDatum().equals(o)) return 1 + instancesOf(t.getLeft()) + instancesOf(t.getRight()); else return instancesOf(t.getLeft()) + instancesOf(t.getRight()); } } // insert object o into tree // For this example, I'm using binary search trees (BST)- nodes are // in ascending order at all times, and each node has at most two // children. Since it is a search tree, the objects in the nodes must // impelement the Comparable interface. We have access to // a function called compareTo(Object o), which returns: // 0 if the two objects are equal // -1 if our object is less than o // 1 if our object is greater than o void insert(TreeCell t, Object o) { if (((Integer)t.getDatum()).compareTo(o) > 0) // our object is "less than" t's object { if (t.getLeft() == null) // no child here, so insert (base case) t.setLeft(new TreeCell(o)); else insert(t.getLeft(), o); // child, so continue in left subtree } else { if (t.getRight() == null) // no child here, so insert (base case) t.setRight(new TreeCell(o)); else insert(t.getRight(), o); // child, so continue in right subtree } }