Random musings on programming, mostly.
Data Structures & Algorithms
You down with BST? Yeah, you know me. Your not down with BST’s you say? Well get ready, your about to be. Last week we talked about Trees in general. This week we are going to narrow our focus and look at Binary Search Trees. So if you need to brush up on Trees in general you can do that here.
A Binary Search Tree, BST from here on out, is simply a tree with the additional property that it’s keys are in sorted order. That is, any Nodes or sub-trees on the left branch of a Node are less than the Node and any Nodes or sub-trees on the right branch of a Node are greater than the Node. See the picture below for clarification.
Notice how all the values to the left of the Root Node, 8, are less than 8 and all the values in the right sub-tree are greater than 8. This property holds for all sub-trees of the Tree as well. This single property, allows for speedy search, insert and delete methods, all three of which have average runtimes of O(log n).
Let’s get started writing some code. First off we need to implement a Node class. It should have a value and pointers to left and right nodes:
class Node attr_accessor :key, :left, :right def initialize(key) @key = key @left = nil @right = nil end end
Pretty straightforward, our class initializer simply sets the value of the Node and the left and right pointers are set to nil.
Next, we should implement the BST class. This will also be fairly simple, as far as initialization goes. We are simply going to create a root that is set to nil. Now that’s great and all, but we can’t really do anything with it, so we will also implement an insert method at the same time which will allow us to insert Nodes into our new BST class.
class BST attr_accessor :root def initialize @root = nil end def insert(key) insert_node(@root, key) end private def insert_node(current_node, key) if current_node.nil? current_node = Node.new(key) elsif key < current_node.key insert_node(current_node.left, key) elsif key > current_node.key insert_node(current_node.right, key) end end end
A few things to note here. The insert
method delegates to a private method, insert_node
. The real work is done in the insert_node
method, which compares the value that needs to be inserted to the current_node
, if it is less than, we recursively call insert_node
on the left child and vice-versa if it is greater, until we find an empty Node, at which point we insert a new Node.
Great, so we can build out a BST by simply creating a new BST with BST.new
. The we insert nodes one by one with the insert
method, passing it a key value. The next step is to implement a search
method that will allow us to traverse the BST and determine if a Node with a certain key value exists.
class BST attr_accessor :root def initialize @root = nil end def insert(key) insert_node(@root, key) end def search(key) find_node(@root, key) end private def insert_node(current_node, key) if current_node.nil? current_node = Node.new(key) elsif key < current_node.key insert_node(current_node.left, key) elsif key > current_node.key insert_node(current_node.right, key) end end def find_node(current_node, key) if current_node.nil? return nil end if current_node.key == key return current_node elsif key < current_node.key find_node(current_node.left, key) elsif key > current_node.key find_node(current_node.right, key) end end end
Much like we did with the insert method, we delegate to a private method called find_node
. This method starts out checking for two potential base cases, either the current_node
is nil, in which case we return nil, or the current_node.key
is equal to the key we are searching for, in that case we found what we are looking for and return it. Otherwise we recursively traverse the tree based on whether the key we are looking for is less than or greater than the current_node.key
, just like we did in the insert method. This is a pattern you are going to see over and over in Trees. Since Trees are a recursive structure, we typically traverse them recursively.
Finally, we will implement a delete method. This is slightly more challenging than the insert and search methods. Once we find the Node we are looking for there are basically three cases to consider.
The first two are fairly straightforward. If the Node has no children we simply remove it. If the Node has only one child we remove the Node and replace it with its child. If the Node has two children we find it’s next successor in the Tree. This Node can be found by taking the right sub-tree and finding the minimum value in it. See the image below again.
In the instance we want to remove the Node whose value is 3, we would find its successor. To do this we would find the smallest value in its right sub-tree. In other words we go to the right child, the Node whose value is 6 and then traverse the Tree to the left until we find a leaf node, which is the Node whose value is 4. We then replace the 3 with 4 and remove the leaf Node. Let’s see the code.
class BST attr_accessor :root def initialize @root = nil end def insert(key) insert_node(@root, key) end def search(key) find_node(@root, key) end def delete(key) delete_node(@root, key) end private def insert_node(current_node, key) if current_node.nil? current_node = Node.new(key) elsif key < current_node.key insert_node(current_node.left, key) elsif key > current_node.key insert_node(current_node.right, key) end end def find_node(current_node, key) if current_node.nil? return nil end if current_node.key == key return current_node elsif key < current_node.key find_node(current_node.left, key) elsif key > current_node.key find_node(current_node.right, key) end end def delete_node(current_node, key) if current_node.key == key remove_node(current_node) elsif current_node.key < key delete_node(current_node.left, key) elsif current_node.key > key delete_node(current_node.right, key) end end def remove_node(node) # Case 1: No children if node.left.nil? && node.right.nil? node = nil # Case 2: One child elsif node.right.nil? && !node.left.nil? node = node.left elsif node.left.nil? && !node.right.nil? node = node.right else node = replace(node) end end def replace(node) node.value = successor(node.right) node.right = update(node.right) end def successor(node) if !node.left.nil? successor(node.left) end return node.key end def update(node) if !node.left.nil? node.left = update(node) end return node.right end end
So there is a lot of new code here, let’s go through it. Like before the delete
method is simply an interface method, that delegates to delete_node
. delete_node
should look familiar it simply traverses the tree searching for the node to be deleted, much like we did with the insert and search methods. Once we find it we call the remove_node
method which evaluates the three methods we discussed above.
If the Node has no children it simply sets it to nil. If it has only one child it sets it to that child. Finally, if it has two children we call the replace method using the Node’s right child. The replace method takes advantage of the successor and update methods to find the next successor, and update the Nodes appropriately.
This is really only the most basic implementation of a BST. I encourage you to look into traversals: in-order, pre-order, and post-order. Perhaps extend the code above to print out the tree using these traversal methods. You can also consider implementing a method that returns the height of your tree, whether or not the tree is balanced, or the number of Nodes in the tree. The possibilities are endless. Thanks for reading, below are some further study resources. Until next time.
Binary search tree - Wikipedia
Binary Search Tree | Set 1 (Search and Insertion) - GeeksforGeeks