Algorithmics study and revision notes
2026-08-22
A tree turns hierarchy into paths and recursive subproblems. File systems, syntax, search dictionaries, database indexes, spatial partitions, priority queues, and connectivity algorithms all use tree-shaped representations. Their performance depends less on the word “tree” than on shape, ordering invariants, branching, and the operations required.
After studying this note, you should be able to:
You should understand records, references or array indices, recursion, stacks and queues, sorting order, and asymptotic and amortised analysis. No previous balanced-tree implementation is assumed.
An undirected tree is a connected acyclic graph. A tree with nodes has exactly edges and a unique simple path between every pair of nodes. Choosing one node as the root orients the hierarchy: every other node has one parent, and nodes may have children, siblings, ancestors, and descendants.
A leaf has no children. The depth of a node is the number of edges from the root. Its height is the maximum number of edges on a downward path to a leaf; the tree height is the root height. The degree of a rooted node is its number of children.
In an ordered tree, sibling order matters. A binary tree distinguishes a left and right child even when only one exists. Several shape terms must not be confused:
A perfect binary tree of height contains
nodes. A complete tree supports compact array storage. With zero-based indexing, node has potential children and , and a non-root node has parent . This layout becomes the basis of binary heaps.
A fixed-small-degree node can store one field per child. A general tree can store a dynamic child array or list. The first-child/next-sibling representation uses two links per node to encode arbitrary branching. Parent pointers make upward navigation cheap at an additional space and update cost.
An array representation removes explicit links only when shape determines positions, as in a complete binary tree. Sparse arbitrary trees would waste array slots.
Static ordered trees can also store shape
succinctly. A depth-first traversal that emits bit
1 as an opening parenthesis on entry and bit 0
as its closing parenthesis on exit uses exactly
bits for an
-node
shape before labels and navigation indexes.
Here rank counts opening bits up to a position, select locates the position of a given opening bit, and matching-parenthesis support finds the closing bit paired with an opening. Indexes for these operations can recover parent, child, and subtree relations without pointers. The price is more involved construction and updates, and the indexes must be included in any space claim.
The core representation invariant is reachability: every non-root node is reached from its parent exactly once, no link creates a cycle, and all recorded metadata agrees with the represented subtree. Ordered structures add constraints on child order or keys.
A general depth-first traversal has an event before and after every subtree:
dfs(v):
if v == null: return
enter(v) // preorder event
for child in children(v):
dfs(child)
leave(v) // postorder event
For a binary tree, an inorder event occurs between the left and right recursive calls. Preorder is useful for copying or prefix notation; postorder processes children before a parent and is useful for deletion or evaluation; inorder exposes BST keys in sorted order.
Breadth-first traversal replaces recursion with a queue:
bfs(root):
if root == null: return
Q = empty queue
enqueue(Q, root)
while Q is not empty:
v = dequeue(Q)
visit(v)
for child in children(v):
enqueue(Q, child)
By induction on subtrees, DFS visits every node reachable from its argument exactly once. BFS enqueues a node exactly when its unique parent is processed, so it visits nodes in nondecreasing depth. Both take time. DFS uses call-stack space; BFS uses queue space where is the maximum width. Either can need space on an unfavourable shape.
A traversal order alone may not identify tree shape. A binary-tree serialization can emit a value in preorder and a marker for every null child:
serialize(v):
if v == null: print "#"; return
print v.value
serialize(v.left)
serialize(v.right)
Null markers make reconstruction unambiguous. Balanced parentheses
give another representation: print an opening parenthesis at
enter, the label, each child serialization, and a closing
parenthesis at leave.
In an expression tree, leaves are operands and internal nodes are operators. Postorder recursively obtains child values before applying the parent operator, exactly matching postfix evaluation. Correctness follows structurally: if both child subexpressions are evaluated correctly, applying the stored operator evaluates their parent expression.
A trie stores strings by prefixes. An edge carries a
character; a node represents the prefix along its root path; a terminal
flag says that the prefix itself is a stored key. The flag is necessary
when one key is a prefix of another, such as he and
hers.
To search a pattern of length , follow one outgoing edge per character and then test the terminal flag. Time is if child lookup is . A full alphabet-sized child array gives fast lookup but wastes space at sparse nodes; maps or sorted edge lists use less space with a different lookup cost. Path compression merges nonbranching paths and leads toward radix and suffix-tree structures.
Fix a duplicate policy. One rotation-stable choice orders records by
(key, unique tie-breaker); another stores all records with
an equal key together in one node. In the resulting total record order,
every record in a node’s left subtree is smaller and every record in its
right subtree is greater. This BST invariant implies
that inorder traversal lists user-visible keys in nondecreasing
order.
bst_search(root, k):
v = root
while v != null and v.key != k:
if k < v.key: v = v.left
else: v = v.right
return v
At each step, the invariant proves that the discarded subtree cannot
contain k. Search takes
time and
iterative space. Insertion follows the same path until a null child is
found, then attaches the new record there. Minimum follows left links;
maximum follows right links.
The successor of a node is the minimum of its right subtree when that subtree exists. Otherwise, move upward until first arriving from a left child. Both cases identify the next record in inorder order; its user-visible key can be equal when duplicate records use tie-breakers.
Deletion has three structural cases:
When records have associated values, move or transplant the complete record, not only its key. Copying a key alone can pair it with the wrong value. Parent links, subtree metadata, and the root reference must also be updated.
Insert keys 8,3,10,1,6,14,4,7,13. Searching for 7
follows 8 -> 3 -> 6 -> 7, so its cost is
proportional to that path.
Deleting 3 uses the two-child case. Its successor is 4, the minimum
in the right subtree rooted at 6. Move the key-value record for 4 into
3’s structural position, then detach the old 4 node, which is a leaf.
The left subtree still contains only keys below 4, and the remaining
right subtree contains 6,7, both above 4. Thus the BST
invariant is restored. The operation touches
nodes.
An ascending insertion sequence can produce a chain of height , making all operations linear. Balance is therefore a structural guarantee, not a consequence of the BST order alone.
A rotation changes a parent-child relationship while preserving
inorder order. In a right rotation around y, its left child
x moves up, x’s right subtree becomes
y’s left subtree, and y becomes
x’s right child. The inorder sequence remains
A, x, B, y, C before and after. Parent pointers and all
augmentation metadata must be recomputed.
An AVL tree stores heights or balance factors and requires the two child heights of every node to differ by at most one. After insertion, repairing the lowest unbalanced ancestor with one or two rotations restores the required height along the remaining path. After deletion, height loss can propagate, so additional ancestors may also need repair.
Why does this local rule control the whole height? The minimum number of nodes at height follows a Fibonacci-like recurrence, which implies and therefore logarithmic search and update time.
A red-black tree stores one colour bit and maintains:
Every root-to-null path has the same black-node count, and red nodes cannot be adjacent. Therefore any such path has at most twice as many internal nodes as its black-node count, giving height at most . Insertions and deletions recolour and rotate to restore the properties. AVL trees enforce tighter height; red-black trees usually permit fewer structural update repairs.
A splay tree moves an accessed node toward the root and gives logarithmic amortised, not per-operation worst-case, bounds. A treap stores a BST key and an independently randomized priority, maintaining BST order on keys and heap order on priorities; it has expected logarithmic height.
When one storage-block access is far more expensive than comparisons in memory, a wide shallow tree is preferable. A B-tree of minimum degree maintains:
Search chooses one child after locating a key interval inside a node. Insertion never descends into a full child without first splitting it: promote the median key to the parent and leave keys in each new child. Splitting the root creates a new root and increases height by one.
Deletion must also preserve minimum occupancy. For an internal target, use a predecessor or successor from a child with spare capacity, or merge two minimal children with their separating key. Before descending into a minimal child, a top-down implementation similarly borrows through the parent from a sibling or merges with one.
These repair rules preserve the invariants; the performance gain comes from matching a node to a storage block.
If a node is sized to one block and holds keys and child pointers, the height and number of block transfers are . CPU work inside each node depends on whether its keys are scanned, binary-searched, or indexed. “” here describes block-scale branching, not an unexplained universal constant.
One-dimensional key order does not directly answer geometric queries. Spatial trees instead attach a region or bounding object to each subtree, then try to prove that a query can ignore whole regions:
These structures do not guarantee logarithmic queries for every geometry. Degenerate distributions, overlapping bounds, and the curse of dimensionality can force extensive search. State the metric, distribution, exact/approximate guarantee, and output size.
Augmentation stores metadata computable from a node and its children.
If size(v) = 1 + size(v.left) + size(v.right), a balanced
BST can select rank
:
select(v, k): // zero-based rank
left_size = size(v.left)
if k < left_size: return select(v.left, k)
if k == left_size: return v
return select(v.right, k - left_size - 1)
The BST invariant fixes relative rank, and subtree sizes choose the only possible branch. Time is . Updates recompute size along changed paths and after rotations.
For intervals ordered by left endpoint, storing the maximum right endpoint in each subtree allows overlap search to prune a left subtree whose maximum endpoint lies before the query. Correctness requires proving that every pruned subtree cannot contain an answer. The general recipe is: define metadata locally, prove the pruning rule, and update every structural change.
Here the trees are an internal representation, not the abstract value exposed to clients. The ADT represents a partition; changing parent links is valid whenever it preserves which elements share a representative.
Union-find maintains a partition under merging:
make_set(x) creates the singleton set containing
x;find(x) returns a representative of x’s
current set; andunion(x,y) merges the sets containing them.A parent-pointer forest represents each set by one rooted tree. Roots represent sets.
make_set(x):
parent[x] = x
rank[x] = 0
find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
union(x, y):
rx = find(x); ry = find(y)
if rx == ry: return
if rank[rx] < rank[ry]: swap(rx, ry)
parent[ry] = rx
if rank[rx] == rank[ry]: rank[rx] = rank[rx] + 1
Union links only roots, so it cannot create a cycle between nodes already in one tree. Path compression changes parent links to the same representative, so it does not change set membership. Union by rank ensures that a rank- root represents at least elements, hence rank is at most .
After union(1,2), union(3,4), and
union(1,3), a possible path is
4 -> 3 -> 1. Calling find(4) returns 1
and rewrites it to 4 -> 1.
For a sequence of operations on at most elements, including the singleton creations, union by rank plus path compression takes total time. The inverse Ackermann function grows so slowly that the amortised cost is effectively constant for practical sizes, though it is not literally a constant worst-case bound for each call.
| Structure | Principal bound | Assumption or strength |
|---|---|---|
| Unbalanced BST | , worst | simple dynamic ordered set |
| AVL/red-black tree | guaranteed height under updates | |
| B-tree | block transfers | block-scale branching |
| Trie | for key length | constant-time child lookup |
| k-d/R-tree family | data- and query-dependent | geometric pruning |
| Augmented balanced BST | base operation plus local metadata | specialised rank/interval queries |
| Union-find | amortised | unions and finds, no set splitting |
UnionFind source is a compact Python implementation
using path compression and weighted linking. Looking up an unknown
object creates a singleton, which is an API choice rather than a
universal union-find rule.KDTree implements exact and approximate
nearest-neighbour queries in Python. Its documentation warns that around
20 dimensions can already erase the advantage over brute force, and
mutating uncopied input data can invalidate results.BTreeMap provides an ordered map, range queries, and
ordered iteration backed by a B-tree. Its public API deliberately hides
node degree and layout, so textbook page-transfer constants cannot be
inferred from the type alone.TreeMap is a red-black NavigableMap with
guaranteed logarithmic basic operations. It is not synchronized, and its
ordering should be consistent with equals when used as a
Map.