The base list of data structures: what each stores, what every operation costs, how much memory it takes, and when to reach for it. Algorithms with their time and space are in π¬ Complexity Cheat Sheet; the decision map βquery pattern structureβ is in Containers, Hashing and Choosing a Structure.
Memory figures were measured on 64-bit libstdc++ (GCC) by counting operator new requests for elements; node-based containers pay another 8β16 bytes of malloc bookkeeping per node on top. Costs: = amortized, = expected.
Linear structures
| Structure | Access -th | Push / pop back | Push / pop front | Insert / erase middle | Search | Memory per int | Iterators stable? | Use when |
|---|---|---|---|---|---|---|---|---|
static array, std::array | β | β | , sorted | 4 B | yes | size known at compile time; fastest | ||
vector | / | 4 B (up to 8 B before shrinking: capacity doubles) | no (reallocation) | default container | ||||
deque | (two indirections) | ~4.2 B (512-byte blocks + map) | references yes at the ends, iterators no | both ends, or stable references while pushing | ||||
list (doubly linked) | with an iterator; splice | 24 B + malloc | yes | many erasures/splices at known positions (rare in contests) | ||||
forward_list | β | after an iterator | 16 B + malloc | yes | memory-tight singly linked lists | |||
index-based linked list (prv/nxt arrays) | 8 B | indices never move | βdelete element, find live neighbourβ simulations; below | |||||
stack / queue (adapters on deque) | top / front only | (queue pop) | β | β | as deque | β | LIFO / FIFO; use vector as a stack for speed | |
| ring buffer (circular array) | 4 B, fixed capacity | indices relative to head | BFS queues, sliding windows with a known bound; below | |||||
| monotonic stack / deque | top / ends | β | β | 4 B | β | next greater element, sliding window min/max: Monotonic Stack | ||
| min-stack, min-queue (two stacks) | β | / | β | β | min in | 8 B | β | queue with min/max/gcd of contents; below |
bitset<N> | β | β | β | word-parallel ops | 1 bit | β | boolean DP, sets over a small universe: Bitset | |
vector<bool> | (bit proxy) | β | β | 1 bit | no | large boolean arrays; not a real container of bool | ||
string | find worst | 1 B per char (15 chars inline: SSO) | no | text; hashing and automata in chapter 18 |
Associative structures
| Structure | Insert / erase / find | Ordered queries (lower_bound, min, next) | -th / rank | Memory per int key | Use when |
|---|---|---|---|---|---|
sorted vector + binary search | insert, find | 4 B | static set: build once, query many | ||
set / map (red-black tree) | β ( distance) | 40 B (map<int,int> 40 B, map<ll,ll> 48 B) | dynamic ordered set; default | ||
multiset / multimap | β | 40 B | duplicates; erase one copy with erase(find(x)) | ||
unordered_set / unordered_map (chaining) | , worst | no | no | 27 B (<int,int>), 35 B (<ll,ll>) | exact lookups; add a randomized hash against anti-hash tests |
gp_hash_table (open addressing, pb_ds) | , faster in practice | no | no | ~48 B at default load | hot lookups; PBDS |
pb_ds tree (order statistics) | find_by_order / order_of_key | 48 B | rank queries on a dynamic set | ||
| treap / implicit treap | ~24 B (arrays) | split/merge, sequence operations, reversals: Treaps | |||
| splay tree | ~16β24 B | link-cut trees, access locality | |||
| AVL tree | worst | with sizes | ~24 B | strict height ; rarely hand-written | |
| skip list | with widths | ~2 pointers per key on average | simple concurrent-friendly ordered set; below | ||
| B-tree (order ) | node visits | yes | with counts | compact, keys per node | external memory and cache-friendly ordered sets (databases, file systems) |
| trie | per string | lexicographic order | with counts | one node per distinct prefix | prefixes, autocomplete, XOR queries: Trie, Binary Trie |
| radix tree (compressed trie) | yes | with counts | one node per branching point | long strings with shared prefixes; suffix trees | |
| 64-ary bit tree (βfast setβ) | next / prev | β | words | integer keys in : Fast Set | |
| van Emde Boas tree | β | theory; the 64-ary tree is the practical version | |||
| Chtholly tree (interval map) | amortized by assignments | β | one node per run | random βassign rangeβ operations: Chtholly Tree |
Priority structures
| Structure | Push | Pop min | Decrease key | Meld | Notes |
|---|---|---|---|---|---|
binary heap (priority_queue) | lazy deletion | 4 B per int; default | |||
| -ary heap | is often faster (cache) | ||||
| leftist / skew heap | ( for skew) | β | mergeable heaps in a few lines | ||
| randomized meldable heap | β | shortest mergeable heap code | |||
| pairing heap | fast in practice; pb_ds pairing_heap_tag | ||||
| binomial heap | forest of binomial trees | ||||
| Fibonacci heap | best bounds, poor constants; Heaps | ||||
| radix heap | β | β | monotone integer keys (Dijkstra) | ||
| two heaps (median), heap with global add | β | β | running median, offset tricks |
Range and tree structures (index into chapters 7, 8, 12)
| Structure | Build | Query | Update | Memory | Note |
|---|---|---|---|---|---|
| prefix sums / difference array | range sum | (offline: range add) | Prefix Sums | ||
| sparse table | idempotent (min, gcd) | static | Sparse Table | ||
| Fenwick tree | prefix sums, -th by descent | point | Fenwick Tree | ||
| segment tree | any monoid | point | β | Segment Tree | |
| lazy segment tree | range | Lazy Propagation | |||
| dynamic / persistent segment tree | β | new nodes | Dynamic and Persistent Segment Trees | ||
| segment tree beats | chmin / chmax amortized | Segment Tree Beats | |||
| Li Chao tree | β | min of lines | add line | Li Chao Tree | |
| sqrt decomposition | β | anything with cheap block summaries | |||
| merge sort tree / wavelet tree | / -th, rank | static | / bits | 8. Range Queries | |
| DSU (union by size + path compression) | find | union | DSU; with rollback | ||
| binary lifting / LCA | ( with Euler tour + RMQ) | static | LCA | ||
| heavy-light decomposition | path query | 12. Trees | |||
| link-cut tree / Euler-tour tree | β | path / subtree | link, cut | dynamic forests: Euler Tour Tree | |
| centroid decomposition | path counting per vertex | β | 12. Trees | ||
| KD-tree | 2D orthogonal range worst case; nearest neighbour fast on typical data | insert with periodic rebuilding | KD-Tree | ||
| persistent array / treap / segment tree | β | any version | new nodes | per update | Persistence |
Four small structures worth having
// Doubly linked list over fixed indices 0..n-1 (sentinel n): O(1) erase and neighbour queries, no allocation.
struct IndexList {
int n; vector<int> prv, nxt;
IndexList(int n) : n(n), prv(n + 1), nxt(n + 1) { // starts as 0, 1, ..., n-1
for (int i = 0; i <= n; i++) prv[i] = (i + n) % (n + 1), nxt[i] = (i + 1) % (n + 1);
}
void erase(int x) { nxt[prv[x]] = nxt[x]; prv[nxt[x]] = prv[x]; }
void insertAfter(int p, int x) { prv[x] = p; nxt[x] = nxt[p]; prv[nxt[p]] = x; nxt[p] = x; } // p = n: front
int front() const { return nxt[n]; } // n means "empty" / "end"
int next(int x) const { return nxt[x]; }
int prev(int x) const { return prv[x]; }
};
template<class T> struct RingBuffer { // fixed capacity (power of two), O(1) at both ends
vector<T> a; int head = 0, len = 0, mask;
RingBuffer(int capPow2) : a(capPow2), mask(capPow2 - 1) {}
void push_back(const T& x) { a[(head + len++) & mask] = x; }
void push_front(const T& x) { head = (head - 1) & mask; a[head] = x; len++; }
T pop_front() { T x = a[head]; head = (head + 1) & mask; len--; return x; }
T pop_back() { return a[(head + --len) & mask]; }
T& operator[](int i) { return a[(head + i) & mask]; }
int size() const { return len; }
};
struct MinQueue { // queue with O(1) amortized min
vector<pair<ll, ll>> in, out; // (value, min of this stack up to here)
void push(ll x) { in.push_back({x, in.empty() ? x : min(x, in.back().second)}); }
void pop() {
if (out.empty()) while (!in.empty()) { ll x = in.back().first; in.pop_back(); out.push_back({x, out.empty() ? x : min(x, out.back().second)}); }
out.pop_back();
}
ll getMin() const {
if (in.empty()) return out.back().second;
if (out.empty()) return in.back().second;
return min(in.back().second, out.back().second);
}
int size() const { return sz(in) + sz(out); }
};
struct SkipList { // ordered set of ints > INT_MIN, expected O(log n)
static const int MAXL = 24;
vector<int> key; vector<vector<int>> nxt; int lvl = 1; mt19937 rng{20240601};
SkipList() { key.push_back(INT_MIN); nxt.push_back(vector<int>(MAXL, -1)); } // node 0 = head
void findPrev(int k, int* upd) {
int x = 0;
for (int i = lvl - 1; i >= 0; i--) { while (nxt[x][i] != -1 && key[nxt[x][i]] < k) x = nxt[x][i]; upd[i] = x; }
}
bool insert(int k) {
int upd[MAXL]; findPrev(k, upd); int y = nxt[upd[0]][0];
if (y != -1 && key[y] == k) return false;
int l = 1; while (l < MAXL && (rng() & 1)) l++; // height: geometric with p = 1/2
for (int i = lvl; i < l; i++) upd[i] = 0;
lvl = max(lvl, l);
int id = sz(key); key.push_back(k); nxt.push_back(vector<int>(l));
for (int i = 0; i < l; i++) { nxt[id][i] = nxt[upd[i]][i]; nxt[upd[i]][i] = id; }
return true;
}
bool erase(int k) { // the node's memory is not reused
int upd[MAXL]; findPrev(k, upd); int y = nxt[upd[0]][0];
if (y == -1 || key[y] != k) return false;
for (int i = 0; i < sz(nxt[y]); i++) nxt[upd[i]][i] = nxt[y][i];
return true;
}
optional<int> lowerBound(int k) { int upd[MAXL]; findPrev(k, upd); int y = nxt[upd[0]][0]; if (y == -1) return nullopt; return key[y]; }
};Proof: amortized vector growth and the two-stack queue
Vector. With capacity doubling, the -th reallocation copies elements, and after pushes the copies total . So pushes cost : amortized. (libstdc++ doubles; MSVC grows by , the same argument with ratio gives .) The capacity is between and , hence between 4 and 8 bytes per
int.Two-stack queue. The queue is
out(top = front) followed byinreversed. A pop with emptyoutmoves all ofintoout, reversing the order, so the oldest element becomes the top ofout. Each element is pushed ontoinonce, moved tooutat most once and popped once: amortized per operation. Each stack entry stores the minimum of its stack from the bottom up to it, so the minimum of the queue is the smaller of the two tops.Ring buffer and index list. Positions are taken modulo the capacity, so
head + iaddresses the -th element forlenas long aslenstays at most the capacity. In the index list,eraseandinsertAfterchange exactly the links of and its two neighbours, preserving the invariantnxt[prv[v]] == vfor all live (the sentinel closes the cycle). Tested againststd::listandstd::dequewith random operations.
Proof: hash tables, skip lists and B-trees
Chaining. With buckets, keys and a hash function that sends each key to a uniformly random bucket independently, the expected number of other keys in the bucket of a given key is ; an operation scans one bucket, so it costs . Rehashing to keep doubles the table, amortized by the vector argument. Real hash functions are not random: a fixed
std::hash<int>(the identity) lets an adversary put every key in one bucket, which is why contests need a randomized hash (see the containers note).Skip list. Each node has height with probability , independently. The expected number of nodes at level is , so levels above are empty except with probability , and the expected maximum level is . Search cost, read backwards from the found position: at each step the path either goes up one level (if the current node is tall enough, probability ) or left one node. The expected number of left moves before each up move is at most 1, so climbing levels costs about expected steps, plus expected above level . Insert and erase do one search and pointer updates, expected for the height. Expected memory: pointers per key.
B-tree. Every node except the root has at least keys and children, so level has at least nodes and the tree with height holds keys. Hence : with , a billion keys need at most 5 node reads, which is why disks and databases use B-trees. Tested: the skip list against
std::setwith random operations.