6.1 Binary Search Tree Definition
A binary search tree (BST) is a binary tree that conforms to the following condition, known as the binary search tree property. All nodes stored in the left subtree of a node whose key value is have key values less than or equal to . All nodes stored in the right subtree of a node whose key value is have key values greater than . The figure below shows two BSTs for a collection of values. One consequence of the binary search tree property is that if the BST nodes are printed using an inorder traversal, then the resulting enumeration will be in sorted order from lowest to highest.
Figure: Example BSTs
Two Binary Search Trees for a collection of values. Tree (a) results if values are inserted in the order 37, 24, 42, 7, 2, 40, 42, 32, 120. Tree (b) results if the same values are inserted in the order 120, 42, 42, 7, 2, 32, 37, 24, 40.
Here is a class declaration for the BST Map:
class BSTMap implements Map:
BSTMap():this.root = null
this.treeSize = 0
class BSTNode:
BSTNode(key, value):this.key = key // the key that are used for looking up values
this.value = value // the value associated with the key
this.left = null // the left subtree, initially null
this.right = null // the right subtree, initially null
6.1.1 BST Search
The first operation that we will look at in detail will find the
record that matches a given key. Notice that in the BST class, public
member function get
calls private member function
getHelper
. Method get
takes the search key as
an explicit parameter and its BST as an implicit parameter, and returns
the record that matches the key. However, the find operation is most
easily implemented as a recursive function whose parameters are the root
of a subtree and the search key. Member getHelper
has the
desired form for this recursive subroutine and is implemented as
follows.
class BSTMap implements Map:
...get(key):
return getHelper(this.root, key)
getHelper(node, key):if node is null:
return null
else if key < node.key:
return getHelper(node.left, key)
else if key > node.key:
return getHelper(node.right, key)
else: // key == node.key
return node.value
6.1.2 BST Insert
Now we look at how to insert a new node into the BST.
class BSTMap implements Map:
...put(key, value):
this.root = this.putHelper(this.root, key, value)
putHelper(node, key, value):// Helper method for 'put', returns the updated node.
if node is null:
this.treeSize = this.treeSize + 1
return new BSTNode(key, value)
else if key < node.key:
= this.putHelper(node.left, key, value)
node.left else if key > node.key:
= this.putHelper(node.right, key, value)
node.right else: // key == node.key
= value
node.value return node
Note that, except for the last node in the path, putHelp
will not actually change the child pointer for any of the nodes that are
visited. In that sense, many of the assignments seem redundant. However,
the cost of these additional assignments is worth paying to keep the
insertion process simple. The alternative is to check if a given
assignment is necessary, which is probably more expensive than the
assignment!
We have to decide what to do when the node that we want to insert has a key value equal to the key of some node already in the tree. If during insert we find a node that duplicates the key value to be inserted, then we have two options. If the application does not allow nodes with equal keys, then this insertion should be treated as an error (or ignored). If duplicate keys are allowed, our convention will be to insert the duplicate in the left subtree.
The shape of a BST depends on the order in which elements are inserted. A new element is added to the BST as a new leaf node, potentially increasing the depth of the tree. Figure #BSTShape illustrates two BSTs for a collection of values. It is possible for the BST containing nodes to be a chain of nodes with height . This would happen if, for example, all elements were inserted in sorted order. In general, it is preferable for a BST to be as shallow as possible. This keeps the average cost of a BST operation low.
6.1.3 BST Remove
Removing a node from a BST is a bit trickier than inserting a node,
but it is not complicated if all of the possible cases are considered
individually. Before tackling the general node removal process, we need
a useful companion method, largestNode
, which returns a
pointer to the node containing the maximum value in the subtree.
class BSTMap implements Map:
...
largestNode(node):while node.right is not null:
= node.right
node return node
Now we are ready for the removeHelper
method. Removing a
node with given key value
from the BST requires that we first find
and then remove it from the tree. So, the first part of the remove
operation is a search to find
.
Once
is found, there are several possibilities. If
has no children, then
’s
parent has its pointer set to NULL. If
has one child, then
’s
parent has its pointer set to
’s
child (similar to deleteMax
). The problem comes if
has two children. One simple approach, though expensive, is to set
’s
parent to point to one of
’s
subtrees, and then reinsert the remaining subtree’s nodes one at a time.
A better alternative is to find a value in one of the subtrees that can
replace the value in
.
Thus, the question becomes: Which value can substitute for the one being removed? It cannot be any arbitrary value, because we must preserve the BST property without making major changes to the structure of the tree. Which value is most like the one being removed? The answer is the least key value greater than the one being removed, or else the greatest key value less than (or equal to) the one being removed. If either of these values replace the one being removed, then the BST property is maintained.
When duplicate node values do not appear in the tree, it makes no difference whether the replacement is the greatest value from the left subtree or the least value from the right subtree. If duplicates are stored in the left subtree, then we must select the replacement from the left subtree. To see why, call the least value in the right subtree . If multiple nodes in the right subtree have value , selecting as the replacement value for the root of the subtree will result in a tree with equal values to the right of the node now containing . Selecting the greatest value from the left subtree does not have a similar problem, because it does not violate the Binary Search Tree Property if equal values appear in the left subtree.
Note: Alternatively, we can decide to store duplicate values in the right subtree instead of the left. Then we must replace a deleted node with the least value from its right subtree.
The code for removal is shown here.
class BSTMap:
...remove(key):
this.root = removeHelper(this.root, key)
removeHelper(node, key):// Helper method for 'remove', returns the updated node.
if node is null:
return null
else if key < node.key:
= this.removeHelper(node.left, key)
node.left return node
else if key > node.key:
= this.removeHelper(node.right, key)
node.right return node
else: // key == node.key
if node.left is null:
this.treeSize = this.treeSize - 1
return node.right
else if node.right is null:
this.treeSize = this.treeSize - 1
return node.left
else:
= this.largestNode(node.left)
predecessor = predecessor.key
node.key = predecessor.value
node.value = this.removeHelper(node.left, predecessor.key)
node.left return node
6.1.4 BST Analysis
The cost for getHelper
and putHelper
is the
depth of the node found or inserted. The cost for
removeHelper
is the depth of the node being removed, or in
the case when this node has two children, the depth of the node with
smallest value in its right subtree. Thus, in the worst case, the cost
for any one of these operations is the depth of the deepest node in the
tree. This is why it is desirable to keep BSTs balanced,
that is, with least possible height. If a binary tree is balanced, then
the height for a tree of
nodes is approximately
.
However, if the tree is completely unbalanced, for example in the shape
of a linked list, then the height for a tree with
nodes can be as great as
.
Thus, a balanced BST will in the average case have operations costing
,
while a badly unbalanced BST can have operations in the worst case
costing
.
Consider the situation where we construct a BST of
nodes by inserting records one at a time. If we are fortunate to have
them arrive in an order that results in a balanced tree (a “random”
order is likely to be good enough for this purpose), then each insertion
will cost on average
,
for a total cost of
.
However, if the records are inserted in order of increasing value, then
the resulting tree will be a chain of height
.
The cost of insertion in this case will be
.
Traversing a BST costs regardless of the shape of the tree. Each node is visited exactly once, and each child pointer is followed exactly once.
Below is an example traversal, named printHelper
. It
performs an inorder traversal on the BST to print the node keys, in
sorted order.
function printHelper(node):
if node is not null:
printHelper(node.left)print(node.key)
printHelper(node.right)
While the BST is simple to implement and efficient when the tree is balanced, the possibility of its being unbalanced is a serious liability. There are techniques for organizing a BST to guarantee good performance. Two examples are the AVL tree and the splay tree. There also exist other types of search trees that are guaranteed to remain balanced, such as the 2-3 Tree.
6.1.5 Practice questions: BST definition
Answer TRUE or FALSE.
A BST is another name for a binary tree.
- A binary tree is either empty, or else a root node with two binary trees as children.
- A BST (binary search tree) is a binary tree where, for every node N, all nodes in the left subtree of N have key values less than the key value of N, and all nodes in the right subtree of N have key values greater than the key value of N.
BST search, insert, and delete operations typically run in time . What is ?
- Think about a given insert or search operation.
- How many nodes get looked at?
- The operation moves down the tree, looking at a node at each level that it visits.
- So the total work is the number of nodes that it visits, which is the same as the depth of the last node.