Data Structures: Linear, Tree & Hash-Based
When to Use
- Deciding which structure fits a task (e.g. "should I use a list or a hash table for k-mer counts") before writing code.
- Implementing or debugging a linked list, stack, queue, or dynamic array from scratch (interview-style or teaching).
- Building/traversing a BST, AVL, or Red-Black tree for sorted-order or range-query needs (genome interval index, position lookup).
- Designing a hash table (chaining/open addressing) or a Bloom filter for exact or approximate membership tests (k-mer sets, known-variant lookup, read dedup).
- Reasoning about complexity (O(1) vs O(log n) vs O(n)) to justify a structure choice in a PR or design doc.
Version Compatibility
Pure Python stdlib (hashlib, math) — no external dependencies. Works on Python ≥3.8; f-strings/walrus not required. Patterns generalize to any language with references/pointers (C, Java, Go).
Prerequisites
- Comfortable with Python classes, references vs values, and recursion.
- Big-O notation (see
algo-complexity-analysis for the formal treatment).
- No packages to install.
Quick Reference: Complexity
| Structure |
Access |
Search |
Insert |
Delete |
Space |
| Singly linked list |
O(n) |
O(n) |
O(1) head/tail* |
O(n) |
O(n) |
| Doubly linked list |
O(n) |
O(n) |
O(1) both ends |
O(1)‡ |
O(n) |
| Dynamic array |
O(1) |
O(n) |
O(1) amortized tail |
O(n) |
O(n) |
| Stack / Queue |
O(1) top/front |
— |
O(1) |
O(1) |
O(n) |
| BST (avg / worst) |
O(log n) / O(n) |
O(log n) / O(n) |
O(log n) / O(n) |
O(log n) / O(n) |
O(n) |
| AVL / Red-Black |
O(log n) |
O(log n) |
O(log n) |
O(log n) |
O(n) |
| Hash table |
O(1) avg |
O(1) avg |
O(1) avg |
O(1) avg |
O(n) |
| Bloom filter |
— |
O(k) |
O(k) |
N/A |
O(m) bits |
* requires tail pointer; ‡ given a node reference.
Key Patterns
- Linked lists: two-pointer (Floyd cycle detection:
slow +1, fast +2), dummy-node trick for merge/delete edge cases, in-place reverse.
- Stacks: bracket/Newick validation (push on open, pop-and-match on close); postfix eval; iterative DFS.
- Dynamic arrays: 2x growth → O(1) amortized append (CPython actually uses ~1.125x). Never
np.append in a loop — it's O(n²); pre-allocate instead.
- BST: inorder traversal yields sorted order; delete-with-two-children uses the inorder successor; sorted input degenerates to O(n) height — use AVL/RB or randomize (treap) in production.
- AVL: balance factor
height(left) - height(right), |bf| ≤ 1, height ≤ 1.44·log₂(n). LL→right-rotate, RR→left-rotate, LR/RL→double rotate.
- Red-Black: 5 invariants (root BLACK, RED node's children BLACK, equal black-height on all paths); fewer rotations than AVL → preferred for write-heavy workloads (Linux CFS,
std::map, Java TreeMap).
- Hash tables: load factor
α = n/m; rehash chaining at α>0.75, open addressing at α>0.5; open addressing needs tombstones on delete or probe chains break.
- Bloom filters: no false negatives, no deletion (use a counting Bloom filter if you need deletes); ~10 bits/element ≈ 1% false-positive rate.
Core Operations
Goal: implement a hash table with separate chaining that supports O(1)-average put/get for arbitrary keys (e.g. k-mer strings).
Approach: hash the key into a bucket index, store [key, value] pairs in that bucket's list, and linearly scan the (small, load-factor-bounded) bucket on put/get.
class HashTable:
"""Separate-chaining hash table. O(1) average put/get when load factor stays low."""
def __init__(self, size=7):
self.buckets = [[] for _ in range(size)]
self.size = size
def _h(self, k):
return hash(k) % self.size
def put(self, k, v):
bucket = self.buckets[self._h(k)]
for item in bucket:
if item[0] == k:
item[1] = v
return
bucket.append([k, v])
def get(self, k):
for item in self.buckets[self._h(k)]:
if item[0] == k:
return item[1]
return None
Goal: test set membership in O(k) time and O(m) bits with a tunable false-positive rate and zero false negatives (e.g. "is this variant in the known-common set?").
Approach: size the bit array m and hash-function count k from the target false-positive rate fp and expected element count n, then set/check k bit positions derived from two independent hashes (double hashing avoids needing k distinct hash functions).
import math
import hashlib
class BloomFilter:
"""Probabilistic set membership: no false negatives, tunable false-positive rate."""
def __init__(self, n, fp=0.01):
self.m = int(-n * math.log(fp) / math.log(2) ** 2)
self.k = max(1, int(self.m / n * math.log(2)))
self.bits = bytearray(self.m)
def _hashes(self, item):
h1 = int(hashlib.md5(str(item).encode()).hexdigest(), 16)
h2 = int(hashlib.sha1(str(item).encode()).hexdigest(), 16)
return [(h1 + i * h2) % self.m for i in range(self.k)]
def add(self, item):
for i in self._hashes(item):
self.bits[i] = 1
def __contains__(self, item):
return all(self.bits[i] for i in self._hashes(item))
Goal: keep a binary tree height-balanced (O(log n) guaranteed) after insert, for sorted range queries over genome coordinates.
Approach: track a height field per node; after insert, walk back up recomputing height and balance factor, applying a single or double rotation the moment |bf| > 1.
def height(node):
"""Height of a node, treating None as height 0."""
return node.height if node else 0
def rotate_right(z):
"""LL-case fix: promote z.left to root of this subtree."""
y, z.left = z.left, z.left.right
y.right = z
z.height = 1 + max(height(z.left), height(z.right))
y.height = 1 + max(height(y.left), height(y.right))
return y
def rotate_left(z):
"""RR-case fix: promote z.right to root of this subtree."""
y, z.right = z.right, z.right.left
y.left = z
z.height = 1 + max(height(z.left), height(z.right))
y.height = 1 + max(height(y.left), height(y.right))
return y
Pitfalls
- Linked list: forgetting to update the
tail pointer on head-insert or delete-last.
- Stack/queue: check empty before pop/dequeue, or you'll raise/segfault on the wrong operation.
- Dynamic array:
np.append in a loop is O(n²); pre-allocate a NumPy array when the final size is known.
- BST: sorted input yields O(n) height — never use a plain BST on pre-sorted genomic positions without balancing.
- AVL: update the child's height before the new parent's after a rotation, or heights go stale.
- Red-Black: new nodes are always inserted RED; the root must be recolored BLACK after every fix-up.
- Hash table (open addressing): deletes need tombstones, or later searches stop early at the "empty" slot.
- Bloom filter: cannot delete a single item (bit is shared); pick m/n ≥ 10 for <1% false-positive rate;
in can lie (false positive) but never misses a true member.
See Also
algo-hash-tables-bloom — deep dive on hash table collision strategies and Bloom filter tuning.
algo-avl-trees, algo-red-black-trees, algo-binary-search-trees — full implementations with insert/delete/rebalance.
algo-complexity-analysis — formal Big-O derivations behind the complexity table above.
python-collections-regex — when collections.deque, heapq, or sortedcontainers already solve the problem instead of hand-rolling a structure.
1---2name: linear-tree-hash-structures3description: Implement Python linked lists, stacks/queues, BST/AVL/Red-Black trees, hash tables, Bloom filters with Big-O tradeoffs. Use when choosing a data structure, k-mer hash counting, VCF dedup Bloom filters, or interval trees.4---56# Data Structures: Linear, Tree & Hash-Based78## When to Use910- Deciding which structure fits a task (e.g. "should I use a list or a hash table for k-mer counts") before writing code.11- Implementing or debugging a linked list, stack, queue, or dynamic array from scratch (interview-style or teaching).12- Building/traversing a BST, AVL, or Red-Black tree for sorted-order or range-query needs (genome interval index, position lookup).13- Designing a hash table (chaining/open addressing) or a Bloom filter for exact or approximate membership tests (k-mer sets, known-variant lookup, read dedup).14- Reasoning about complexity (O(1) vs O(log n) vs O(n)) to justify a structure choice in a PR or design doc.1516## Version Compatibility1718Pure Python stdlib (`hashlib`, `math`) — no external dependencies. Works on Python ≥3.8; f-strings/walrus not required. Patterns generalize to any language with references/pointers (C, Java, Go).1920## Prerequisites2122- Comfortable with Python classes, references vs values, and recursion.23- Big-O notation (see `algo-complexity-analysis` for the formal treatment).24- No packages to install.2526## Quick Reference: Complexity2728| Structure | Access | Search | Insert | Delete | Space |29|-----------|--------|--------|--------|--------|-------|30| Singly linked list | O(n) | O(n) | O(1) head/tail* | O(n) | O(n) |31| Doubly linked list | O(n) | O(n) | O(1) both ends | O(1)‡ | O(n) |32| Dynamic array | O(1) | O(n) | O(1) amortized tail | O(n) | O(n) |33| Stack / Queue | O(1) top/front | — | O(1) | O(1) | O(n) |34| BST (avg / worst) | O(log n) / O(n) | O(log n) / O(n) | O(log n) / O(n) | O(log n) / O(n) | O(n) |35| AVL / Red-Black | O(log n) | O(log n) | O(log n) | O(log n) | O(n) |36| Hash table | O(1) avg | O(1) avg | O(1) avg | O(1) avg | O(n) |37| Bloom filter | — | O(k) | O(k) | N/A | O(m) bits |3839\* requires tail pointer; ‡ given a node reference.4041## Key Patterns4243- **Linked lists**: two-pointer (Floyd cycle detection: `slow` +1, `fast` +2), dummy-node trick for merge/delete edge cases, in-place reverse.44- **Stacks**: bracket/Newick validation (push on open, pop-and-match on close); postfix eval; iterative DFS.45- **Dynamic arrays**: 2x growth → O(1) amortized append (CPython actually uses ~1.125x). Never `np.append` in a loop — it's O(n²); pre-allocate instead.46- **BST**: inorder traversal yields sorted order; delete-with-two-children uses the inorder successor; sorted input degenerates to O(n) height — use AVL/RB or randomize (treap) in production.47- **AVL**: balance factor `height(left) - height(right)`, |bf| ≤ 1, height ≤ 1.44·log₂(n). LL→right-rotate, RR→left-rotate, LR/RL→double rotate.48- **Red-Black**: 5 invariants (root BLACK, RED node's children BLACK, equal black-height on all paths); fewer rotations than AVL → preferred for write-heavy workloads (Linux CFS, `std::map`, Java `TreeMap`).49- **Hash tables**: load factor `α = n/m`; rehash chaining at α>0.75, open addressing at α>0.5; open addressing needs tombstones on delete or probe chains break.50- **Bloom filters**: no false negatives, no deletion (use a counting Bloom filter if you need deletes); ~10 bits/element ≈ 1% false-positive rate.5152## Core Operations5354**Goal:** implement a hash table with separate chaining that supports O(1)-average put/get for arbitrary keys (e.g. k-mer strings).55**Approach:** hash the key into a bucket index, store `[key, value]` pairs in that bucket's list, and linearly scan the (small, load-factor-bounded) bucket on put/get.5657```python58class HashTable:59 """Separate-chaining hash table. O(1) average put/get when load factor stays low."""60 def __init__(self, size=7):61 self.buckets = [[] for _ in range(size)]62 self.size = size6364 def _h(self, k):65 return hash(k) % self.size6667 def put(self, k, v):68 bucket = self.buckets[self._h(k)]69 for item in bucket:70 if item[0] == k:71 item[1] = v72 return73 bucket.append([k, v])7475 def get(self, k):76 for item in self.buckets[self._h(k)]:77 if item[0] == k:78 return item[1]79 return None80```8182**Goal:** test set membership in O(k) time and O(m) bits with a tunable false-positive rate and zero false negatives (e.g. "is this variant in the known-common set?").83**Approach:** size the bit array `m` and hash-function count `k` from the target false-positive rate `fp` and expected element count `n`, then set/check `k` bit positions derived from two independent hashes (double hashing avoids needing `k` distinct hash functions).8485```python86import math87import hashlib888990class BloomFilter:91 """Probabilistic set membership: no false negatives, tunable false-positive rate."""92 def __init__(self, n, fp=0.01):93 self.m = int(-n * math.log(fp) / math.log(2) ** 2)94 self.k = max(1, int(self.m / n * math.log(2)))95 self.bits = bytearray(self.m)9697 def _hashes(self, item):98 h1 = int(hashlib.md5(str(item).encode()).hexdigest(), 16)99 h2 = int(hashlib.sha1(str(item).encode()).hexdigest(), 16)100 return [(h1 + i * h2) % self.m for i in range(self.k)]101102 def add(self, item):103 for i in self._hashes(item):104 self.bits[i] = 1105106 def __contains__(self, item):107 return all(self.bits[i] for i in self._hashes(item))108```109110**Goal:** keep a binary tree height-balanced (O(log n) guaranteed) after insert, for sorted range queries over genome coordinates.111**Approach:** track a `height` field per node; after insert, walk back up recomputing height and balance factor, applying a single or double rotation the moment |bf| > 1.112113```python114def height(node):115 """Height of a node, treating None as height 0."""116 return node.height if node else 0117118119def rotate_right(z):120 """LL-case fix: promote z.left to root of this subtree."""121 y, z.left = z.left, z.left.right122 y.right = z123 z.height = 1 + max(height(z.left), height(z.right))124 y.height = 1 + max(height(y.left), height(y.right))125 return y126127128def rotate_left(z):129 """RR-case fix: promote z.right to root of this subtree."""130 y, z.right = z.right, z.right.left131 y.left = z132 z.height = 1 + max(height(z.left), height(z.right))133 y.height = 1 + max(height(y.left), height(y.right))134 return y135```136137## Pitfalls138139- **Linked list**: forgetting to update the `tail` pointer on head-insert or delete-last.140- **Stack/queue**: check empty before pop/dequeue, or you'll raise/segfault on the wrong operation.141- **Dynamic array**: `np.append` in a loop is O(n²); pre-allocate a NumPy array when the final size is known.142- **BST**: sorted input yields O(n) height — never use a plain BST on pre-sorted genomic positions without balancing.143- **AVL**: update the child's height before the new parent's after a rotation, or heights go stale.144- **Red-Black**: new nodes are always inserted RED; the root must be recolored BLACK after every fix-up.145- **Hash table (open addressing)**: deletes need tombstones, or later searches stop early at the "empty" slot.146- **Bloom filter**: cannot delete a single item (bit is shared); pick m/n ≥ 10 for <1% false-positive rate; `in` can lie (false positive) but never misses a true member.147148## See Also149150- `algo-hash-tables-bloom` — deep dive on hash table collision strategies and Bloom filter tuning.151- `algo-avl-trees`, `algo-red-black-trees`, `algo-binary-search-trees` — full implementations with insert/delete/rebalance.152- `algo-complexity-analysis` — formal Big-O derivations behind the complexity table above.153- `python-collections-regex` — when `collections.deque`, `heapq`, or `sortedcontainers` already solve the problem instead of hand-rolling a structure.