Lab 7: Binary Search Trees

Deadline Note

This lab is a bit more complex than some of the previous labs, thus you will have until Wednesday (2013/11/13) just before midnight (23:59:59) to complete this lab.

Goals for this lab

By the time you have completed this lab, you should

  • learn about Tree ADT
  • implement parts of the Binary Search Tree ADT
  • get familiar with using Stack and Queue ADT
  • understand depth first and breadth first tree traversals

The purpose of this lab is for you to get familiar with the tree ADT. A tree is another basic abstract data type, which is widely used in computer science. Because of the tree-like structure, there are multiple ways to traverse data in a tree. In this lab you will get familiar with and implement some frequently used BST operations.

Lab pairing

For this lab you may work with a partner. To facilitate your submissions as a pair you MUST pair up using the submission system. When you visit the project page from where you can view all your submission you should see a link to a page where you can join a group. Of the two people in the group, one of you must invite the other, and the other must confirm the invitation. Once you group up, any submissions that either you, or your partner have already made will be visible to each other in addition to all future submissions.

Lab preparation

After logging in, create the directory for this lab:

mkdir -p cs24/lab7

Change into the lab7 directory and copy the files you will need for this lab:

cd cs24/lab7
cp ~bboe/public_html/cs24_f13/code/lab7/* .

Trees and Binary Search Trees

Trees

A tree, in general is a data structure that, similar to a List, consists of nodes with data and, this time, one or more pointers to other nodes. However, a tree structure has an important requirement: no two pointers point to the same node. This property is important, because tree must not have any loops. From the requirement it follows that each node may have only one parent (the node, that is pointing to it), except for the very top one, which is called root. The root has no parents, (similar to the head node in the List). Nodes that don’t have any children (no nodes pointed to) are called leaf nodes. (Similar to the tail node in the List). More about the trees are on the wiki page.

Binary Trees, Binary Search Trees

There is a special type of tree called the Binary Tree. Each of its nodes must have no more than two children, usually distinguished as left and right. Among the Binary Trees there are Binary Search Trees, for which there are additional requirements, enforcing data in the tree to be structured in some way. First of all, there must be no duplicate nodes in the tree (nodes with the same data value). Secondly, for every node its left subtree (a tree that has its left child as a root) should consist only of nodes whose values (the data contained within it) are all less than its value. Additionally, for every node its right subtree (a tree that has its right child as a root) should consist only of nodes with values greater than its value. Finally, left and right subtrees should also be a BST themselves. More on BSTs. Below is an example BST created when inserting the following integers: 5, 3, 7, 4, 2, 6, 8

Insert the 5:

     5

Insert the 3:

     5
   -/
  3

Insert the 7:

     5
   -/ \-
  3     7

Insert the 4:

     5
   -/ \-
  3     7
   \
    4

Insert the 2:

     5
   -/ \-
  3     7
 / \
2   4

Insert the 6:

     5
   -/ \-
  3     7
 / \   /
2   4 6

And finally, insert the 8:

     5
   -/ \-
  3     7
 / \   / \
2   4 6   8

Inserting and searching in BST

Since the data in a BST follows the aforementioned structure, BSTs have an advantage in search operation over the List. The search algorithm itself is very easy. You start at the root node and compare its data with one you are looking for. If it matches, you return and appriopriate value. When it does not match and the value you are searching for is greater than the node’s value traverse down the right child (why?). If the value you are searching for is less than the node’s value traverse down the left child (why?). This process is repeated until a match is found, or there are no children in the appropriate search direction.

When inserting a node in a BST it is important that the new tree still has original properties. Therefore, it is important to select proper position for a new node. The insertion algorithm is similar to search, for except when we will reach a NULL pointer, we create a node and insert it in that position.

Templates in C++

Thus far we have discussed abstract data values in the BST node without mentioning the type we want to store specifically. In theory, BST nodes can contain data of any type: integers, chars, strings, etc. But in C++ , when you declare a node class, you must specify the data type in the very beginning. If later we want to have the same structure with a different data type, we will have to duplicate the code and only change the type. We will have to keep adding code for every new type we want to use. For someone writing code, this quite inefficient and error prone. To avoid this routine, C++ provides templates. Templates allow you to create functions and classes with a generic type. The type is then passed as template parameter to the newly created function or class. Here is an example.

Creating a template class:

template <class T>
Node{
  private:
    T data;
  public:
    Node(T data): data(data) {}
};

Creating an integer and char nodes:

Node<int> node_int(20);
Node<char> node_char('z');

For more on templates see the week 6 example and/or this tutorial.

Creating a BST

For this lab you are given a few source files and your goal is to complete the implementation of the BST class in bst.cpp. The specification for each of those functions (and all the other BST functions) are provided in bst.h.

Initially, when you create a BST, the constructor should create only a root pointer equal to NULL (an empty tree). If you want to grow your tree, you should implement and use the insert method for creating and adding nodes to the tree.

After you’ve created your BST, it is time to traverse it! As you might notice, there are multiple possibilities to traverse the BST because at every step you have an option to move to the left child or to the right child or to the node on the same level (same number of nodes away from the root as the current one).

Commonly, depth first search (traversal) is the one where you consistently move towards left or right children and when you’ve reached the leaf node go one level up and move towards the child you haven’t checked out (the right or left one, respectively). If you’ve checked out all of the descendants of this node, go one level up.

For example, a depth first traversal of the above tree will output nodes in the following order (this assumes you first output the node you are “visiting” and then “visit” the left hand side before the right hand side):

5 3 2 4 7 6 8 

The provided stack_output function (in bst.h) output should match the above exactly (note that there is trailing space before the newline (for simplicity). More on dfs.

Depth first traversal of a tree can be conveniently implemented with the help of stack. We initialize the stack with the root node, and then loop until the stack is empty. The first step of the loop is to do “something” with the value of the node we’re currently looking at (may be to output it in the case of stack_output) and then we push both of its children (if they exist) on to the stack. Note that because of the way items are added/removed from the stack we push the right-hand side on before the left-hand side to acheive the expected left-to-right output.

Breadth first search (traversal) is the one where you consistently move to the nodes on the same level from left to right or in opposite directions. For example, a breadth first traversal of the above tree will output nodes in the following order:

5 3 7 2 4 6 8 

Your queue_output function output should match the above output exactly (note that there is trailing space before you output the newline. More on bfs.

Conveniently, breadth first search can be implemented by using queues rather than stacks in a manor similar to how depth first search was implemented (replace push with enqueue and pop with dequeue).

Provided Files

  • Makefile A simple makefile for building the lab. Just run make to compile everything.

  • bst.h - This file contains the definition of the BST class. You do not have to modify this file but you are responsible for understanding all of the code.

  • _data_structures.h This file contains the definitions for the BinaryNode class as well as the Queue and Stack data structures. You do not have to modify this file but you are responsible for understanding all of the code.

  • bst.cpp This file contains stubs for the functions that you will implement. To complete the lab you must write the following methods for BST:

    • destructor
    • insert
    • queue_output (BFS traversal)
  • driver.cpp Provides an interface for inserting nodes into the BST (based off the command line arguments), and testing whether or not the integers 0 through 10 are contained in the BST.

Hints

Traversing the BST using a Stack or Queue

The linear structure of a List allows us to traverse the elements without having to keep track of the ones we’ve already visited. However BSTs do not afford us this convenience. Consider the BST in the example above:

      5
    -/ \-
   3     7
  / \   / \
 2   4 6   8

Let’s say we are in the midst of a DFS and we are currently at node 4. How are we to find the next node (7)? Trying to individually store each node we traverse would lead to messy and inefficient code. A much better solution is to store the nodes in another data structure to keep track of them. Consider a DFS using a stack. We initialize by pushing 5 onto the stack:

      5         Stack: 5
    -/ \-
   3     7
  / \   / \
 2   4 6   8

We then iterate over the following steps until the stack is empty:

WHILE stack is not empty:
	POP the last element
	PUSH its left child
	PUSH its RIGHT child
	PRINT the element
END WHILE

After 1 iteration (the star indicates that we’ve printed this node):

     *5         Stack: 3 7
    -/ \-
   3     7
  / \   / \
 2   4 6   8

After 2 iterations:

     *5         Stack: 2 4 7
    -/ \-
  *3     7
  / \   / \
 2   4 6   8

After 2 iterations:

     *5         Stack: 4 7
    -/ \-
  *3     7
  / \   / \
*2   4 6   8

After 3 iterations:

     *5         Stack: 7
    -/ \-
  *3     7
  / \   / \
*2  *4 6   8

After 4 iterations:

     *5         Stack: 6 8
    -/ \-
  *3    *7
  / \   / \
*2  *4 6   8

Until we’ve traversed all of the elements. In this lab you will have to perform a BFS in a similar manner using a Queue.

Declaring the Stack and Queue

To implement queue_output and stack_output you need to respectively declare both a templated version of the Stack class, and one of the Queue class. If the Stack, or Queue needed to store integers, you could declare them like:

Stack<int> stack_variable_name;
Queue<int> queue_variable_name;

However, you want to store a pointer to the proper tree node type, BinaryNode. To make things slightly more confusing, the BinaryNode type is also templated so you need to specify what type the BinaryNode should be. If you knew for certain the BinaryNode is to store a character, then you might declare the Stack and Queue like:

Stack<BinaryNode<char>*> char_stack;
Queue<BinaryNode<char>*> char_queue;

However, your BST implementation is templated so it should work with whatever type the user of your program uses. That’s where the magic T variable as declared in the line template <class T> comes in handy. T specifies what type the class holds so you can simply replace char in our previous example with T like the following in order to declare the Stack and Queue for the respective functions:

Stack<BinaryNode<T>*> stack;
Queue<BinaryNode<T>*> queue;

Submitting the project

Only one person in a group need submit the assignment but make sure that both you and your partner can view the submission on the submission site. If you cannot, you need to complete the making a group process. Please review the submission instructions as needed. Note that you may resubmit this assignment as many times as necessary up until the deadline.

 

Template design by Andreas Viklund