A tree is a hierarchy of nodes: one root at the top, each node linked to its children, and no loops. Folders on a disk, an organisation chart and the categories of a plant catalogue are all trees. A binary search tree (BST) is a tree where each node has at most two children and the keys are ordered: smaller keys go left, larger keys go right.
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
root = Node(8, Node(3), Node(12)) # 8
# / \
# 3 12This lesson covers tree terminology, BST insertion and search, three ways to traverse a tree, and why an unbalanced BST can be as slow as a list. The examples organise plant catalogue numbers for a botanical garden. It builds on linked lists and recursion.
Tree structure
Key terms, using a plant catalogue as the example:
- Root: the single top node ("Plants").
- Parent / child: "Trees" is a child of "Plants" and the parent of "Oak".
- Leaf: a node with no children ("Oak").
- Subtree: a node together with all its descendants.
- Depth of a node: its number of edges from the root. Height of the tree: the depth of its deepest leaf.
catalogue = {"Plants": {"Trees": {"Oak": {}, "Birch": {}}, "Herbs": {"Mint": {}}}}
def leaves(tree):
for name, children in tree.items():
if children:
yield from leaves(children)
else:
yield name
print(list(leaves(catalogue))) # ['Oak', 'Birch', 'Mint']Nested dictionaries and objects with a children list represent general trees, where a node can have any number of children. In an ordinary rooted tree every node except the root has exactly one parent and there are no cycles, which is what lets recursive functions process trees cleanly: handle this node, then recurse into each subtree.
Search ordering
A BST keeps a rule at every node: all keys in the left subtree are smaller, all keys in the right subtree are larger. Searching compares the target with the current key and goes left or right, discarding a whole subtree each time, like binary search:
def search(node, key):
while node is not None:
if key == node.key:
return node
node = node.left if key < node.key else node.right
return None
def insert(node, key):
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
elif key > node.key:
node.right = insert(node.right, key)
return node # duplicates are ignoredInsertion follows the same path as a search and adds the new node where the search fell off the tree. You must choose a duplicate policy: ignore repeats (as here), store a count on the node, or consistently send equal keys to one side. Leaving it undecided produces trees whose searches disagree with their inserts.
Traversal
A traversal visits every node once. The three classic depth-first orders differ only in when the current node is visited relative to its subtrees:
| Order | Visit sequence | Typical use |
|---|---|---|
| Inorder | left, node, right | a BST's keys in sorted order |
| Preorder | node, left, right | copying or printing a tree's structure top-down |
| Postorder | left, right, node | deleting or sizing subtrees before their parent |
def inorder(node):
if node:
yield from inorder(node.left)
yield node.key
yield from inorder(node.right)
def preorder(node):
if node:
yield node.key
yield from preorder(node.left)
yield from preorder(node.right)Inorder traversal of a valid BST always yields sorted keys, which makes it a handy correctness check. The order you get depends on the tree's shape: the same keys inserted in a different order build a different tree with a different preorder. Breadth-first (level-order) traversal, using a queue, visits nodes row by row, and is covered in graph traversal.
Balance and costs
Search and insertion follow one path from the root, so their cost is proportional to the tree's height:
- A balanced tree with n nodes has height about log₂ n: around 20 for a million keys.
- A skewed tree, built by inserting keys in sorted order, becomes a chain where every node has only a right child: height n, and search degrades to O(n), no better than a linked list.
def height(node):
return 0 if node is None else 1 + max(height(node.left), height(node.right))A plain BST is not automatically balanced. Self-balancing variants, such as AVL trees and red-black trees, rotate nodes after insertions to keep the height logarithmic, guaranteeing O(log n) operations. Python doesn't include a balanced tree in the standard library; in practice, a dict covers exact lookups, and the bisect module or the third-party sortedcontainers package covers sorted data and range queries.
Independent practice and a worked check
Task: implement BST insertion and search with a duplicate policy, then compare balanced and skewed shapes. Store a count for duplicates instead of ignoring them:
class Node:
def __init__(self, key):
self.key, self.count, self.left, self.right = key, 1, None, None
def insert(node, key):
if node is None:
return Node(key)
if key == node.key:
node.count += 1
elif key < node.key:
node.left = insert(node.left, key)
else:
node.right = insert(node.right, key)
return node
def find(node, key):
while node and node.key != key:
node = node.left if key < node.key else node.right
return node.count if node else 0
root = None
for plant in [42, 17, 63, 17, 8, 42, 42]:
root = insert(root, plant)
print(find(root, 42), find(root, 17), find(root, 99)) # 3 2 0Then build trees from range(1, 8) and from [4, 2, 6, 1, 3, 5, 7] and compare their heights: 7 versus 3. For the worked check, the search cost in a simple BST is determined by the tree height.
Reference and next practice
For production needs, see the bisect module and the sortedcontainers package. Continue with graphs and graph traversal and revisit recursion. Practise with the Python exercises and Python quiz.
Trees and Binary Search Trees Summary
- Tree structure: A tree models parent and child relationships without cycles in its ordinary rooted form.
- Search ordering: A binary search tree maintains a defined ordering between a node and its subtrees.
- Traversal: Inorder, preorder and postorder visit the same structure in different sequences.
- Balance and costs: Search follows tree height, which can be logarithmic in a balanced tree or linear in a skewed one.
Trees and Binary Search Trees Examples
Each example shows the code, the output it produces and what each part does.
Trees and Binary Search Trees in a small program
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def inorder(node):
if node is not None:
yield from inorder(node.left)
yield node.key
yield from inorder(node.right)
root = Node(8, Node(3), Node(12))
print(list(inorder(root)))[3, 8, 12]Line-by-line explanation
- Lines 1 to 3Each node stores a key and up to two children.
- Lines 5 to 9Inorder traversal: left subtree, then the node, then the right subtree.
- Lines 11 to 12A three-node BST yields its keys in sorted order.
Balanced versus skewed insertion
class Node:
def __init__(self, key):
self.key, self.left, self.right = key, None, None
def insert(node, key):
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
elif key > node.key:
node.right = insert(node.right, key)
return node
def height(node):
return 0 if node is None else 1 + max(height(node.left), height(node.right))
def inorder(node):
if node:
yield from inorder(node.left)
yield node.key
yield from inorder(node.right)
def build(keys):
root = None
for k in keys:
root = insert(root, k)
return root
mixed = build([40, 20, 60, 10, 30, 50, 70])
sorted_input = build([10, 20, 30, 40, 50, 60, 70])
print(list(inorder(mixed)) == list(inorder(sorted_input)))
print("height when keys arrive mixed:", height(mixed))
print("height when keys arrive sorted:", height(sorted_input))True
height when keys arrive mixed: 3
height when keys arrive sorted: 7Line-by-line explanation
- Lines 29 to 30The same seven keys inserted in two orders.
- Line 31Both trees are valid BSTs, so inorder traversal gives the same sorted keys.
- Lines 32 to 33Mixed insertion order gives a balanced tree of height 3; sorted insertion builds a chain of height 7, as slow to search as a list.
Common Trees and Binary Search Trees Mistakes
Losing the new node in recursive insertion
def insert(node, key):
if node is None:
return Node(key)
if key < node.key:
insert(node.left, key) # returned node is thrown awaydef insert(node, key):
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
...
return nodeAssuming a BST stays fast whatever the input
for plant_id in sorted(all_ids): # sorted input builds a skewed chain
root = insert(root, plant_id)import random
ids = list(all_ids)
random.shuffle(ids) # or use a balanced tree / bisect / dict
for plant_id in ids:
root = insert(root, plant_id)Trees and Binary Search Trees in Real Projects
Trees in real software
Trees are everywhere: file systems, HTML and XML documents (the DOM), JSON data, the abstract syntax trees Python builds from your code (see the ast module), decision trees in machine learning, and the B-trees behind database indexes. Balanced search trees power ordered maps in many languages' standard libraries and the indexes that make SQL WHERE and ORDER BY fast.
In Python applications, you'll mostly traverse trees that already exist, such as nested JSON or a parsed document, with the recursive patterns shown here, rather than implement a BST yourself.
Trees and Binary Search Trees FAQ
What is the difference between a binary tree and a binary search tree?
A binary tree only limits each node to two children. A binary search tree also keeps keys ordered, smaller on the left and larger on the right at every node, which is what makes efficient searching possible.
What is inorder traversal used for?
Visiting a BST's keys in sorted order. It visits the left subtree, then the node, then the right subtree. Checking that inorder output is sorted is also a quick way to test that a tree is a valid BST.
Why can a binary search tree become slow?
If keys are inserted in sorted or nearly sorted order, each new key goes to the same side and the tree becomes a long chain. Its height grows to n, so searches take O(n). Balanced trees prevent this by restructuring after insertions.
Does Python have a built-in tree data structure?
No general-purpose one. Use nested dictionaries or small classes for hierarchies, dict for fast exact lookups, heapq for priority queues and bisect or the sortedcontainers package for sorted data with range queries.
Should I write tree traversals recursively?
Recursion matches a tree's structure and is the clearest choice for trees of moderate depth. For very deep or skewed trees, Python's recursion limit (around 1,000) can be hit, so use an explicit stack in a loop instead.
Trees and Binary Search Trees Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What determines search cost in a simple BST?
Which review concern is valid for trees and binary search trees?
Trees and Binary Search Trees Quiz
2 questions to check what you learned.
What determines search cost in a simple BST?
- Only the root value
- The tree height
- Python ignores the distinction
Which statement correctly describes traversal?
- Inorder, preorder and postorder visit the same structure in different sequences.
- Only the example output matters, not the rule
- Every operation has the same behavior