Binary Search Tree implemented by C++
This is a simple implementation of binary search tree implemented by C++. Here are the methods of my BST:
class Node
{
public:
Node *left, *right, *parent;
int key;
};
class BST
{
private:
Node *nil;
public:
BST();
Node* root();
// Traversals
void inorder();
void inorderRec(Node*);
void inorderIter();
void preOrder();
void preOrderRec(Node*);
void postOrder();
void postOrderRec(Node*);
// Search Operations
Node* find(int);
Node* minimum(Node*);
Node* maximum(Node*);
Node* successor(Node*);
Node* predecessor(Node*);
// Element Operation
void insert(int);
void insert(Node*);
void remove(int);
void remove(Node*);
void transplant(Node*, Node*);
bool empty();
};
I will talk about some of the implementations. The complete source file can be found in my algorithm repository. If you found any problem with the implementation, or anything that is worth adding to this, please leave a comment under this article or open an issue in the algorithm repository. This BST does not insert repeated element.
Basic Idea
Binary Search Tree is a basic data structure in computer science. It stores data, in my case some integers, into a tree-like structure.
In a tree, there are nodes, which are the storage a piece of data and also connection to other nodes. A node can connect to its parent and its children. In other trees, there could be unlimited children, but for our case, a binary tree could have at most two children. Naturally, let’s call them left and right child respectively.
The top node is called root, and the nodes that do not have a child are called leaves. There is a nil
sentinel node, which is not in the data structure but a useful element in our program.
Traversals
The recursive in-order traversal is easy to implement:
void BST::inorderRec(Node* node)
{
if (node != nil)
{
inorderRec(node -> left);
cout << node -> key << " ";
inorderRec(node -> right);
}
}
The iterative algorithm is slightly more complex. It involves two pointers, marking the current and previous position of our traversal. Although this is not recursive, we could still think of it using an inductive reasoning. We first set the initial states to root and nil (who is the parent of root):
auto x = root();
auto prev = nil;
For an arbitrary iteration, there are three possibilities:
-
prev
is the parent ofx
. It means we have been going downwards in the tree, and both left branch and right branch ofx
has not yet been explored; then as in-order traversal requires, we go left from here. -
prev
is the left child ofx
. This means we have just come back from the left branch, implying that the left is already printed. Now we have to printx
itself and going to the right. -
prev
is the right child ofx
. This means we have printed everything in the subtree rooted atx
, and should now go up.
prev pointing at
|
printed subtree |
---|---|
-> parent
|
none |
-> left
|
rooted at left
|
-> right
|
rooted at x
|
The trick here is to 1) progress the printing and 2) ensure that at the end of each iteration, the invariant is preserved, i.e. the situation falls into one of the situations.
prev
Pointing at Parent
Nothing has been printed, so we go to left directly;
prev = x;
x = x -> left;
We need to consider the case where left
is nil, but this is easy because we can just throw prev
to left. This works because if we examine the loop invariant, now the tree complies with the second situation.
prev
Pointing at left
Left tree is already printed (we consider nil
as printed), so we try to go right. Don’t forget to first print out x
before entering right.
There is a slight complication if x
is a leaf, as both children are nil
. We would not be able to know whether we have come back from left or right! The traversal will circle around at x
forever.
{:width=“36px”}.
The solution is that we never come from right at a leaf node. When we reach a leaf node from its left, we directly move upwards.
cout << x -> key << " ";
if (x -> right == nil)
{ // right branch is nil
prev = x;
x = x -> parent;
}
else
{
prev = x;
x = x -> right;
}
prev
Pointing at Right
We know x
subtree is all printed, so all we need to do is to move x
upwards.
prev = x;
x = x -> parent;
Insertion
Insertion always occur at leaf in a BST. Generally, we need to 1) find a proper leaf as the parent of our new node and 2) insert it into the proper child of the leaf.
Sentinel Node
How is a sentinel node useful? There are several uses of it, and the most important one is to replace NULL
for representing a non-existing element. It guards against null pointers.
NULL
is a very bad design from the very beginning, as it is passed into a function as pointer type
but is not a pointer
at all. Any attempt to call a member function will cause a crush. It is also hard to debug, especially when there are layers of function calls. You cannot dereference a null pointer in debugger; it points to 0x00000000, which has no useful information at all.
A sentinel nil
on the other hand is a legitimate Node
object. It has all functions supported, and could be more useful for debugging. Debugging-wise, it is printable and assignable.
It is also used as the parent of root
in our tree. This eliminates the difference between an empty tree and non-empty tree, so insertion and removal of root
could be carried out without an extra conditional branch.