Choosing a Data Structure
Every system that stores and retrieves data eventually confronts the same question: which data structure should hold this collection? A ride-sharing dispatcher needs the nearest driver in milliseconds. A database index needs range queries over billions of rows. A network router needs the longest-prefix match on every packet. Each of these is, underneath, the same abstract decision. It means matching a workload's operation profile to a structure whose complexity guarantees fit that profile. Get it wrong and a system that should run in microseconds crawls in milliseconds. Or worse, it degrades catastrophically under adversarial input. This decision is harder than memorizing "hash tables are O(1)" because real workloads mix operations. A cache needs fast lookup and fast eviction of the least-recently-used item. A scheduler needs fast insertion and fast extraction of the minimum. A spell-checker needs fast membership testing. It also needs compact storage of a huge dictionary with shared prefixes. No single structure dominates on every axis. The choice is a multi-dimensional tradeoff. It weighs average-case speed, worst-case guarantees, ordering support, memory overhead, and resilience to adversarial or skewed input. The big picture is that data structures are not isolated tools. They should not be picked from a menu by name recognition. Instead, they are points in a design space. That space is defined by the operations a workload demands. It is also defined by the guarantees it can tolerate. Red-Black Trees trade a constant factor of speed for guaranteed logarithmic worst case and sorted order. Hash tables trade worst-case guarantees for blazing average-case speed. Skip Lists trade determinism for simplicity and concurrency-friendliness. Priority Queues specialize entirely around one operation: extract-the-extreme-element, fast. This article builds a systematic framework for making this choice. It enumerates the operations that matter. It tabulates the complexity of the major linear, hash-based, and tree-based structures against them. It walks through the reasoning process on realistic scenarios. Finally, it highlights the traps that turn a "reasonable-looking" choice into a production incident.
- Data structure choice is a matching problem: list the operations your workload actually needs, then pick the structure whose guarantees fit — not the most familiar name.
- Arrays and linked lists are the baseline: O(1) index vs O(1) splice, and neither gives you both cheaply.
- Hash tables win on average-case O(1) lookup/insert/delete but sacrifice ordering and worst-case guarantees.
- Balanced BSTs (e.g. red-black trees) give O(log n) worst-case for everything, including range queries and order statistics, at a constant-factor cost over hashing.
- Heaps (binary or otherwise) specialize in O(log n) insert / O(log n) extract-min, beating balanced trees when you never need arbitrary search.
- Skip lists trade a randomized guarantee for radically simpler code, and shine in concurrent and range-query-heavy settings.
Choosing a data structure is choosing which operations to make fast, in the worst case or the average case, at the price of which others.
The idea
An abstract data type (ADT) specifies a contract — the operations a client can invoke (insert, delete, search, min, successor, union) — without prescribing an implementation. A data structure is a concrete realization of that contract, with specific complexity guarantees attached to each operation. The central insight of this article is that no data structure is best; each is Pareto-optimal for some slice of the operation-frequency space, and the engineering task is to identify which slice your workload occupies. Formally, think of a workload as a vector of operation frequencies: , together with a distribution over keys (uniform random, adversarial, skewed/Zipfian, already sorted, streaming). The expected cost of running the workload on structure is
where is the structure's cost (worst-case or expected, depending on what you need to guarantee) for that operation. Choosing a data structure is choosing to minimize this cost subject to the guarantees your system must uphold — e.g., a real-time system cannot accept "amortized ," it needs worst-case per-operation bounds. Three families dominate practical use, and each buys speed by sacrificing something else. Sequential structures (arrays, linked lists, dynamic arrays) are simple and cache-friendly but support at most one of {fast random access, fast insertion/deletion} well. Hash-based structures flatten the key space via a hash function to get expected search/insert/delete, but abandon order — there is no efficient "find the next larger key" or "list all keys in [a, b]." Comparison-based tree structures (balanced BSTs, heaps, skip lists) keep keys ordered, enabling range queries, predecessor/successor, and order statistics, at the cost of an extra factor and pointer-chasing overhead. A fourth family, specialized structures (tries, Bloom filters, disjoint-set forests, segment/Fenwick trees), abandons generality entirely to dominate one narrow operation profile.
How it works
The decision procedure is a sequence of filtering questions, each of which eliminates whole families of structures. This is not a flowchart to memorize blindly — it is a reconstruction of the reasoning an experienced engineer runs mentally in seconds.
Step 1 — Enumerate required operations. List every operation the workload performs, not just the one that comes to mind first. A "dictionary" that occasionally needs "give me the 10 smallest keys" is not a pure dictionary — it needs order statistics.
Step 2 — Determine required guarantee strength. Decide, per operation, whether average-case, amortized, or hard worst-case bounds are required. Real-time systems, latency-SLA services, and adversarial-input-facing systems (e.g., public web servers) must assume worst case; batch analytics jobs can usually accept amortized or expected bounds.
Step 3 — Check for ordering requirements. If you ever need min/max, predecessor/successor, range queries, or sorted iteration, hash tables are disqualified outright — you need a tree-based or array-based ordered structure.
Step 4 — Check key structure. If keys are strings sharing long common prefixes (URLs, file paths, dictionary words), a trie or radix tree may dominate both hashing and comparison trees by exploiting shared structure instead of paying comparisons or hashing the whole key.
Step 5 — Check concurrency and memory constraints. Balanced BST rebalancing (rotations) is notoriously hard to parallelize correctly; skip lists admit much simpler lock-free concurrent implementations. Cache-conscious systems prefer structures with better locality (arrays, B-trees) over pointer-heavy ones (linked lists, red-black trees) even at equal asymptotic complexity.
Step 6 — Compute expected cost and select. Combine the operation frequencies with the surviving candidates' complexities and pick the minimizer, breaking ties by implementation simplicity and constant factors. ```pseudocode function ChooseStructure(operations, guaranteeLevel, keyType, concurrency, memoryBudget): candidates := { Array, LinkedList, DynamicArray, HashTable, BalancedBST, Heap, SkipList, Trie, BloomFilter }
# Step 3: ordering requirement if RequiresOrderOrRangeQuery(operations): candidates := candidates \ { HashTable, BloomFilter }
# Step 2: worst-case guarantee requirement if guaranteeLevel == WORST_CASE: candidates := candidates \ { HashTable } # expected O(1), not worst-case
# (unless using a worst-case-safe scheme, e.g. cuckoo hashing / dynamic perfect hashing)
# Step 4: key structure exploitation if keyType == STRING_WITH_SHARED_PREFIXES and IsMember(RangeOrPrefixQuery, operations): candidates := candidates ∪ { Trie } # add, don't just filter preferredFamily := Trie
# Step 1: single dominant operation -> specialize if OnlyOperationsAre(operations, {INSERT, EXTRACT_MIN}): return Heap if OnlyOperationsAre(operations, {SEARCH}) and ApproximateOK(operations): return BloomFilter # only if false positives tolerable
# Step 5: concurrency if concurrency == HIGH and NeedsOrder(operations): candidates := PreferIfPresent(candidates, SkipList)
# Step 6: minimize expected cost over remaining candidates best := argmin_{S in candidates} ExpectedCost(S, operations) return best ``` The pseudocode is deliberately a decision procedure over structures, not an algorithm operating on data — the object being manipulated is the design space itself. Each filtering step corresponds to a hard constraint (worst-case safety, ordering) or a dominant-strategy shortcut (single-operation specialization), and the final step is the soft optimization once hard constraints have pruned the space.
Complexity & correctness
The table below is the reference every step of the decision procedure ultimately consults. All logarithms are base 2; is the number of elements currently stored.
| Structure | Search | Insert | Delete | Min/Max | Predecessor/Successor | Range query (k results) | Space |
|---|---|---|---|---|---|---|---|
| Unsorted array | |||||||
| Sorted array | |||||||
| Linked list | |||||||
| Hash table (open addressing) | expected | expected | expected | ||||
| Balanced BST (red-black) | w.c. | w.c. | w.c. | ||||
| Binary heap | (extract-ext.) | peek | |||||
| Skip list | expected | expected | expected | expected | expected | ||
| Trie (string keys, length ) | — | w.c. |
assumes a pointer/handle to the node is already held; deletion by key value still costs to locate it.
Why hash tables are only expected-case, not worst-case. With open addressing (or chaining), a hash function maps keys from a universe into . For any fixed , an adversary who knows can choose keys that all collide, forcing probes on every operation — this is the basis of algorithmic-complexity-attack denial of service. Average-case is provable under simple uniform hashing (each key equally likely to hash anywhere) or via universal hashing, where is drawn randomly from a family such that for . Under universal hashing, the expected chain length (or expected probe count under open addressing with load factor ) is , giving expected when — but the worst case over all inputs remains , only now the adversary must guess the random seed rather than the fixed function.
Why balanced BSTs give worst-case . A red-black tree maintains the invariant that every root-to-leaf path has the same number of black nodes, and no red node has a red child, which together force the longest path to be at most twice the shortest, so height . Rotation and recoloring after insert/delete restore this invariant in amortized rotations and recolorings, all along the search path — giving worst case for every dictionary operation, with no dependence on key distribution or adversarial input, unlike hashing.
Why heaps beat balanced BSTs for pure priority-queue workloads. A binary heap is an implicit, array-packed complete binary tree, needing no parent/child pointers (child of index is at ), so it has better cache locality and lower constant factors than a pointer-based red-black tree, while both give insert/extract-min. The heap sacrifices arbitrary search (finding an arbitrary key requires , since sibling subtrees have no ordering relation to each other) — a fine trade if the workload never searches for arbitrary keys, only ever asks for the current minimum.
Why tries win on shared-prefix key sets. A trie's cost depends on key length , not on the total number of stored keys , because each character selects a child pointer directly rather than requiring a comparison against other stored keys. When (short fixed-width keys among a huge key set — IP prefixes, DNA k-mers), tries strictly dominate comparison-based trees; when keys are long and mostly unique, the worst-case space cost (unless compressed via a radix tree / Patricia trie) can lose to a hash table.
The honest tradeoff summary. There is a strict impossibility here, not just a folklore rule: a comparison-based structure cannot support search in better than worst case, because a decision tree distinguishing outcomes needs depth . Hashing escapes this bound only by giving up determinism and giving up order — you cannot have expected- search and worst-case- search and sorted iteration in one structure; every real system's choice is a projection onto two of these three axes.
Worked example
Consider designing the backend for an autocomplete/typeahead service: given a prefix typed so far, return the 10 most popular completions, and support insertion of new terms with frequency updates, at production request-latency SLAs (worst-case bounded per request).
Step 1 (enumerate operations). `prefixSearch(p) → top-10 by frequency`, `insert/update(term, freq)`, no arbitrary-order iteration needed, no full-range scan.
Step 2 (guarantee level). Latency SLA ⇒ we cannot tolerate a hash table's rare worst case on a hot path; but we also cannot afford scans per keystroke at scale.
Step 3 (ordering). Not a total order over all keys — but a prefix structure, which disqualifies hash tables and plain balanced BSTs outright, since neither exploits shared prefixes; both would need an or search per exact key, but a full range scan of "everything starting with p" in a BST costs only if we can identify the subtree efficiently, which a BST keyed on whole strings can do lexicographically — a workable but not optimal fit.
Step 4 (key structure). Terms share long common prefixes by construction — this is exactly the trie's specialty. Decision: use a trie, augmenting each node with a small auxiliary max-heap (or a cached top-10 list) of the best-scoring completions in its subtree, updated incrementally on insert. Trace on a tiny dictionary with typed prefix `"ca"`:
| Term | Frequency |
|---|---|
| cat | 120 |
| car | 95 |
| cab | 40 |
| cap | 15 |
| dog | 200 |
Trie structure (only the relevant subtree shown): root → `c` → `a` → {`t`(120), `r`(95), `b`(40), `p`(15)}; and root → `d` → `o` → `g`(200). 1. Walk root → `c` → `a`: where , independent of or . 2. At the node for prefix `"ca"`, read the pre-aggregated top- list stored at that node (maintained by a small bounded max-heap of size 10, updated in per insertion that touches this node). 3. Return `[cat:120, car:95, cab:40, cap:15]` — sorted, in or already sorted if the auxiliary structure keeps it sorted. Total query cost: , completely independent of dictionary size — exactly the guarantee the SLA needs, and something no plain hash table or balanced BST offers directly, because both would require either scanning all keys with a prefix test (hash table has no locality among keys sharing a prefix) or walking a lexicographic BST range that could touch irrelevant nodes if the prefix is short and popular. Insertion of a new term costs trie-node creations/updates plus per ancestor node for the auxiliary heap update — still independent of . Contrast with the "obvious" first guess of a hash table on full terms plus a linear scan filtered by prefix: correct results, but per query — a service with a million terms would do a million string-prefix comparisons per keystroke, violating the latency SLA the moment the dictionary grows past a few thousand entries. The framework's Step 4 filter (shared-prefix key structure) is precisely what catches this before it becomes a production incident.
Common pitfalls & traps
| Pitfall | Why it bites | Fix |
|---|---|---|
| Choosing a hash table when range queries or "next larger key" will eventually be needed | Hash tables have no notion of order between buckets; adding sorted iteration later requires a rewrite, not a patch | Ask explicitly about future range/order needs before committing, not just current ones |
| Assuming hash table is a worst-case guarantee | Adversarial or pathological input (hash flooding) can force per op; public-facing services have been DoS'd this way | Use a keyed/seeded universal hash family, or switch to a worst-case-safe structure (balanced BST, cuckoo hashing) on adversarial-input paths |
| Using a balanced BST purely as a priority queue | Correct but pays extra constant-factor overhead (pointers, rotations) for guarantees (arbitrary search, ordering) the workload never uses | Use a binary or d-ary heap when the only order-sensitive operations are insert/extract-min |
| Ignoring load factor when picking open addressing vs. chaining | Open addressing performance degrades sharply as (clustering); chaining degrades more gracefully but costs extra pointers | Track load factor explicitly; resize/rehash well before approaches 1, per Open Addressing Collision Resolution |
| Treating amortized bounds as if they were per-operation worst-case bounds | Dynamic array `push_back` is amortized but a single resizing call is — unacceptable inside a hard real-time deadline | For hard real-time systems, require true worst-case per-op bounds (e.g., pre-sized arrays, red-black trees) not amortized ones |
| Picking a trie for long, mostly-unique string keys | Trie space cost can balloon to with poor prefix sharing, losing to a hash table both in time and memory | Use a trie only when prefix sharing is substantial; otherwise a hash table or compressed (radix/Patricia) trie |
Pioneers & history
The decision framework itself synthesizes results developed across three overlapping traditions: balanced search trees, hashing theory, and heap-based priority queues.
Origins · 1 minRudolf Bayer and how this was discovered
Bayer's symmetric binary B-tree (1972, with Edward McCreight and later formalized as the red-black tree by Leonidas Guibas and Robert Sedgewick in 1978) established that logarithmic worst-case dictionary operations were achievable without amortization tricks, giving systems designers a hard guarantee independent of insertion order — the bedrock of the "worst-case tree" branch of the decision framework.
Where this matters
Database engines choose B-trees (a wide-fanout generalization of balanced BSTs) for on-disk indexes because they minimize the number of disk-block reads for range queries — exactly the "ordering requirement" filter in Step 3, weighted by an I/O-cost model rather than a comparison-cost model. In-memory caches such as Redis or Memcached lean on hash tables for O(1) key-value lookup, but layer an LRU/LFU eviction structure (often a linked list plus hash table, or an approximate structure) on top precisely because the pure hash table cannot answer "which item is least recently used" — a second operation profile requiring a second structure. Network routers perform longest-prefix matching using tries (specifically Patricia tries or multi-bit tries) because IP prefixes are exactly the shared-prefix key structure Step 4 identifies. Operating system schedulers use heaps or heap-like structures (e.g., the Completely Fair Scheduler's red-black tree in Linux, chosen for its ordering and worst-case bounds together, since scheduling needs both "who runs next" and "insert a newly-woken task") to pick the next runnable task in logarithmic time. Compilers and interpreters use hash tables for symbol tables where insertion order and range queries are irrelevant, and disjoint-set forests for register allocation and type-inference union-find problems where the operation profile is exclusively union/find, not covered by any of the general families above. Every one of these is the same underlying exercise: enumerate operations, filter by guarantee strength and ordering needs, and let the residual structure fall out of the workload rather than out of habit.
Ask the tutor: How would this decision framework change if the workload were mostly writes streamed once and read many times later (a write-once, read-many analytics log)?
Is a hash table always faster than a balanced BST?
On average, yes, for pure search/insert/delete with no ordering needs — hash tables achieve expected O(1) versus a BST's O(log n), and the constant factor is usually smaller since there is no rebalancing overhead. But "faster" here is an average-case, non-adversarial claim. If the workload needs sorted iteration, range queries, or worst-case latency guarantees (e.g., in a system exposed to untrusted input), the balanced BST wins outright because the hash table simply cannot supply those guarantees at any speed. The right question is never "which is faster" in isolation, but "faster at doing what, under what guarantee."
When should I reach for a skip list instead of a red-black tree?
Skip lists are attractive when implementation simplicity and concurrency matter more than squeezing out the last constant factor. Balancing a red-black tree after insert/delete requires careful rotation and recoloring logic that is notoriously easy to get subtly wrong, and hard to parallelize safely because rotations can touch nodes far from the insertion point. A skip list rebalances probabilistically — each new node's height is chosen by coin flips — so insertion and deletion touch only a local, expected O(log n) set of nodes, with no global rotation logic, making lock-free concurrent implementations considerably more tractable. See Skip Lists for the mechanics of the coin-flip height assignment.
Why isn't there one data structure that is best at everything?
Because provable lower bounds forbid it. Comparison-based structures cannot beat Ω(log n) worst-case search (an information-theoretic argument from decision-tree depth), while hash-based structures achieve O(1) only by discarding order and accepting adversarial risk. You cannot simultaneously have O(1) worst-case search, full sorted-order support, and adversarial-input safety in one structure — these three properties are provably in tension, not merely conventionally so. Every real structure is a specific, deliberate projection onto a subset of these axes.
How do amortized and worst-case guarantees differ in practice, and why does it matter for this decision?
An amortized bound guarantees that the total cost of a sequence of n operations is bounded (e.g., O(n) total, or O(1) per operation "on average over the sequence"), but any single operation in that sequence may be expensive — a dynamic array's resizing push_back is O(1) amortized but O(n) on the rare resize. A worst-case bound guarantees every single operation individually meets the bound. Systems with hard real-time deadlines (flight control, high-frequency trading matching engines) cannot tolerate an occasional O(n) spike even if it "averages out," so they require true worst-case structures like balanced BSTs or pre-allocated arrays, not amortized ones.
Where do Bloom filters fit into this framework?
Bloom filters answer only approximate membership queries — "definitely not present" or "possibly present" — using a bit array and multiple hash functions, at O(1) time and remarkably small space (independent of key size), but they never support deletion (in the basic form) or retrieval of the actual stored value, and they admit false positives. They belong to Step 1's "single dominant operation" shortcut: if the only operation you need is an approximate pre-filter (e.g., "should I even bother querying the disk-backed database for this key?"), a Bloom filter dominates every other structure on memory, at the cost of a tunable false-positive rate and no support for enumeration or exact retrieval.
Does this framework apply to persistent/on-disk structures the same way?
The same operation-profile-matching logic applies, but the cost model changes: instead of counting comparisons or hash computations, you count disk block reads, because disk I/O is orders of magnitude slower than in-memory operations. This is why databases favor B-trees (wide-fanout generalizations of balanced BSTs) over red-black trees or hash tables for on-disk indexes — a B-tree's larger branching factor means far fewer block reads per search, even though the asymptotic complexity is still O(log n), just with a much smaller effective base for the logarithm relative to block size.
If I'm unsure which structure to pick, is it safe to default to a balanced BST?
It is a reasonably safe default when guarantees matter and you are unsure of the future operation mix, because a balanced BST like a red-black tree supports search, insert, delete, min/max, predecessor/successor, and range queries, all in worst-case O(log n), with no dependence on key distribution. You pay a constant-factor tax versus a specialized structure (a heap for pure priority-queue use, a hash table for pure lookup), but you avoid the correctness and security risks of a mismatched choice, and you retain flexibility if requirements grow — a genuinely useful "when in doubt" fallback, though never a substitute for actually running through the decision steps.