Hierarchical Data Structures: Trees, Balanced BSTs, Heaps, and Tries
Trees are non-linear, hierarchical data structures composed of nodes connected by edges. They provide efficient logarithmic search, ordered traversals, priority queuing, and prefix matching across database indexing (B-Trees), memory management (Red-Black Trees), and scheduling algorithms (Heaps).
⚡ Quick Dive
Tree Data Structures Complexity Matrix
| Structure | Search | Insertion | Deletion | Space | Primary Real-World Application |
|---|---|---|---|---|---|
| Unsorted Binary Tree | $O(n)$ | $O(1)^*$ | $O(n)$ | $O(n)$ | Expression evaluation, ASTs in compilers |
| Binary Search Tree (BST) | $O(\log n)$ avg / $O(n)$ worst | $O(\log n)$ avg / $O(n)$ worst | $O(\log n)$ avg / $O(n)$ worst | $O(n)$ | Ordered data storage (unbalanced risks skew) |
| AVL Tree (Strictly Balanced) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(n)$ | Read-heavy lookups (faster search than Red-Black) |
| Red-Black Tree | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | $O(n)$ | Standard library maps (C++ std::map, Java TreeMap, Linux CFS) |
| Binary Heap (Priority Queue) | $O(1)$ (Peek Min/Max) | $O(\log n)$ (Push) | $O(\log n)$ (Pop Min/Max) | $O(n)$ | Dijkstra's shortest path, CPU task scheduling |
| Trie (Prefix Tree) | $O(L)$ ($L$ = string length) | $O(L)$ | $O(L)$ | $O(N \cdot L \cdot | \Sigma |
📖 Extended Guide
1. Tree Terminology & Topologies
( Root: 10 ) ◄── Level 0 (Height 2)
/ \
( Node: 5 ) ( Node: 15 ) ◄── Level 1 (Height 1)
/ \ / \
(Leaf:2)(Leaf:7)(Leaf:12)(Leaf:18) ◄── Level 2 (Height 0, Leaves)
- Height of Node: Number of edges on the longest path from node to a leaf.
- Depth of Node: Number of edges from the root to the node.
- Full Binary Tree: Every node has 0 or 2 children.
- Complete Binary Tree: Every level is completely filled except possibly the last, which is filled from left to right.
- Perfect Binary Tree: All interior nodes have 2 children and all leaves are at the same level (Total nodes $= 2^{h+1} - 1$).
2. Tree Traversals
Depth-First Traversals (DFS):
- In-Order (Left $\to$ Root $\to$ Right): Yields sorted order in a BST.
- Pre-Order (Root $\to$ Left $\to$ Right): Used for serializing/copying trees.
- Post-Order (Left $\to$ Right $\to$ Root): Used for bottom-up cleanup (deleting trees, calculating directory sizes).
Breadth-First Traversal (BFS / Level-Order):
- Uses an explicit queue to traverse level-by-level.
3. Binary Search Tree (BST) Operations
BST Property:
For every node $X$, all keys in the left subtree are $< X$, and all keys in the right subtree are $> X$.
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// Insert adds a value to the BST in O(log n) average time.
func Insert(root *TreeNode, val int) *TreeNode {
if root == nil {
return &TreeNode{Val: val}
}
if val < root.Val {
root.Left = Insert(root.Left, val)
} else if val > root.Val {
root.Right = Insert(root.Right, val)
}
return root
}
4. Self-Balancing Trees: AVL vs. Red-Black
Unbalanced BSTs can degenerate into $O(n)$ linked lists on sorted inputs.
Degenerate Skewed BST (O(n)): Balanced AVL / Red-Black Tree (O(log n)):
( 1 ) ( 2 )
\ / \
( 2 ) ( 1 ) ( 3 )
\
( 3 )
- AVL Trees: Enforce balance factor $|Height(Left) - Height(Right)| \le 1$. Strict balance gives faster searches but requires more rotations on insertion/deletion.
- Red-Black Trees: Enforce color invariants (root is black, no consecutive red nodes, equal black depth to all leaves). Looser balance guarantees maximum height $\le 2\log_2(n+1)$, resulting in faster inserts and deletes.
5. Binary Heaps & Priority Queues
A Binary Heap is a complete binary tree stored compactly in a flat array (zero pointer overhead):
Array Representation: [ 2, 5, 8, 12, 16, 23, 38 ]
Index Formulas:
- Left Child of index i = 2*i + 1
- Right Child of index i = 2*i + 2
- Parent of index i = (i - 1) / 2
Min-Heap Implementation in Go:
package main
import "fmt"
type MinHeap struct {
data []int
}
func (h *MinHeap) Push(val int) {
h.data = append(h.data, val)
h.siftUp(len(h.data) - 1)
}
func (h *MinHeap) Pop() (int, bool) {
if len(h.data) == 0 {
return 0, false
}
minVal := h.data[0]
lastIdx := len(h.data) - 1
h.data[0] = h.data[lastIdx]
h.data = h.data[:lastIdx]
if len(h.data) > 0 {
h.siftDown(0)
}
return minVal, true
}
func (h *MinHeap) siftUp(idx int) {
for idx > 0 {
parent := (idx - 1) / 2
if h.data[idx] >= h.data[parent] {
break
}
h.data[idx], h.data[parent] = h.data[parent], h.data[idx]
idx = parent
}
}
func (h *MinHeap) siftDown(idx int) {
n := len(h.data)
for {
smallest := idx
left := 2*idx + 1
right := 2*idx + 2
if left < n && h.data[left] < h.data[smallest] {
smallest = left
}
if right < n && h.data[right] < h.data[smallest] {
smallest = right
}
if smallest == idx {
break
}
h.data[idx], h.data[smallest] = h.data[smallest], h.data[idx]
idx = smallest
}
}
func main() {
h := &MinHeap{}
for _, v := range []int{15, 10, 20, 8, 12} {
h.Push(v)
}
for len(h.data) > 0 {
val, _ := h.Pop()
fmt.Printf("%d ", val) // 8 10 12 15 20 (Sorted Output)
}
fmt.Println()
}
6. Prefix Trees (Trie)
A Trie is a specialized search tree used to store associative keys (typically strings).
Trie for keys: "app", "apple", "apt"
(root)
│
[a]
│
[p]
/ \
(end)[p] [t](end)
│
[l]
│
[e](end)
- Query time is strictly $O(L)$ where $L$ is the length of the query string, completely independent of the total number of words in the dictionary $N$.