Lesson 14 / 25
Binary Search Trees and Sorted Collections
Implement a BST and use bisect and sorted containers in practice.
Ordered data with fast search
A binary search tree (BST) keeps an ordering invariant: every key in a node's left subtree is smaller, and every key in the right subtree is larger. Search, insertion and deletion follow a single path, so they cost O(h), where h is the height: O(log n) when balanced, but O(n) when the tree degenerates, for example when keys are inserted in sorted order. Self-balancing trees (AVL, red-black, B-trees in databases) maintain O(log n) height through rotations. A BST also supports ordered queries that hash tables cannot: minimum and maximum, the next larger key (successor), range queries and iteration in sorted order. Python's standard library has no tree-based sorted map. In practice, use the bisect module to keep a sorted list (insort is O(n) because of shifting, but binary search is O(log n) and fast in C), or the third-party sortedcontainers package (SortedList, SortedDict), which is widely used and efficient. Implementing a BST yourself remains a great exercise and a common interview topic.
A BST and the bisect alternative
Insert, search and range queries; then the same with a sorted list.
from __future__ import annotations
import bisect
from dataclasses import dataclass
from typing import Iterator, Optional
@dataclass
class Node:
key: int
left: Optional[Node] = None
right: Optional[Node] = None
class BST:
def __init__(self):
self.root: Optional[Node] = None
def insert(self, key: int) -> None:
if self.root is None:
self.root = Node(key)
return
node = self.root
while True:
if key < node.key:
if node.left is None:
node.left = Node(key); return
node = node.left
elif key > node.key:
if node.right is None:
node.right = Node(key); return
node = node.right
else:
return # ignore duplicates
def __contains__(self, key: int) -> bool:
node = self.root
while node and node.key != key:
node = node.left if key < node.key else node.right
return node is not None
def range(self, lo: int, hi: int) -> Iterator[int]: # keys in [lo, hi], in order
def walk(node):
if node is None:
return
if lo < node.key:
yield from walk(node.left)
if lo <= node.key <= hi:
yield node.key
if node.key < hi:
yield from walk(node.right)
yield from walk(self.root)
tree = BST()
for k in [50, 30, 70, 20, 40, 60, 80]:
tree.insert(k)
print(40 in tree, 45 in tree) # True False
print(list(tree.range(35, 65))) # [40, 50, 60]
prices = [] # practical alternative: a sorted list
for p in [50, 30, 70, 20, 40, 60, 80]:
bisect.insort(prices, p)
lo, hi = bisect.bisect_left(prices, 35), bisect.bisect_right(prices, 65)
print(prices[lo:hi]) # [40, 50, 60]A guessing game
Searching a balanced BST is the number-guessing game where each answer ("higher" or "lower") halves the possibilities. A degenerate BST is a game where the host only ever says "higher", so you end up guessing every number.
Quick check: What is the worst-case search time in an unbalanced BST with n keys?
- O(1)
- O(log n)
- O(n log n)
- O(n)
Answer
O(n) — Sorted insertions create a chain-like tree with height n.