| name | algo-binary-search-trees |
| description | Implement/debug a binary search tree in Python: insert, search, delete (3 cases), successor/predecessor, inorder/level-order traversal. Use for BST coding, O(log n) vs O(n) degenerate cases, or choosing AVL/Red-Black. |
| tool_type | python |
| primary_tool | Python |
Binary Search Trees (BST)
When to Use
- Implementing a BST from scratch for an interview, assignment, or as a building block for another data structure.
- Explaining or reasoning about BST time complexity (search/insert/delete/traversal) and why sorted input is a worst case.
- Debugging a broken
delete (especially the two-children case) or successor/predecessor logic.
- Deciding whether a plain BST is sufficient or whether you need a self-balancing variant (AVL, Red-Black) for guaranteed O(log n).
- Needing inorder (sorted-order) or level-order (BFS) traversal of a tree of ordered values.
Version Compatibility
Pure Python standard library only — collections.deque, typing. Works on Python ≥3.10 (uses X | None union syntax); for 3.8/3.9 replace with Optional[X].
Prerequisites
- No external packages required.
- Helpful prior concepts:
algo-complexity-analysis (Big-O basics), algo-linear-binary-search (binary search on sorted arrays, for contrast).
- For guaranteed O(log n) instead of amortized/average behavior, see
algo-avl-trees or algo-red-black-trees.
Invariant: for every node, all values in the left subtree < node value < all values in the right subtree (no duplicates stored; duplicates are treated as no-ops on insert).
Complexity
| Operation | Average | Worst (degenerate) |
|---|
| Search / Insert / Delete | O(log n) | O(n) |
| Min / Max | O(log n) | O(n) |
| Successor / Predecessor | O(log n) | O(n) |
| Inorder traversal | O(n) | O(n) |
Worst case occurs on sorted (or reverse-sorted) input, which degenerates the tree into a linked list — use AVL or Red-Black tree to guarantee O(log n).
Goal: a correct, iterative BST supporting search/insert/delete plus ordering queries (min, max, successor, predecessor) and two traversal orders.
Approach: store left/right/parent pointers per node; keep insert/search iterative (O(height) stack-free); implement delete via the classic three-case rule, using _transplant to splice a subtree in place of a node.
from collections import deque
from typing import Any
class Node:
def __init__(self, value: Any) -> None:
self.value = value
self.left = self.right = self.parent = None
class BST:
def __init__(self) -> None:
self.root: Node | None = None
def insert(self, value: Any) -> Node:
"""Insert value, returning the new (or existing, if duplicate) node."""
if not self.root:
self.root = Node(value)
return self.root
node = self.root
while True:
if value < node.value:
if node.left is None:
node.left = Node(value)
node.left.parent = node
return node.left
node = node.left
elif value > node.value:
node.right :
node.right = Node(value)
node.right.parent = node
node.right
node = node.right
:
node
() -> Node | :
node = .root
node:
value == node.value:
node
node = node.left value < node.value node.right
() -> Node | :
node = node .root
node:
node.left:
node = node.left
node
() -> Node | :
node = node .root
node:
node.right:
node = node.right
node
() -> Node | :
node.right:
.find_min(node.right)
p = node.parent
p node == p.right:
node, p = p, p.parent
p
() -> Node | :
node.left:
.find_max(node.left)
p = node.parent
p node == p.left:
node, p = p, p.parent
p
() -> :
node = .search(value)
node:
._delete_node(node)
() -> :
node.left node.right:
succ = .find_min(node.right)
node.value = succ.value
._delete_node(succ)
:
child = node.left node.right
._transplant(node, child)
() -> :
u.parent:
.root = v
u == u.parent.left:
u.parent.left = v
:
u.parent.right = v
v:
v.parent = u.parent
() -> []:
result: [] = []
():
n:
_rec(n.left)
result.append(n.value)
_rec(n.right)
_rec(.root)
result
() -> []:
.root:
[]
result, q = [], deque([.root])
q:
n = q.popleft()
result.append(n.value)
n.left:
q.append(n.left)
n.right:
q.append(n.right)
result
() -> :
.search(value)
() -> :
bst = BST()
v [, , , , , , ]:
bst.insert(v)
bst.inorder() == [, , , , , , ]
bst bst
n40 = bst.search()
bst.successor(n40).value ==
bst.predecessor(n40).value ==
bst.delete()
bst
bst.inorder() == [, , , , , ]
bst.delete()
()
__name__ == :
demo()
Delete: Three Cases
| Node type | Action |
|---|
| Leaf | Remove directly (transplant with None) |
| One child | Replace node with that child |
| Two children | Copy inorder successor's value into the node, then delete the successor (which has at most one child) |
Pitfalls
- Sorted input degenerates to O(n): inserting
[1,2,3,4,5] creates a linked list; use a self-balancing tree (AVL, Red-Black) for production use where input order isn't controlled.
- Inorder successor during deletion: after copying the successor's value to the deleted node, you must delete the successor node — not the original node again (recursing on
succ, not node, avoids this).
- Parent pointer consistency: when implementing with parent pointers, only set
v.parent in _transplant if v is not None, or you'll crash on leaf deletion.
- Recursive height/traversal on deep trees: Python's default recursion limit (~1000) is hit on degenerate (near-linear) trees; use iterative traversal (e.g. an explicit stack, like
level_order's BFS) for untrusted input sizes.
- Duplicates: this implementation silently ignores inserting a value already present; decide explicitly whether your use case needs a count/multiset instead.
See Also
algo-avl-trees — self-balancing BST guaranteeing O(log n) via rotations.
algo-red-black-trees — another self-balancing BST, used by many standard library map/set implementations.
algo-linear-binary-search — binary search over a sorted array (no explicit tree structure).
algo-complexity-analysis — Big-O fundamentals referenced throughout this skill.