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.

OperationComplexity
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.

OperationSinglyDoubly
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 20

Time 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.

OperationQueueDeque
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 .

OperationComplexity
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 >= 3

Space 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(); // 9

Priority 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 find is 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