Trees, Traversals and BSTs: Why Balance Is the Only Thing Standing Between You and a Linked List
Published:
Heights, and why they are everything
A binary tree is nodes with up to two children. Its height \(h\) is the longest root-to-leaf edge count. With \(n\) nodes, \(h\) can be anywhere in
the lower bound because a tree of height \(h\) holds at most \(2^{h+1}-1\) nodes, the upper bound achieved by a path. Every search, insert and delete on a search tree walks one root-to-leaf path, so all of them are \(\Theta(h)\), and which end of that range you are at is the entire question.
The four traversals
All four visit every node once, so all are \(\Theta(n)\) time. The depth-first three use \(\Theta(h)\) stack space; level-order uses \(\Theta(w)\) for the maximum level width \(w\), which is \(\Theta(n)\) in a balanced tree.
from collections import deque
class Node:
def __init__(self, key, left=None, right=None):
self.key, self.left, self.right = key, left, right
def inorder(node, out): # left, node, right
if node:
inorder(node.left, out); out.append(node.key); inorder(node.right, out)
def preorder(node, out): # node, left, right
if node:
out.append(node.key); preorder(node.left, out); preorder(node.right, out)
def postorder(node, out): # left, right, node
if node:
postorder(node.left, out); postorder(node.right, out); out.append(node.key)
def level_order(root): # BFS, one queue
out, q = [], deque([root] if root else [])
while q:
node = q.popleft()
out.append(node.key)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
return out
# 4
# / \
# 2 5
# / \
# 1 3
root = Node(4, Node(2, Node(1), Node(3)), Node(5))
out = []; inorder(root, out); print(out) # [1, 2, 3, 4, 5] -- sorted
out = []; preorder(root, out); print(out) # [4, 2, 1, 3, 5]
out = []; postorder(root, out); print(out) # [1, 3, 2, 5, 4]
print(level_order(root)) # [4, 2, 5, 1, 3]
Each has a job. Pre-order emits a parent before its children, so it is the serialisation order, replay it and you rebuild the same shape. In-order on a BST yields sorted keys, which is the cheapest correctness check you can run on one. Post-order finishes children first, so it is the traversal for anything computed from children: subtree sizes, heights, freeing memory, evaluating an expression tree. Level-order explores by depth, so it finds the shallowest node satisfying a predicate and gives you per-level output.
The BST invariant
Every key in a node’s left subtree is smaller than the node, every key in its right subtree is larger. The invariant is a contract about the whole subtree, not just the immediate children, checking only children is the classic wrong is_valid_bst.
The payoff: one comparison at a node discards an entire subtree. If the tree is balanced, that halves the candidates, giving \(\Theta(\log n)\) search. Insert descends to a leaf and attaches there. Delete is the fiddly one: a node with two children is replaced by its in-order successor (leftmost node of the right subtree), which is then deleted from its old position.
Nothing in the insert rule maintains balance, and that is the problem.
Self-balancing, stated as guarantees
You do not need the rotation cases in an interview; you need the contracts.
| Structure | Balance condition | Height bound | Search / insert / delete |
|---|---|---|---|
| Plain BST | none | \(n-1\) worst | \(\Theta(n)\) worst |
| AVL | subtree heights differ by \(\le 1\) at every node | \(\approx 1.44 \log_2 n\) | \(\Theta(\log n)\) worst |
| Red-black | no root-leaf path more than twice another | \(\le 2\log_2(n+1)\) | \(\Theta(\log n)\) worst |
| B-tree (order \(m\)) | all leaves at equal depth | \(O(\log_m n)\) | \(O(\log n)\) worst, few disk reads |
Both AVL and red-black restore their invariant with \(O(1)\) rotations per update, found on the way back up the insertion path. AVL is more strictly balanced, so lookups are marginally shorter; red-black does less restructuring work per update, which is why it backs most standard-library ordered maps (std::map, Java’s TreeMap) and the Linux kernel’s schedulers. B-trees widen the node so that one disk or page read covers many keys, the reason every relational database index is one.
Python ships no balanced tree. For ordered data use bisect on a sorted list (\(\Theta(\log n)\) search, \(\Theta(n)\) insert), or the third-party sortedcontainers. Most of the time a dict is the right answer, because you did not actually need order.
Tries
A trie keys on the path, not on comparisons: each edge is one character, so a word of length \(m\) costs \(\Theta(m)\) to look up regardless of how many words are stored. That is its selling point over a hash table, which also gives \(\Theta(m)\) to hash the string but cannot answer “all keys with this prefix”, a trie enumerates a prefix’s subtree directly. The cost is memory: a node per character per distinct prefix. Autocomplete, IP routing tables and subword tokeniser vocabularies are the usual applications.
Recap
- All BST operations are \(\Theta(h)\), with \(\lfloor\log_2 n\rfloor \le h \le n-1\); quoting \(O(\log n)\) is a claim about balance.
- Pre-order serialises, in-order sorts a BST, post-order aggregates from children, level-order is BFS and finds the shallowest match. All \(\Theta(n)\) time; DFS uses \(\Theta(h)\) stack.
- AVL keeps height near \(1.44\log_2 n\), red-black within \(2\log_2(n+1)\); both give \(\Theta(\log n)\) worst-case operations with \(O(1)\) rotations per update.
- B-trees flatten the tree so one page read covers many keys, the structure behind database indexes.
- A trie costs \(\Theta(m)\) in key length rather than \(\Theta(\log n)\) in dictionary size, and is the structure that can answer prefix queries.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. Introduction to Algorithms, 4th ed. MIT Press, 2022, ch. 12 (BSTs) and 13 (red-black trees).
- Adelson-Velsky, G. M., & Landis, E. M. An algorithm for the organization of information. Doklady Akademii Nauk SSSR 146, 263–266, 1962, AVL trees.
- Guibas, L. J., & Sedgewick, R. A dichromatic framework for balanced trees. 19th Annual Symposium on Foundations of Computer Science (FOCS), 8–21, 1978.
- Bayer, R., & McCreight, E. Organization and maintenance of large ordered indices. Acta Informatica 1(3), 173–189, 1972, B-trees.
- Fredkin, E. Trie memory. Communications of the ACM 3(9), 490–499, 1960.
- Python Software Foundation. bisect, Array bisection algorithm.
- Jenks, G. sortedcontainers, pure-Python sorted list, dict and set.
