Demystifying Recursion in Binary Search Trees
Binary Search Trees (or BSTs) have always fascinated me, not only because of their inherent nature to keep things sorted but also due to the variety of algorithms they offer. From insertion to deletion, BSTs are robust. Today, I'd like to delve into a key concept that often perplexes beginners: recursion. Let's use it in the context of BSTs to insert and remove nodes.
Binary Search Tree 101
Before diving into the recursion, it's essential to understand the basic structure. At the very heart of a BST is its Node. This Node contains a value, and two references - one for its left child and one for its right child.
class Node:
def __init__(self, value):
self.value = value # The value or key
self.left = None # Reference to left child
self.right = None # Reference to right child
Then, there's the Binary_Search_Tree class, which houses the core BST functionality. Its primary attribute is the root node.
class Binary_Search_Tree:
def __init__(self):
# Initialize the root of the tree to None
self.root = None
Insertion - The Iterative Way
Insertion in a BST is based on comparison. When we want to insert a value, we start at the root and traverse the tree. If the new value is less than the current node's value, we move to the left child. Otherwise, we move to the right. This continues until we find an empty spot.
def insert(self, value):
# Create a new node with the given value
new_node = Node(value)
# If the tree is empty, set the new node as the root
if self.root is None:
self.root = new_node
return True
# Otherwise, use a temporary variable to traverse the tree
temp = self.root
while True:
# If the value already exists in the tree, exit without inserting
if new_node.value == temp.value:
return False # Value already exists
# If the value is less than the current node's value, go left
if new_node.value < temp.value:
if temp.left is None:
# If there's no left child, insert the new node here
temp.left = new_node
return True
# Move the temporary variable to the left child
temp = temp.left
# If the value is greater than the current node's value, go right
else:
if temp.right is None:
# If there's no right child, insert the new node here
temp.right = new_node
return True
# Move the temporary variable to the right child
temp = temp.right
Recursive Insertion
So, you might ask, where does recursion come in? Let’s see. The idea is simple. If the tree is empty, the new node becomes the root. If not, we decide to move left or right, based on the value.
def r_insert(self, value):
# If the tree is empty, set the new node as the root
if self.root == None:
self.root = Node(value)
# Otherwise, start the recursive insertion process
self.__r_insert(self.root, value)
The __r_insert
method does the heavy lifting:
def __r_insert(self, current_node, value):
# Base condition: if the current node is None, return a new node with the value
if current_node == None:
return Node(value)
# If the value is less than the current node's value, recursively insert to the left
if value < current_node.value:
current_node.left = self.__r_insert(current_node.left, value)
# Otherwise, recursively insert to the right
elif value > current_node.value:
current_node.right = self.__r_insert(current_node.right, value)
# Return the current node after the appropriate insertion
return current_node
The process keeps repeating for each node until we find the perfect spot for our new value.
Deletion in Binary Search Trees
Binary Search Trees, with their systematic organization and ability to maintain a sorted order, have a certain beauty to them. We've previously discussed insertion, and today, we're diving deep into a topic that many developers find challenging: deletion in BSTs.
Setting the Stage
To recap, our BST structure comprises nodes with potential left and right children:
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
The Binary Search Tree itself, starting with a root:
class Binary_Search_Tree:
def __init__(self):
self.root = None
Understanding the Deletion Challenge
When deleting a node from a BST, we encounter three primary scenarios:
Leaf Node: A node with no children. This is the simplest case, where we just remove the node from the tree.
Single Child Node: A node with one child. Here, we bypass the node to be deleted and link its parent to its only child.
Two Children Node: The trickiest scenario. For a node with two children, we can't just bypass it. Instead, we identify either the largest node of the left sub-tree (in-order predecessor) or the smallest node of the right sub-tree (in-order successor) to replace the deleted node.
The Deletion Process
Let's dive into the deletion method:
def delete_node(self, value):
self.__delete_node(self.root, value)
This method invokes a private recursive method __delete_node
:
def __delete_node(self, current_node, value):
if current_node is None:
return current_node
# Recursive calls for left and right subtrees
if value < current_node.value:
current_node.left = self.__delete_node(current_node.left, value)
elif value > current_node.value:
current_node.right = self.__delete_node(current_node.right, value)
# If value is same as current_node's value, then this node needs to be deleted
else:
# Node with only one child or no child
if current_node.left is None:
temp = current_node.right
current_node = None
return temp
elif current_node.right is None:
temp = current_node.left
current_node = None
return temp
# Node with two children
# Get the inorder successor (smallest in the right subtree)
current_node.value = self.min_value(current_node.right)
# Delete the inorder successor
current_node.right = self.__delete_node(current_node.right, current_node.value)
return current_node
You might have noticed the method min_value
which gets us the smallest value node from a BST:
def min_value(self, current_node):
while current_node.left is not None:
current_node = current_node.left
return current_node.value
Wrapping Up
BST deletions can be intimidating initially, especially due to the third scenario. However, with a systematic approach and clear understanding, it becomes much more manageable. Recursion plays a pivotal role here, handling each case beautifully and offering a concise solution.
It's important to practice and visualize these deletions with different examples to become comfortable. Remember, coding is as much about understanding as it is about implementation. The journey from a beginner to a proficient developer is all about overcoming challenges, and this is just one of them on the path.
Stay curious and keep decoding the intricacies of programming!
Subscribe to my newsletter
Read articles from Jose Ramirez directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by