DSA course menu
Binary Search Trees (BST) in Python: Insert, Search and Delete
Learn binary search trees: the BST property, searching and inserting in O(h), deleting nodes with zero, one or two children, validating a BST, and why unbalanced BSTs become slow.
On this page
A binary search tree (BST) is a binary tree with one rule, the BST property: for every node, all values in its left subtree are smaller and all values in its right subtree are larger.
50
/ \
30 70
/ \ / \
20 40 60 80That rule turns every step down the tree into a binary search decision: smaller goes left, larger goes right.
Search and insert#
class Node:
def __init__(self, value):
self.value = value
self.left = self.right = None
def insert(root, value):
if root is None:
return Node(value)
if value < root.value:
root.left = insert(root.left, value)
elif value > root.value:
root.right = insert(root.right, value)
return root # duplicates are ignored
def search(root, value):
steps = 0
while root:
steps += 1
if value == root.value:
return True, steps
root = root.left if value < root.value else root.right
return False, steps
def inorder(node):
return inorder(node.left) + [node.value] + inorder(node.right) if node else []
root = None
for v in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, v)
print(inorder(root))
print(search(root, 60))
print(search(root, 65))[20, 30, 40, 50, 60, 70, 80]
(True, 3)
(False, 3)An inorder traversal of a BST always comes out sorted. Search and insert follow one path from the root, so they cost O(h), where h is the height.
Minimum, maximum and successor#
The smallest value is reached by going left until you cannot; the largest by going right:
class Node:
def __init__(self, value):
self.value = value
self.left = self.right = None
def insert(root, value):
if root is None:
return Node(value)
if value < root.value:
root.left = insert(root.left, value)
elif value > root.value:
root.right = insert(root.right, value)
return root
def minimum(node):
while node.left:
node = node.left
return node.value
def maximum(node):
while node.right:
node = node.right
return node.value
root = None
for v in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, v)
print(minimum(root), maximum(root), minimum(root.right))20 80 60minimum(root.right) is the inorder successor of the root: the next larger value. Deletion uses it.
Deleting a node#
There are three cases:
- No children: just remove it.
- One child: replace the node with its child.
- Two children: copy the inorder successor (smallest value in the right subtree) into the node, then delete the successor from the right subtree. The successor has no left child, so that second delete is case 1 or 2.
class Node:
def __init__(self, value):
self.value = value
self.left = self.right = None
def insert(root, value):
if root is None:
return Node(value)
if value < root.value:
root.left = insert(root.left, value)
elif value > root.value:
root.right = insert(root.right, value)
return root
def inorder(node):
return inorder(node.left) + [node.value] + inorder(node.right) if node else []
def delete(root, value):
if root is None:
return None
if value < root.value:
root.left = delete(root.left, value)
elif value > root.value:
root.right = delete(root.right, value)
else:
if root.left is None: # cases 1 and 2
return root.right
if root.right is None:
return root.left
successor = root.right # case 3
while successor.left:
successor = successor.left
root.value = successor.value
root.right = delete(root.right, successor.value)
return root
root = None
for v in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, v)
root = delete(root, 20) # leaf
print(inorder(root))
root = delete(root, 30) # one child (40)
print(inorder(root))
root = delete(root, 50) # two children: replaced by 60
print(inorder(root), "root is now", root.value)[30, 40, 50, 60, 70, 80]
[40, 50, 60, 70, 80]
[40, 60, 70, 80] root is now 60Validating a BST#
Checking only that each node is bigger than its left child and smaller than its right child is not enough: a value deep in the left subtree could still be larger than the root. Pass down the allowed range instead:
class Node:
def __init__(self, value, left=None, right=None):
self.value, self.left, self.right = value, left, right
def is_bst(node, low=float("-inf"), high=float("inf")):
if node is None:
return True
if not (low < node.value < high):
return False
return is_bst(node.left, low, node.value) and is_bst(node.right, node.value, high)
good = Node(50, Node(30, Node(20), Node(40)), Node(70))
sneaky = Node(50, Node(30, Node(20), Node(55)), Node(70)) # 55 is in the left subtree of 50
print(is_bst(good), is_bst(sneaky))True FalseIn the second tree, 55 is larger than its parent 30, which looks fine locally, but it sits in the left subtree of 50 and so breaks the rule.
The height problem#
A BST is only fast if it stays short. Inserting values that are already sorted makes every node a right child: the tree becomes a linked list and every operation is O(n).
class Node:
def __init__(self, value):
self.value = value
self.left = self.right = None
def insert(root, value):
if root is None:
return Node(value)
node = root
while True:
if value < node.value:
if node.left is None:
node.left = Node(value); break
node = node.left
else:
if node.right is None:
node.right = Node(value); break
node = node.right
return root
def height(node):
return -1 if node is None else 1 + max(height(node.left), height(node.right))
import random
random.seed(5)
values = list(range(500))
sorted_root = None
for v in values:
sorted_root = insert(sorted_root, v)
random.shuffle(values)
random_root = None
for v in values:
random_root = insert(random_root, v)
print("height after sorted inserts:", height(sorted_root))
print("height after shuffled inserts:", height(random_root))height after sorted inserts: 499
height after shuffled inserts: 18Self-balancing trees fix this by restructuring as they go; see Balanced Trees (AVL).
Common mistakes#
Practice#
Practice exercise
Write kth_smallest(root, k) that returns the kth smallest value (1-based) in a BST without building the full sorted list.
Show hint
Do an iterative inorder traversal (go left, pop, visit, go right) and count visits. Stop when the count reaches k.
Show solution
class Node:
def __init__(self, value):
self.value = value
self.left = self.right = None
def insert(root, value):
if root is None:
return Node(value)
if value < root.value:
root.left = insert(root.left, value)
else:
root.right = insert(root.right, value)
return root
def kth_smallest(root, k):
stack, node = [], root
while stack or node:
while node:
stack.append(node)
node = node.left
node = stack.pop()
k -= 1
if k == 0:
return node.value
node = node.right
return None
root = None
for v in [50, 30, 70, 20, 40, 60, 80]:
root = insert(root, v)
print([kth_smallest(root, k) for k in range(1, 8)])
print(kth_smallest(root, 9))[20, 30, 40, 50, 60, 70, 80]
NoneIt stops after k visits, so it costs O(h + k) instead of O(n).
Key takeaway#
Next: keeping trees short automatically, in Balanced Trees (AVL).
Finished this lesson?