Key-Value Stores §
Key-value stores organize data so that a key maps to a value, supporting fast lookup, insertion, and deletion by key. They are relevant whenever the access pattern is “give me the value for this key” — from an in-memory cache to a distributed database. They build on flat structures (arrays, lists) by adding a mapping layer: a hash function or an ordering that routes the key to the right location.
Authoritative sources
Comparison §
| Structure | Search | Insert | Space | Medium | Primary use case |
|---|---|---|---|---|---|
| Hash table | O(1) expected | O(1) expected | O(n) | In-memory | Caches, dictionaries, dedup |
| Balanced BST | O(log n) | O(log n) | O(n) | In-memory | Ordered iteration, range queries |
| Skip list | O(log n) expected | O(log n) expected | O(n) expected | In-memory | Sorted sets (Redis ZSET), memtables |
| B-tree | O(logm n) I/Os | O(logm n) I/Os | O(n) | Disk | Database indexes (InnoDB, SQLite) |
| LSM-tree | O(log n) with bloom filters | O(1) (memtable) | O(n · L) worst case | Disk + RAM | Write-heavy stores (RocksDB, Cassandra) |
Hash tables §
A hash table maps keys to values via a hash function .
- Lookup / Insert / Delete: expected.
- Load factor: . Performance degrades when (chaining) or (open addressing).
Details
A hash table is an array of slots. The hash function computes the slot index for key . When two keys map to the same slot, a collision occurs and must be resolved.
Step 1 — The hash function.
The hash function compresses an arbitrary key into a small integer. For a table of size , a simple hash is .
Example: insert keys 5, 3, 8, 1.
- → slot 5
- → slot 3
- → slot 1
- → slot 1 (collision with 8!)
Step 2 — Collision resolution.
Two standard strategies:
- Chaining: each slot holds a linked list of all keys that hash to it. Slot 1 now holds [8, 1]. Lookup walks the chain: expected chain length is .
- Open addressing: on collision, probe to the next slot. Linear probing: . Key 1 collides at slot 1, so it probes to slot 2.
Step 3 — Resizing.
When exceeds a threshold (e.g. 0.75), the table is rehashed into an array of size . Every key is reinserted. Amortized cost per insertion remains because the expensive resize is spread over many cheap insertions.
Step 4 — Hash function quality.
A good hash is approximately universal: for any two distinct keys , . In practice, 64-bit hashes (xxHash, MurmurHash, FNV) with a random seed suffice.
Open addressing vs. chaining: open addressing wins on cache locality (single contiguous array) and space (no pointers). Chaining wins on simplicity and handles deletion more gracefully (tombstones in open addressing complicate probing).
Authoritative source: CLRS Ch. 11 (Hash Tables), pp. 257–290.
Example problem
You have a hash table of size using linear probing with . Insert keys 22, 33, 44, 55, 66. Then look up key 33. How many probes are needed?
Solution
- → slot 0
- → slot 0 occupied, probe slot 1 → empty, insert at 1
- → slot 0 occupied, slot 1 occupied, probe slot 2 → insert at 2
- → probe 0, 1, 2 (all occupied), slot 3 → insert at 3
- → probe 0, 1, 2, 3 (all occupied), slot 4 → insert at 4
Lookup 33: → occupied (22), probe 1 → found (33). 2 probes.
Balanced binary search trees §
A binary search tree (BST) stores keys in a binary tree satisfying the BST invariant: for every node, all keys in its left subtree are smaller and all keys in its right subtree are larger. Without balancing, a BST can degenerate to a linked list (). Balanced BSTs enforce structural invariants that guarantee height.
- Search / Insert / Delete: worst case.
- Space: (one node per key, two child pointers + color/balance field).
- Ordered iteration: in-order traversal yields keys in sorted order in .
Details
Step 1 — The BST invariant.
In a BST, for any node with key :
- Every key in ’s left subtree is .
- Every key in ’s right subtree is .
This means a search for key follows a single path: go left if , right if . The search cost equals the depth of the target node.
Example: insert 5, 3, 8, 1, 4 into an empty BST.
Search for 4: 5 → go left (4 < 5) → 3 → go right (4 > 3) → found. 2 comparisons.
Step 2 — Why balance is needed.
If keys arrive in sorted order (1, 2, 3, …, n), the BST becomes a right-leaning chain of height . Search degrades to . A balanced BST enforces a height bound of .
Step 3 — AVL trees (height balance).
An AVL tree (Adelson-Velskii–Landis, 1962) requires that for every node, the heights of the left and right subtrees differ by at most 1. After an insertion or deletion, the tree is rebalanced with at most two rotations (left, right, left-right, right-left) along the path to the root.
Example: insert 1, 2, 3 into an AVL tree. After inserting 3, node 1 has a right-heavy subtree (height 2 vs. 0). A left rotation at node 1 makes node 2 the root, restoring balance.
Step 4 — Red-black trees (color invariants).
A red-black tree (Guibas & Sedgewick, 1978) colors each node red or black and enforces:
- Every node is red or black.
- The root is black.
- Every leaf (NIL) is black.
- A red node has only black children.
- Every path from a node to its descendant leaves contains the same number of black nodes.
Rule 5 bounds the height: the longest path (alternating red-black) is at most twice the shortest (all black), so height .
Example: insert 5, 3, 8, 1, 4. After inserting 4 (red), its parent 3 is also red — violating rule 4. A left rotation at 3 and a color swap restores the invariants.
Step 5 — Comparison and use.
| Property | AVL | Red-black |
|---|---|---|
| Height bound | ≤ 1.44 log₂(n+2) | ≤ 2 log₂(n+1) |
| Rotations per insert | ≤ 2 | ≤ 3 |
| Use | When search is dominant | When insert/delete are frequent |
Red-black trees are the basis of C++ std::map, Java TreeMap, and most OS schedulers. AVL trees appear in database indexes where search is more common than update.
Authoritative sources: CLRS Ch. 12 (Elementary BSTs) and Ch. 13 (Red-Black Trees). Adelson-Velskii & Landis, “An algorithm for the organization of information” (1962).
Example problem
You insert the keys 10, 20, 30, 40, 50 into an AVL tree. After each insertion, the tree is rebalanced. What is the height of the tree after all five insertions?
Solution
- Insert 10: tree = [10], height 0.
- Insert 20: 10 → right 20. Height 1. Balanced.
- Insert 30: 10 → right 20 → right 30. Node 10 has right height 2, left height 0. Imbalance! Left rotation at 10: 20 becomes root, 10 left, 30 right. Height 1.
- Insert 40: 20 → left 10, right 30 → right 40. Node 20: left height 1, right height 2. Balanced (diff ≤ 1). Height 2.
- Insert 50: 30 → right 40 → right 50. Node 30: right height 2, left height 0. Imbalance! Left rotation at 30: 40 becomes child of 20’s right, 30 left of 40, 50 right of 40. Height 2.
Final height: 2 (3 levels). The tree is balanced.
Skip lists §
A skip list is a probabilistic balanced search structure: each element has a random number of “levels” of forward pointers, creating a multi-level linked list that skips over elements at higher levels.
- Search / Insert / Delete: expected.
- Space: expected (geometric distribution of levels).
- Simpler to implement than AVL or red-black trees (no rotations; only pointer manipulation).
Details
Step 1 — The base level.
Level 0 is a sorted linked list. Search is : walk forward until you find the key.
Step 2 — Adding levels.
Each new element gets a level by “flipping a coin” until it lands: level 1 with probability 1/2, level 2 with probability 1/4, etc. The expected number of levels per element is 2, so total space is . Higher levels act as express lanes: a search starts at the top level and drops down when it would overshoot.
Example: insert keys 3, 7, 12, 1, 9. Suppose the coin flips give levels: 3→2, 7→1, 12→3, 1→1, 9→2.
Step 3 — Search algorithm.
Start at the top-left. At each node, if the forward pointer’s key is target, move forward; otherwise drop down one level. This is analogous to binary search but on a linked structure.
Example: search for 9 in the diagram above. Start at L3 (key 12): 12 > 9, drop to L2. At L2, key 3: 3 < 9, move forward. Key 9: found! Total: 1 drop + 2 forward moves = 3 steps.
Step 4 — Why use them?
Redis uses skip lists for sorted sets (ZSET). LevelDB uses them for the in-memory memtable. They are easier to implement correctly than balanced BSTs (no rotations, only pointer splicing) and perform comparably in practice.
Range queries: a skip list supports range queries (find start, then walk forward at level 0), which is better than a hash table () and comparable to a BST.
Authoritative source: Pugh, “Skip Lists: A Probabilistic Alternative to Balanced Trees”, CACM 33(10), 1990.
Example problem
A skip list uses probability for promoting to the next level. You insert 1000 elements. What is the expected total number of forward pointers across all levels?
Solution
Each element has level with for . The expected level is . Each element at level has forward pointers. So the expected total pointers is .
B-trees §
A B-tree of order is an -ary search tree where:
-
Every internal node except the root has at least children; the root has at least 2 children unless it is a leaf.
-
Every node has at most children.
-
All leaves are at the same depth.
-
A node with children stores keys.
-
Search: disk accesses (each node = one disk block).
-
Insert / Delete: with possible splits / merges.
Details
Step 1 — Why B-trees exist.
A binary tree of keys has height . For , that’s 30 levels. If each level is a separate disk block, a search costs 30 random I/Os. A B-tree of order has height , so only 3 disk accesses. The fan-out (branching factor) is the key: more keys per node → fewer levels.
Step 2 — The structure.
A B-tree node stores up to keys and child pointers. Keys within a node are sorted. For a search target , the node determines which child to follow: the child between the two keys that bracket .
Example: a B-tree of order 4 (max 3 keys per node). Root: [10, 20, 30]. Children: [1,5,8], [12,15,18], [22,25,28], [35,40,45].
Step 3 — Insertion and splitting.
If a node overflows ( keys), it splits into two nodes and the median key is pushed up to the parent. If the root splits, the tree grows by one level.
Example: order 4, leaf [12, 15, 18] receives key 16 → [12, 15, 16, 18] (4 keys > 3 max). Split: left [12, 15], right [18], push 16 up to root. Root becomes [10, 16, 20, 30] — now 4 keys > 3 max, so root also splits.
Step 4 — Deletion and merging.
If a node underflows ( keys), it merges with a sibling or borrows from one. If the root becomes empty, the tree shrinks by one level.
Step 5 — Capacity.
With (a 4 KB block holding 256 16-byte keys), a B-tree of 3 levels holds million keys in 3 disk accesses. A B-tree of 4 levels holds billion keys in 4 disk accesses.
B+ trees (used in MySQL InnoDB, PostgreSQL, SQLite): all data is in the leaves; internal nodes store only keys. Leaves are linked for efficient range scans. This is the standard index structure in relational databases (see Database Theory).
Authoritative source: CLRS Ch. 18 (B-Trees), pp. 431–452. Bayer & McCreight, “Organization and Maintenance of Large Ordered Indices” (1972).
Example problem
A B-tree of order 5 (max 4 keys per node) has a leaf [10, 20, 30, 40]. You insert key 25. Describe the split.
Solution
After inserting 25: [10, 20, 25, 30, 40] — 5 keys, exceeds max of 4. Split at the median (index 2): left = [10, 20, 25], right = [30, 40]. Push median key 25 up to the parent. The parent gains one key and one child pointer.
LSM-trees §
A Log-Structured Merge-tree optimizes for write throughput by converting random writes into sequential appends:
- New writes go to an in-memory memtable (a sorted structure, often a skip list).
- When the memtable is full, it is flushed to an immutable SST (Sorted String Table) on disk.
- Background compaction merges overlapping SSTs, discarding tombstones and expired entries.
- Write: (append to memtable) → (flush, amortized).
- Read: check memtable, then SSTs in order. Worst case where is the number of levels. With per-SST bloom filters, the expected read is closer to because most levels are skipped (a negative lookup is rejected by the filter with high probability).
- Space amplification: up to copies of a key during compaction.
Details
Step 1 — The write path.
Every write is an append to the in-memory memtable (a skip list or a sorted array). This is in the memtable but the memtable is small (tens of MB), so it fits in L2/L3 cache. No random disk I/O occurs on the write path.
Example: 100K writes/sec × 100 bytes/write = 10 MB/s. A 64 MB memtable fills in ~6 seconds. Each flush is a single sequential write of 64 MB to disk.
Step 2 — The flush.
When the memtable reaches its size limit, it is sorted (if not already) and written as an immutable SST file. The SST is a sorted run of (key, value) pairs with an index for binary search. A new empty memtable is activated.
Step 3 — Compaction.
Over time, many SSTs accumulate. Compaction merges overlapping SSTs into fewer, larger ones, discarding tombstones (deleted keys) and expired entries.
Step 4 — Compaction strategies.
- Size-tiered: merge SSTs of similar size. Good write throughput, high space amplification.
- Leveled (LevelDB, RocksDB): each level is larger than the previous. Better read performance, more write amplification.
- Universal (Cassandra): merge a set of SSTs when their total size exceeds a threshold.
Step 5 — Bloom filters on the read path.
Each SST carries a Bloom filter (a probabilistic set). On a read, the filter is checked first: if the key is definitely absent, the SST is skipped without a disk read. With a false-positive rate of 1%, a read that would check levels only actually reads SST on average, bringing expected read cost close to .`}}
Use cases: RocksDB, Cassandra, HBase, LevelDB, ScyllaDB. Ideal for write-heavy workloads (time-series data, event logs, IoT).
Authoritative source: O’Neil, O’Neil, Weikum, “The Log-Structured Merge-Tree (LSM-Tree)” (1996). Also: Google LevelDB design doc (2011).
Example problem
An LSM-tree has 5 levels (L0–L4) with a size ratio of 10×. The total dataset is 100 GB. A read for a key that is not present must check the memtable and then each level. Without bloom filters, how many SSTs must be searched? With bloom filters (1% false positive), how many on average?
Solution
Level sizes: L0 = 1 GB, L1 = 10 GB, L2 = 100 GB (but capped at 100 GB total, so let’s say L0=1, L1=5, L2=10, L3=30, L4=54 GB). Each level has multiple SSTs. Without bloom filters, the read must binary-search every SST in every level: potentially SSTs (one per level in the best case, more in L0). With bloom filters at 1% false positive, each level is skipped with 99% probability. Expected SSTs actually read: plus the memtable check. In practice, the key is found (or confirmed absent) after checking the memtable and ~1 SST.
Example problem §
Example problem
Choose between a B-tree and an LSM-tree for a write-heavy workload with 10M keys and 100K writes/sec on a 4 KB-block SSD. Justify.
Solution
B-tree: each write may touch 3–4 nodes (leaf + internal levels). With 100K writes/sec, that’s 300–400K random 4 KB I/Os/sec. An SSD sustains ~50K random IOPS, so the B-tree is I/O-bound and will queue writes.
LSM-tree: writes go to the memtable (). When full (say 64 MB), a single sequential flush of 64 MB occurs. At 100K writes/sec × 100 bytes/write = 10 MB/s, a flush happens every ~6 seconds. Compaction is sequential and can be throttled. The write path is never random.
Verdict: LSM-tree. The write path is sequential (append to memtable, sequential flush), avoiding the random I/O bottleneck of B-tree updates. The cost is higher read latency (check memtable + up to SSTs) and space amplification during compaction — acceptable for a write-heavy workload.