Array
A contiguous block of memory holding elements of the same type, indexed from to . Because the address of any element can be computed directly from its index (), access is — but inserting or deleting anywhere except the end requires shifting every element after it. A dynamic array (std::vector) wraps a fixed-size array and reallocates to double its capacity whenever it fills up, which is why push_back is amortized rather than worst-case.
| Operation | Complexity |
|---|---|
| Access by index | |
| Search (unsorted) | |
| Search (sorted, binary search) | |
| Insert/delete at end | amortized |
| Insert/delete at arbitrary index |
Arrays are the backbone of prefix sums, sliding-window techniques, and most indexed DP tables, since contiguous memory also makes them by far the most cache-friendly structure available.
vector<int> a = {5, 3, 8, 1};
a.push_back(9); // O(1) amortized
a.insert(a.begin() + 1, 4); // O(n): shifts everything after index 1
int x = a[2]; // O(1)Space Complexity:
Linked List
A chain of nodes, each storing a value and a pointer to the next node (singly linked), or to both the next and previous nodes (doubly linked). Unlike an array, memory isn’t contiguous, so there’s no random access — reaching the -th element means walking pointers from the head, . The payoff is that once you’re already at a node, insertion and deletion there is , since it’s just pointer rewiring rather than shifting elements. Doubly linked lists cost an extra pointer per node but allow deletion given only a pointer to the node itself (no need to track its predecessor) and traversal in either direction; a circular variant links the tail back to the head, useful for round-robin scheduling.
| Operation | Singly | Doubly |
|---|---|---|
| Access / search | ||
| Insert/delete at known node | ||
| Insert/delete at head | ||
| Delete given only the node (no predecessor) | requires predecessor scan, |
Doubly linked lists paired with a hash table are the classic backbone of an LRU cache — the list keeps eviction order while the hash table gives lookup of any node.
struct Node {
int val;
Node *next, *prev;
Node(int v) : val(v), next(nullptr), prev(nullptr) {}
};
void insertAfter(Node *node, int v) {
Node *fresh = new Node(v);
fresh->next = node->next;
fresh->prev = node;
if (node->next) node->next->prev = fresh;
node->next = fresh;
}Space Complexity: , plus one (singly) or two (doubly) pointers of overhead per node
Stack
A LIFO (last-in, first-out) structure exposing only push, pop, and top, all — the trade-off for that simplicity is that only the most recently inserted element is ever reachable. It can be built on top of either an array (fast, cache-friendly, occasional resize) or a linked list (no resize, one extra pointer per element); std::stack uses a std::deque as its underlying container by default.
Stacks naturally model any process with a “most recent unmatched thing” — matching brackets, undo history, and the explicit call stack used to convert recursion into iteration.
stack<int> st;
st.push(10);
st.push(20);
st.top(); // 20
st.pop(); // removes 20Time Complexity: for push, pop, top
Space Complexity:
Queue & Deque
A queue is FIFO (first-in, first-out): elements are pushed at the back and popped from the front, both when backed by a circular buffer or linked list (a plain array would need pops from the front due to shifting). A deque (double-ended queue) generalizes this to push/pop at both ends; std::deque implements it as a sequence of fixed-size blocks with an index, giving random access as well — strictly more powerful than a queue, which is why std::queue and std::stack both use std::deque as their default underlying container.
| Operation | Queue | Deque |
|---|---|---|
| Push/pop front | ||
| Push/pop back | — | |
| Random access | — |
Queues drive any breadth-first traversal, while deques are the standard tool behind the “monotonic deque” technique for sliding-window minimum/maximum.
deque<int> dq;
dq.push_back(1);
dq.push_front(2);
dq.pop_back();
dq.pop_front();Space Complexity:
Hash Table
Maps keys to values using a hash function that converts a key into a bucket index, giving average-case insert, find, and erase — regardless of how large the table grows. Collisions (two keys hashing to the same bucket) are resolved either by chaining (each bucket holds a small list) or open addressing (probe for the next free slot); std::unordered_map / std::unordered_set use chaining internally. The average-case guarantee assumes a reasonably random hash — with a known, fixed hash function (as in most standard libraries), an adversary who knows the judge’s hash implementation can craft inputs that collide into a single bucket, degrading every operation to (an “anti-hash test”). The standard defense in competitive programming is to supply a custom hash that mixes in something unpredictable, such as chrono::steady_clock:
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t seed = chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + seed);
}
};
unordered_map<long long, int, custom_hash> safe_map;Time Complexity: average, worst case per operation
Space Complexity:
Ordered Map / Set
std::map and std::set are implemented as a self-balancing binary search tree (a red-black tree), which keeps keys in sorted order at the cost of for every insert, find, and erase — slower than a hash table’s average case, but with two abilities a hash table doesn’t have: iterating keys in sorted order, and answering “closest key” queries via lower_bound / upper_bound in .
| Operation | Complexity |
|---|---|
| Insert / erase / find | |
lower_bound / upper_bound | |
| In-order traversal | total |
Common uses: coordinate compression, maintaining a sorted multiset of “active” values (e.g. the current window in a sliding-window problem), and — via the GNU policy-tree extension __gnu_pbds::tree (an “order-statistics tree”) — answering “how many elements are less than ” or “what’s the -th smallest element” in , which plain std::set cannot do.
map<int, int> freq;
freq[5]++;
auto it = freq.lower_bound(3); // first key >= 3Space Complexity:
Heap / Priority Queue
A complete binary tree (every level full except possibly the last, filled left to right) stored implicitly in an array, satisfying the heap property: in a min-heap, every parent is its children (max-heap: ). Completeness means the tree’s height is always , so push and pop — which each involve moving one element up or down a root-to-leaf path — are both ; peeking at the minimum (or maximum) is since it always sits at the root. Building a heap from existing elements is , not , because most nodes near the bottom need little to no adjustment.
std::priority_queue is a max-heap by default; pass greater<int> as the comparator for a min-heap.
priority_queue<int> maxHeap;
priority_queue<int, vector<int>, greater<int>> minHeap;
maxHeap.push(5); maxHeap.push(1); maxHeap.push(9);
maxHeap.top(); // 9Priority queues are the go-to structure whenever you repeatedly need “the current best/smallest/largest item” — Dijkstra’s shortest path and merging sorted lists both lean on exactly this.
Time Complexity: push/pop, peek, build
Space Complexity:
Trie (Prefix Tree)
A tree specialized for storing strings, where each edge represents one character and every root-to-node path spells out a prefix shared by all strings beneath it. Inserting or searching a string of length costs , independent of how many other strings are stored — unlike scanning a list of strings one by one. Each node typically holds an array (or map) of child pointers, one per possible next character.
struct TrieNode {
TrieNode* children[26] = {};
bool isEnd = false;
};
void insert(TrieNode* root, const string &word) {
TrieNode* cur = root;
for (char c : word) {
int idx = c - 'a';
if (!cur->children[idx]) cur->children[idx] = new TrieNode();
cur = cur->children[idx];
}
cur->isEnd = true;
}A binary trie over the bits of each number (instead of characters) is the standard structure for maximum-XOR-pair queries: insert every number’s binary representation, then for each query greedily walk down the opposite bit at every level to maximize the XOR.
Time Complexity: per insert/search, for a string of length
Space Complexity: worst case
Disjoint Set Union
Maintains a partition of elements into disjoint sets, exposing find(x) (which set is in?) and union(x, y) (merge ‘s set with ‘s). Two optimizations make both operations run in amortized — effectively constant for any that could ever appear in practice, since (the inverse Ackermann function) is at most for any remotely realistic input size:
- Path compression: every node visited during
findis re-pointed directly at the root, flattening future lookups. - Union by rank/size: always attach the smaller/shallower tree under the root of the larger one, keeping trees shallow to begin with.
vector<int> parent, rank_;
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]); // path compression
return parent[x];
}
void unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
if (rank_[x] < rank_[y]) swap(x, y);
parent[y] = x;
if (rank_[x] == rank_[y]) rank_[x]++;
}DSU is the standard structure behind Kruskal’s MST, cycle detection in an undirected structure, and offline connectivity queries.
Time Complexity: amortized per operation
Space Complexity:
Bitset
A fixed-size array of bits packed into machine words, where operations that would take on a plain boolean array (AND, OR, XOR, count, shift, over all bits) instead run in — the CPU processes a full 64-bit word per instruction rather than one bit at a time. std::bitset<N> fixes at compile time and supports all the usual bitwise operators directly.
bitset<1000> a, b;
a[5] = 1;
a |= b; // O(n / 64)
int ones = a.count(); // O(n / 64)This roughly 64x constant-factor speedup is what makes bitset-optimized brute force (e.g. for subset-sum reachability, or counting pairwise string differences) fast enough in problems where the “intended” complexity is barely out of reach.
Time Complexity: per bitwise operation, for an -bit bitset
Space Complexity: bytes