Binary Search Trees (BSTs)
A binary search tree is a type of binary tree which follows the BST Property.
BST Property
For every node $N$ in a BST, the following conditions are true:
- Every item in $N$'s left subtree is less than $N$.
- Every item in $N$'s right subtree is greater than $N$.
graph TD
A(5) --> B(3)
A --> C(7)
B --> D(2)
B --> E(4)
C --> F(6)
C --> G(8)
Tip
Within CS61BL, BSTs will have unique keys. Therefore, given keys $K_1$ and $K_2$, $K_1 < K_2$ or $K_1 > K_2$.
Consequently, given $K_3$, if $K_3 < K_1$, then $K_3$ must be in $K_1$'s left subtree. If $K_3 > K_1$, then $K_3$ must be in $K_1$'s right subtree. (assuming $K_1$ is the parent)
The transitive property of inequality can be used to extend this to any node in the tree.
Searching
In order to search for a key $K$ in a BST, we can use the BST property to our advantage.
In Python, we can implement this as follows:
def search(root, key):
if root is None or root.key == key:
return root
if root.key < key:
return search(root.right, key)
return search(root.left, key)
Time Complexity
The time complexity of searching for a key $K$ of size $N$ in a BST is $\Theta(\log N)$ in the best case, and $\Theta(N)$ in the worst case.
Best Case
The best case occurs when the tree is bushy, or balanced. This means that for each node, there are roughly the same number of nodes in the left and right subtrees. This produces a tree with $\log N$ levels, where $N$ is the number of nodes in the tree.
Example
graph TD
A(5) --> B(3)
A --> C(7)
B --> D(2)
B --> E(4)
C --> F(6)
C --> G(8)
Worst Case
Conversely, the worst case occurs when the tree is "spindly", or unbalanced. This means that for each node, there are many more nodes in one subtree than the other. This produces a tree with $N$ levels, where $N$ is the number of nodes in the tree.
Example
graph LR
A(1) --> B(2)
B --> C(3)
C --> D(4)
This depiction of the "tree" essentially represents a linked list, which has a time complexity of $\Theta(N)$.
Insertion
In order to insert a key $K$ into a BST, we can use the BST property to our advantage, similar to searching.
In Python, we can implement this as follows:
def insert(root, key):
if root is None:
return Node(key)
if root.key < key:
root.right = insert(root.right, key)
else:
root.left = insert(root.left, key)
return root
None
node. Note that the parent of this node is the node that we want to insert $K$ into.
We then create a new node with key $K$ and return it.
Deletion
Deletion is slightly more tricky than insertion and searching, as we need to consider the following cases:
Deletion of a Leaf
If the node to be deleted is a leaf, we can simply delete it, as it has no "dependants".
Deletion of a Node with One Child
If the node $K$ to be deleted has one child, we can simply replace $K$ with its child. In essence, we are "promoting" the child to take the place of $K$.
Deletion of a Node with Two Children (Hibbard Deletion)
If the node $K$ to be deleted has two children, we need to find the successor of such that BST Property is maintained.
The successor of node $K$ is the greatest leaf in $K$'s left subtree or the smallest leaf in $K$'s right subtree.
After a successor is found, the successor is swapped with $K$, and $K$ is deleted.