13.4.1. Tree Example 1: One Template Variable
The complete Tree template example consists of four files, but this section only presents two: Employee.h and Tree.h . You can find links to all the example files at the bottom of the page.
#include <iostream>
#include <string>
using namespace std;
class Employee
{
private:
string name;
int id;
public:
Employee(string n = "", int en = 0) : name(n), id(en) {} // (a)
bool operator==(Employee& e) { return name == e.name; } // (b)
bool operator<(Employee& e) { return name < e.name; }
friend ostream& operator<<(ostream& out, Employee& me) // (c)
{
out << me.name << " " << me.id;
return out;
}
};
Employee.h: The Employee class specification .
The Employee class specifies the example data type stored in a dynamic data structure, demonstrating a binary tree implemented with one template variable. The Employee functions delegate some or all of their responsibilities to corresponding string functions.
Builds Employee objects from the parameters or "empty" objects if the client doesn't override the default value. The Employee calls the string constructor to initialize its name member variable.
The Employee search , insert , and remove functions less-than and equality operators. These functions examine the name member by calling the corresponding string functions. Furthermore, they do not examine the object's id .
The optional Tree list and listView functions use the Employee inserter.
#include <iostream>
#include <iomanip>
using namespace std;
template <class T>
class Tree
{
private:
T data;
Tree<T>* left = nullptr; // (a)
Tree<T>* right = nullptr;
public:
~Tree();
T* insert(T& key); // (b)
T* search(T& key);
void remove(T& key);
void list() { if (right != nullptr) right->_list(); } // (c)
void tree_view(int level = -1); // (d)
private:
void remove(Tree<T>* top, Tree<T>* bottom); // (e)
void _list(); // (f)
int subtrees(); // (g)
};
Tree.h: The Tree class specification .
This binary tree implementation makes the root a non-data-storing tree "handle," simplifying the code for insert and remove without significantly impacting search . However, it does make implementing list and tree_viw more awkward and slightly less efficient, but clients use these functions less frequently, making a worthwhile tradeoff.
Initializing the subtree pointers in the class specification eliminates the need for a programmer-created Tree constructor and makes the computer-created constructor sufficient.
insert , search , and remove are the tree's primary functions.
If the tree contains data, the right subtree is non-null, and the public list function calls the private _list function for the right subtree, skipping the root node.
tree_view is a non-standard operation introduced as a debugging and validation tool. The parameter controls the indentation reflecting a node's level in the three, and a negative value causes the function to skip the root node.
The Tree class hides the subtree pointers and the operations accessing them. This remove function removes a node and reorganizes the tree based on the top and bottom pointers, an operation not directly available to client programs.
The private
_list function recursively descends the tree, printing the stored data as it visits each node.
The subtree counts a node's subtrees: 0, 1, or 2. Moving the test to a separate function simplifies the logic, helping to "tame" one case in the remove function.
template <class T>
Tree<T> ::~Tree()
{
if (left != nullptr)
delete left;
if (right != nullptr)
delete right;
//cout << data << endl; // shows deletion
}
Tree.h: The Tree destructor . When the delete
operator deletes an object through a pointer, it automatically triggers a call to the object's destructor, making the Tree destructor a recursive function. The function recursively descends each subtree, stopping when both are null and deallocates the nodes (i.e., returns the nodes' memory to the heap) as it returns. The commented-out cout statement prints the node's contents as the function removes, illustrating the function's recursive behavior.
template <class T>
T* Tree<T> ::insert(T& key)
{
Tree<T>* top = this; // (a)
Tree<T>* bottom = right;
while (bottom != nullptr) // (b)
{
if (bottom->data == key) // (c)
return &bottom->data;
top = bottom; // (d)
bottom = (key < bottom->data) ? bottom->left : bottom->right;
}
bottom = new Tree; // (e)
bottom->data = key;
((key < top->data) ? top->left : top->right) = bottom; // (f)
return &bottom->data; // (g)
}
Tree.h: The Tree insert function .
Programmers often implement the insert function with recursion, but this version replaces recursion with a slightly faster iteration. It doesn't allow duplicate data; instead, it returns a pointer to previously inserted data with the same key. This implementation requires that operator< , operator== , and operator= are implemented for the class replacing the template variable. Programmers must overload the first two. The compiler-created assignment operator is sufficient for simple objects, but programmers must override it for complex objects.
The function descends the tree with the top and bottom pointers, locating the insertion point or a previously inserted node with data matching the key.
The function loops until it reaches the bottom of the tree or finds a previously inserted node with data matching the key. It is more common to implement operator== than operator!= , so the equality operator is used here with the negation operator, !
.
Detects and returns previously inserted data, preventing duplicate data in the tree.
The function updates the top and bottom pointers, descending to the next tree level. A conditional operator updates the bottom pointer. It selects the left subtree if the key is less than the data saved in current bottom node, or the right subtree otherwise.
The function only makes it to this point if the tree doesn't already have a node matching key . It creates a new tree node, copies the data to it, and inserts it into the tree.
The conditional operator selects the left or right subtree based on the relative ordering of the key and the data in the top node, returning a pointer in either case. Pointers are valid l-values , so the assignment operator inserts the new node as its parent's left or right subtree.
As a convenience, the function returns a pointer to the newly inserted data.
template <class T>
T* Tree<T> ::search(T& key)
{
Tree<T>* bottom = right; // (a)
while (bottom != nullptr) // (b)
{
if (bottom->data == key) // (c)
return &bottom->data;
bottom = (key < bottom->data) ? bottom->left : bottom->right; // (d)
}
return nullptr; // (e)
}
Tree.h: The Tree search function .
The search function returns a pointer to the data matching the key if found. Otherwise, it returns nullptr
.
Only one pointer, bottom , is needed to descend the tree while looking for the tree node matching key .
The function loops until reaching the bottom of the tree or finding the nodes with data matching key .
Detects and returns the data matching key .
The function updates bottom with one of the current bottom pointer's subtrees, selected by the conditional operator.
Returns nullptr
if key is not stored in the tree.
template <class T>
void Tree<T> ::remove(T& key)
{
Tree<T>* top = this; // (a)
Tree<T>* bottom = right;
while (bottom != nullptr) // (b)
{
if (bottom->data == key) // (c)
remove(top, bottom);
top = bottom; // (d)
bottom = (key < bottom->data) ? bottom->left : bottom = bottom->right;
}
}
Three.h: The public
Tree remove function .
This function descends the tree, looking for the node to remove. The two remove functions separate the tree descent and removal logic, simplifying the remove operation.
The function descends the tree with the top and bottom pointers, locating the node to remove (bottom ) and its parent (top ).
The function loops until it reaches the bottom of the tree or finds the removal node.
If the public
remove function finds the node matching key , it calls the private
remove (highlighted in blue), passing to it the top and bottom pointers.
The function updates the top and bottom pointers, descending to the next tree level. A conditional operator updates the bottom pointer. It selects the left subtree if the key is less than the data saved in current bottom node, or the right subtree otherwise.
template <class T>
void Tree<T> ::remove(Tree<T>* top, Tree<T>* bottom)
{
switch (bottom->subtrees() ) // (a)
{
case 0: // (b)
//cout << "CASE 1" << endl;
if (top->left == bottom)// (b)
top->left = nullptr;
else
top->right = nullptr;
delete bottom;
return;
case 1: // (c)
//cout << "CASE 2" << endl;
if (top->left == bottom)
top->left = (bottom->right == nullptr) ? bottom->left : bottom->right;
else if (top->right == bottom)
top->right = (bottom->right == nullptr) ? bottom->left : bottom->right;
bottom->left = bottom->right = nullptr;
delete bottom;
return;
case 2: // (d)
//cout << "CASE 3" << endl;
top = bottom;
Tree<T>* succ = bottom->right;
while (succ->left != nullptr)
{
top = succ;
succ = succ->left;
}
bottom->data = succ->data;
remove(top, succ); // (e)
return;
}
}
Tree.h: The private
Tree remove function .
A straightforward implementation of the three remove operation cases identified in the previous section resulted in a large, unwieldy structure of nested if-statements. Although the functions in this implementation continue to follow the preceding case analysis, they use several simplifying devices: The two remove functions separate the tree descent and removal logic; a separate function, subtrees , counts a node's filled subtrees, selecting the removal case; the private
remove function uses recursion to solve the two-subtree case (case 3 - 2 subtrees); finally, the functions replace many of the if-else-statements with conditional operators performing the same logic but taking less space.
subtree counts the number of filled subtrees in the bottom node, determining which case applies.
When a node is a leaf (i.e., it doesn't have filled subtrees), remove the node and set the appropriate parent subtree to null (necessary so the delete
operation doesn't cause runaway recursion).
To remove a node with one filled subtree, the function moves the node's subtree to its parent and nulls both subtrees (to prevent runaway recursion).
Removing a node with two filled subtrees is difficult. The function finds the node's successor (the node with the next highest data value) by taking the right subtree and then consistently following the left subtree until it reaches a null. The function copies the successor's data to the removal node (overwriting the removal node's data) and recursively calls itself to remove the now-empty successor node.
template <class T>
void Tree<T> ::_list()
{
if (left != nullptr)
left->_list();
cout << data << endl;
if (right != nullptr)
right->_list();
}
Tree.h: The private
Tree _list function .
The _list function recursively descends the tree, printing the stored data in-order . Moving the cout statement to the top of the function prints the data pre-order , while moving it to the bottom prints the data post-order . The _list function assumes that operator<<
is overloaded for the template class replacing T .
template <class T>
void Tree<T> ::tree_view(int level)
{
if (level < 0) // (a)
{
if (right != nullptr)
right->tree_view(0);
return;
}
cout << setw(level) << "" << data << endl; // (b)
if (left != nullptr) // (c)
left->tree_view(level+4);
else
cout << setw(level+4) << '-' << endl;
if (right != nullptr) // (d)
right->tree_view(level+4);
else
cout << setw(level+4) << '-' << endl;
}
Tree.h: The Tree tree_view function . The tree_view function is uncommon and only included in the Tree class to aid debugging, validation, and demonstration. The parameter controls the output indentation, indicating each node's level in the tree. However, clients should call the function without an argument, accepting the default value and allowing it to manage the arguments. The function prints a hyphen to indicate an empty subtree.
On the first call, the parameter is -1 (the default value), causing the function to skip the root node. If the tree is not empty (i.e., it has a filled right subtree), the function recursively calls itself with a 0 argument, so the first printed node is not indented.
Prints the indented data in the current node. The cout statement creates the indentation with setw(level) << "" .
Recursively follows the left subtree if it is not null, incrementing the indentation amount by 4 or printing a hyphen.
Recursively follows the right subtree if not null, incrementing the indentation amount by 4 or printing a hyphen.
template <class T>
int Tree<T> ::subtrees()
{
if (left == nullptr && right == nullptr) // (a)
return 0;
else if (left == nullptr || right == nullptr) // (b)
return 1;
else // (c)
return 2;
}
Tree.h: The private
subtrees function .
The subtrees function counts the current node's subtree (i.e., the number of subtrees of the node referenced by the this pointer). Moving the test out of the remove functions simplifies the removal logic, eliminating one if-else-statement.
Both subtree pointers are null, making the node a leaf with no subtrees.
One subtree pointer is not null (the if-statement is true if both pointers are null, but the first if-else test handles that situation).
Both subtree pointers are non-null, meaning the node has two subtrees.
#include <iostream>
#include <string>
#include "Tree.h" // (a)
#include "Employee.h"
using namespace std;
int main()
{
Tree<Employee> tree; // (b)
. . .
}
Using a template class .
Once created and validated, programs use a template class much like any other, with only one visible syntax change.
The header file contains the class specification and function definitions. The compiler completes the template expansion (i.e., it replaces the template variable, T , with Employee ) before compiling the C++ code. Although this process differs from "normal" functions, it is automatic and transparent.
The syntax instantiating an object is the only modification to client code. The program specifies the data type replacing the template variable inside angle brackets.
Downloadable Code
Back |
Chapter TOC |
Next