A container is a data structure with a chosen tradeoff . You never "just store data" — you store it in a way that makes some operations cheap and others expensive. The whole STL container zoo exists because no single structure is fast at everything . Your job: match the access pattern you need to the container whose internal memory layout makes that pattern O ( 1 ) O(1) O ( 1 ) or O ( log n ) O(\log n) O ( log n ) instead of O ( n ) O(n) O ( n ) .
Definition Two grand families
Sequence containers — keep elements in the order you inserted them. Position is meaningful. → array, vector, deque, list.
Associative containers — keep elements ordered/organised by their value (key) , not by insertion order. Position is decided by the container. → set, multiset, map, multimap (tree-based), and unordered_* (hash-based).
WHAT decides speed? The memory layout :
Contiguous array (vector, array, deque-ish): index math gives O ( 1 ) O(1) O ( 1 ) random access, but inserting in the middle shifts everything (O ( n ) O(n) O ( n ) ).
Linked nodes (list): each node points to the next, so splicing is O ( 1 ) O(1) O ( 1 ) , but you can't jump to index k k k — you must walk (O ( n ) O(n) O ( n ) ).
Balanced binary search tree (set/map): keeps keys sorted, so search/insert/erase are O ( log n ) O(\log n) O ( log n ) and you can iterate in sorted order.
Hash table (unordered_*): a hash function scatters keys into buckets, giving average O ( 1 ) O(1) O ( 1 ) search/insert — but no order , and worst case O ( n ) O(n) O ( n ) on collisions.
std::array<T, N>
A fixed-size contiguous array whose size N is known at compile time . Lives on the stack (no heap allocation). Zero overhead over a C array but with STL interface (.size(), iterators, .at()).
std::vector<T>
A dynamic contiguous array . Grows automatically. O(1) random access v[i], amortised O(1) push_back, but O(n) to insert/erase in the middle.
push_back is amortised O ( 1 ) O(1) O ( 1 )
When the vector is full it allocates a bigger block (typically capacity × 2 \times 2 × 2 ) and copies the old elements over. A single such resize is O ( n ) O(n) O ( n ) — but it happens rarely. Spread the cost over all the cheap pushes and it averages to O ( 1 ) O(1) O ( 1 ) .
std::deque<T> (double-ended queue)
Fast push_front and push_back, both amortised O ( 1 ) O(1) O ( 1 ) , plus O ( 1 ) O(1) O ( 1 ) random access. Internally a sequence of fixed-size chunks tracked by a "map" of pointers — so it's almost contiguous but not guaranteed one block.
std::list<T> (doubly-linked list)
Each element is a node with prev/next pointers. Strength: O ( 1 ) O(1) O ( 1 ) insert/erase/splice anywhere (given an iterator). Weakness: no [] access — finding index k k k is O ( n ) O(n) O ( n ) , and cache performance is poor (nodes scattered in memory).
Definition Tree-based (ordered) —
set, multiset, map, multimap
Implemented as a self-balancing binary search tree (red-black tree). Keys stay sorted ; find, insert, erase are O ( log n ) O(\log n) O ( log n ) .
set — unique sorted keys .
multiset — sorted keys, duplicates allowed .
map — sorted key → value pairs, unique keys.
multimap — key → value, duplicate keys allowed .
Definition Hash-based (unordered) —
unordered_set, unordered_map
Implemented as a hash table : a hash(key) function picks a bucket; collisions chain inside the bucket. Average O ( 1 ) O(1) O ( 1 ) for find/insert/erase, no ordering , worst case O ( n ) O(n) O ( n ) .
log n \log n log n for trees?
A balanced BST of n n n nodes has height h ≈ log 2 n h \approx \log_2 n h ≈ log 2 n . Each search compares against one node per level and goes left/right — so it touches at most h h h nodes. Halving the search space each step is the same idea as binary search.
Container
Random access
Insert/erase middle
Insert front
Insert back
Search by value
Ordered?
array
O ( 1 ) O(1) O ( 1 )
— (fixed)
—
—
O ( n ) O(n) O ( n )
insertion
vector
O ( 1 ) O(1) O ( 1 )
O ( n ) O(n) O ( n )
O ( n ) O(n) O ( n )
amort. O ( 1 ) O(1) O ( 1 )
O ( n ) O(n) O ( n )
insertion
deque
O ( 1 ) O(1) O ( 1 )
O ( n ) O(n) O ( n )
O ( 1 ) O(1) O ( 1 )
O ( 1 ) O(1) O ( 1 )
O ( n ) O(n) O ( n )
insertion
list
O ( n ) O(n) O ( n )
O ( 1 ) O(1) O ( 1 ) *
O ( 1 ) O(1) O ( 1 )
O ( 1 ) O(1) O ( 1 )
O ( n ) O(n) O ( n )
insertion
set/map
—
O ( log n ) O(\log n) O ( log n )
—
—
O ( log n ) O(\log n) O ( log n )
sorted
unordered_*
—
O ( 1 ) O(1) O ( 1 ) avg
—
—
O ( 1 ) O(1) O ( 1 ) avg
no
* given an iterator to the position.
Worked example 1. Choosing for "frequency count of words"
Need: map each word → count, fast lookups, order doesn't matter.
unordered_map < string, int> freq;
for ( auto& w : words) freq[w] ++ ; // O(1) average per word
Why this step? freq[w] default-constructs the value to 0 if missing, then ++. Hash table gives O ( 1 ) O(1) O ( 1 ) average — total O ( N ) O(N) O ( N ) for N N N words. A map would work but adds a log \log log factor and we don't need sorted output.
Worked example 2. "Insert in the middle a million times"
list <int> L;
auto it = L. begin ();
// ... advance it to the spot, then:
L. insert (it, x); // O(1) — just relinks pointers
Why this step? With a vector, each middle insert shifts up to n n n elements (O ( n ) O(n) O ( n ) ). With list, given the iterator, it's pure pointer surgery — O ( 1 ) O(1) O ( 1 ) . (But getting to the iterator can cost O ( n ) O(n) O ( n ) , so this wins only when you already hold the position.)
Worked example 3. "Keep a sorted set of scores, allow duplicates, get the smallest"
multiset <int> scores;
scores. insert ( 50 ); scores. insert ( 50 ); scores. insert ( 20 );
int smallest = * scores. begin (); // 20, O(1) after sort kept by tree
scores. erase (scores. begin ()); // remove ONE smallest, O(log n)
Why this step? multiset keeps elements sorted at all times, so begin() is the minimum. We use multiset not set because duplicate 50s must be kept. erase(iterator) removes a single element; erase(value) would remove all copies.
Worked example 4. Iterating a map in sorted key order
map < string, int> m = {{ "banana" , 3 },{ "apple" , 5 }};
for ( auto& [k,v] : m) cout << k << " " << v << " \n " ;
// prints apple 5, then banana 3 (alphabetical)
Why this step? map is tree-based ⇒ iteration yields keys in sorted order for free . unordered_map would print in arbitrary bucket order.
unordered_map is always faster than map, so use it everywhere."
Why it feels right: O ( 1 ) O(1) O ( 1 ) average beats O ( log n ) O(\log n) O ( log n ) . The catch: (1) you lose ordering; if you need sorted iteration or lower_bound, map is the right tool. (2) Worst-case unordered_map degrades to O ( n ) O(n) O ( n ) under hash collisions. (3) For small n n n , the constant factor + cache misses can make map faster in practice. Fix: use unordered_map when you need raw key lookup and never iterate in order; use map when order/range queries matter.
erase(value) on a multiset removes just one element."
Why it feels right: erase sounds singular. Reality: ms.erase(50) removes every 50. To remove just one, pass an iterator : ms.erase(ms.find(50)). Fix: iterator-erase = one element; value-erase = all matches.
Common mistake "Inserting into a
vector while holding old iterators/pointers is fine."
Why it feels right: the element 'looks' like it's still there. Reality: a push_back that triggers a reallocation moves the whole array — all old iterators, pointers and references are invalidated (dangling). Fix: re-fetch iterators after insertion, or reserve() capacity up front. (list/deque-front-back don't invalidate other elements the same way.)
vector is slow because it's O ( n ) O(n) O ( n ) to grow."
Why it feels right: copying on resize is O ( n ) O(n) O ( n ) . Reality: doubling makes it amortised O ( 1 ) O(1) O ( 1 ) , and contiguous memory gives superb cache locality — usually the fastest container in real benchmarks. Fix: default to vector unless you have a specific reason not to.
Recall Feynman: explain to a 12-year-old
Imagine ways to keep your toys.
A vector/array is a long shelf — you can grab toy #7 instantly, but to squeeze a new toy in the middle you must slide everyone over.
A list is toys holding hands in a chain — you can splice a new friend between two hands instantly, but to find the 7th toy you must walk the chain counting.
A deque is a shelf open at both ends, so you can add at the front or back easily.
A set/map is a librarian who always keeps toys in alphabetical order — finding any toy is quick because she knows where things go.
An unordered_map is a wall of labelled bins — you compute which bin a toy belongs to and toss it straight in (super fast), but the toys end up in no neat order.
Mnemonic Remember the families
"A Very Dumb List Sits Quietly Mapping Unknown Hashes."
A rray, V ector, D eque, L ist = sequence; S et, multiset, M ap, multimap = ordered (tree); U nordered_* = hash.
And for the speed rule: "Contiguous = jump fast, shift slow; Linked = splice fast, jump slow; Tree = sorted & log; Hash = unsorted & flat-out."
Which container gives O(1) random access AND amortised O(1) push_back? std::vector
Why is vector's push_back amortised O(1) despite resizing? Resizing doubles capacity, so the O(n) copies happen rarely; total copy work over n pushes is < 2n, averaging O(1).
What is the key difference between set and multiset? set stores unique keys; multiset allows duplicate keys (both kept sorted).
What underlying data structure backs set/map? A self-balancing binary search tree (red-black tree), giving O(log n) operations and sorted iteration.
What backs unordered_map/unordered_set? A hash table — average O(1), no ordering, worst case O(n).
When should you prefer map over unordered_map? When you need sorted iteration, range queries (lower_bound/upper_bound), or guaranteed log worst case.
Complexity of inserting in the middle of a vector vs a list (with iterator)? vector O(n) (shifts elements); list O(1) (relinks pointers).
What container allows O(1) push_front and push_back plus O(1) random access? std::deque.
How do you erase exactly ONE element from a multiset by value? ms.erase(ms.find(value)) — passing the value directly (ms.erase(value)) erases ALL matches.
What invalidates vector iterators/pointers? A reallocation (e.g. push_back beyond capacity) moves the array; reserve() avoids it.
Difference between std::array and std::vector? array is fixed compile-time size, on stack, no resize; vector is dynamic, heap-allocated, grows.
Why does a balanced BST give O(log n) search? Its height is ~log2(n), and search visits at most one node per level, halving the search space each step.
Big-O notation — the language of these tradeoffs.
Amortised analysis — justifies vector's O(1) push_back.
Hash functions and collisions — why unordered_* averages O(1).
Red-black trees / Binary search trees — engine behind set/map.
Iterators in C++ — invalidation rules differ per container.
Cache locality — why contiguous vector often beats list in practice.
STL algorithms (sort, find, lower_bound) — operate on these containers.
Tradeoff: no structure fast at everything
Memory layout picks speed
Memory layout picks speed
set, map, multiset, multimap
unordered_set, unordered_map
Intuition Hinglish mein samjho
Dekho, STL containers ka pura funda ek hi baat par tika hai: har container ek tradeoff hai . Koi bhi structure sab kuch fast nahi kar sakta. Agar tumhe index se turant element chahiye (jaise v[7]), toh vector ya array lo — yeh contiguous memory mein rehte hain, isliye jump O(1) hota hai. Lekin beech mein insert karoge toh saare elements shift karne padenge, that's O(n). Iska ulta list hai — yeh nodes ki chain hai jahan har node next/prev pointer rakhta hai, toh beech mein insert karna O(1) (bas pointer jodo), par 7th element dhoondhne ke liye poori chain walk karni padegi.
deque ek mast cheez hai — front aur back dono taraf se O(1) mein add kar sakte ho, aur index access bhi milta hai. Jab queue/sliding window banana ho toh yeh kaam aata hai. array fixed size ka hota hai, compile time pe size pata hota hai, stack pe rehta hai — zero overhead.
Ab associative wale: set aur map andar se balanced binary search tree (red-black tree) hote hain, isliye sab kuch sorted rehta hai aur operations O(log n) lagte hain. Iska bonus yeh ki iterate karoge toh sorted order milega free mein. multiset/multimap mein duplicate keys allowed hain. unordered_map aur unordered_set hash table hain — average O(1), bohot fast, par order kuch nahi milta aur worst case O(n) ho sakta hai agar collisions zyada ho.
Practical rule yaad rakho: by default vector use karo (cache friendly, fastest in real life). Sorted chahiye ya range query (lower_bound) chahiye toh map. Sirf fast lookup chahiye aur order ki parwah nahi toh unordered_map. Aur ek bada trap: multiset.erase(value) saare matching elements uda deta hai — sirf ek hatana