What Is It?
What Are STL Containers?
The Standard Template Library (STL) provides a collection of generic, type-safe container classes that store and organize data. Instead of writing your own linked list, binary search tree, or hash table from scratch, you use battle-tested containers like vector, set, map, and unordered_map that are already optimized and correct.
STL containers are divided into four categories:
- Sequence containers -- store elements in a linear order:
vector,list,deque - Associative containers -- store elements in sorted order using a balanced BST:
set,multiset,map,multimap - Unordered containers -- store elements using hash tables for O(1) average access:
unordered_set,unordered_map - Container adaptors -- wrap other containers with a restricted interface:
stack,queue,priority_queue
#include <iostream>
#include <vector>
#include <set>
#include <map>
#include <stack>
using namespace std;
int main() {
// Sequence: ordered by insertion
vector<int> v = {5, 2, 8, 2, 1};
// Associative: sorted, unique
set<int> s(v.begin(), v.end());
// s = {1, 2, 5, 8} -- sorted, duplicates removed
// Associative: key-value, sorted by key
map<string, int> marks;
marks["Arjun"] = 85;
marks["Priya"] = 92;
// Container adaptor: LIFO
stack<int> st;
st.push(10);
st.push(20);
cout << st.top() << endl; // 20
return 0;
}When Rohan says "use a map for frequency counting" or Meera says "use a priority_queue for the greedy approach," every competitive programmer immediately knows the data structure and its time complexities.
Why Does It Matter?
Why Are STL Containers Important?
1. Competitive Programming Efficiency
In competitive programming, choosing the right container is half the solution. Need O(log n) lookups with sorted order? Use set or map. Need O(1) average lookups? Use unordered_map. Need a max-heap? Use priority_queue. Knowing which container to use and its time complexity directly determines whether your solution passes within the time limit.
2. Correct by Construction
STL containers handle memory management, resizing, rebalancing, and rehashing automatically. A vector grows dynamically, a set stays balanced, and an unordered_map rehashes when the load factor is too high. Implementing these from scratch is error-prone and time-consuming.
3. Type Safety and Generic Programming
STL containers are templates -- vector<int>, map<string, int>, set<pair<int,int>> -- providing compile-time type checking. You cannot accidentally insert a string into a vector<int>.
4. Interoperability with STL Algorithms
All STL containers expose iterators, making them compatible with STL algorithms like sort, find, accumulate, and transform. This uniform interface is one of the most powerful features of the STL.
5. Interview and Placement Relevance
Companies like Google, Amazon, Microsoft, Flipkart, and Razorpay expect fluency with STL containers. Interview problems frequently require choosing the right container, understanding time complexities, and using containers like map, set, and priority_queue effectively.
Detailed Explanation
Detailed Explanation
The Standard Template Library (STL) provides a collection of generic, type-safe container classes that store and organize data. Instead of writing your own linked list, binary search tree, or hash table from scratch, you use battle-tested containers like vector, set, map, and unordered_map that are already optimized and correct.
STL containers are divided into four categories:
- Sequence containers -- store elements in a linear order:
vector,list,deque - Associative containers -- store elements in sorted order using a balanced BST:
set,multiset,map,multimap - Unordered containers -- store elements using hash tables for O(1) average access:
unordered_set,unordered_map - Container adaptors -- wrap other containers with a restricted interface:
stack,queue,priority_queue
#include <iostream>
#include <vector>
#include <set>
#include <map>
#include <stack>
using namespace std;
int main() {
// Sequence: ordered by insertion
vector<int> v = {5, 2, 8, 2, 1};
// Associative: sorted, unique
set<int> s(v.begin(), v.end());
// s = {1, 2, 5, 8} -- sorted, duplicates removed
// Associative: key-value, sorted by key
map<string, int> marks;
marks["Arjun"] = 85;
marks["Priya"] = 92;
// Container adaptor: LIFO
stack<int> st;
st.push(10);
st.push(20);
cout << st.top() << endl; // 20
return 0;
}When Rohan says "use a map for frequency counting" or Meera says "use a priority_queue for the greedy approach," every competitive programmer immediately knows the data structure and its time complexities.
1. Competitive Programming Efficiency
In competitive programming, choosing the right container is half the solution. Need O(log n) lookups with sorted order? Use set or map. Need O(1) average lookups? Use unordered_map. Need a max-heap? Use priority_queue. Knowing which container to use and its time complexity directly determines whether your solution passes within the time limit.
2. Correct by Construction
STL containers handle memory management, resizing, rebalancing, and rehashing automatically. A vector grows dynamically, a set stays balanced, and an unordered_map rehashes when the load factor is too high. Implementing these from scratch is error-prone and time-consuming.
3. Type Safety and Generic Programming
STL containers are templates -- vector<int>, map<string, int>, set<pair<int,int>> -- providing compile-time type checking. You cannot accidentally insert a string into a vector<int>.
4. Interoperability with STL Algorithms
All STL containers expose iterators, making them compatible with STL algorithms like sort, find, accumulate, and transform. This uniform interface is one of the most powerful features of the STL.
5. Interview and Placement Relevance
Companies like Google, Amazon, Microsoft, Flipkart, and Razorpay expect fluency with STL containers. Interview problems frequently require choosing the right container, understanding time complexities, and using containers like map, set, and priority_queue effectively.
Code Examples
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
v.push_back(10);
v.push_back(20);
v.push_back(30);
// Access by index
cout << "v[1] = " << v[1] << endl;
cout << "v.at(2) = " << v.at(2) << endl;
// Size vs Capacity
cout << "Size: " << v.size() << endl;
cout << "Capacity: " << v.capacity() << endl;
// Reserve to avoid reallocations
v.reserve(100);
cout << "After reserve(100), Capacity: " << v.capacity() << endl;
// Resize changes size (fills with default value)
v.resize(5, 0);
for (int x : v) cout << x << " ";
cout << endl;
// pop_back removes last element
v.pop_back();
cout << "After pop_back, size: " << v.size() << endl;
// clear removes all
v.clear();
cout << "After clear, size: " << v.size() << endl;
return 0;
}push_back is amortized O(1), random access via [] or at() is O(1). The key difference: [] has no bounds checking while at() throws out_of_range. Use reserve() when you know the approximate size upfront to avoid repeated reallocations.#include <iostream>
#include <set>
using namespace std;
int main() {
// set: sorted, unique elements
set<int> s = {5, 2, 8, 2, 1, 5};
cout << "Set: ";
for (int x : s) cout << x << " ";
cout << endl; // 1 2 5 8
s.insert(3);
s.insert(2); // duplicate, ignored
cout << "After insert 3 and 2: ";
for (int x : s) cout << x << " ";
cout << endl;
// find and count
if (s.find(3) != s.end()) cout << "3 found" << endl;
cout << "count(2) = " << s.count(2) << endl; // 0 or 1
// lower_bound and upper_bound
auto lb = s.lower_bound(3); // first >= 3
auto ub = s.upper_bound(5); // first > 5
cout << "lower_bound(3) = " << *lb << endl;
cout << "upper_bound(5) = " << *ub << endl;
// multiset: allows duplicates
multiset<int> ms = {5, 2, 8, 2, 1, 5};
cout << "Multiset: ";
for (int x : ms) cout << x << " ";
cout << endl; // 1 2 2 5 5 8
cout << "count(2) = " << ms.count(2) << endl; // 2
return 0;
}set stores unique elements in sorted order using a balanced BST (usually red-black tree). All operations -- insert, find, count, erase, lower_bound, upper_bound -- are O(log n). A multiset allows duplicates while maintaining sorted order. Use set when you need sorted unique values and O(log n) lookups.#include <iostream>
#include <map>
using namespace std;
int main() {
// map: sorted by key, unique keys
map<string, int> marks;
marks["Arjun"] = 85;
marks["Priya"] = 92;
marks["Kiran"] = 78;
marks.insert({"Neha", 88});
// Iterate (sorted by key)
for (auto& [name, score] : marks)
cout << name << ": " << score << endl;
// Access with [] (creates if missing!) vs at() (throws if missing)
marks["Ravi"]; // Creates entry with value 0!
cout << "Ravi (auto-created): " << marks["Ravi"] << endl;
// Safe access with find
auto it = marks.find("Priya");
if (it != marks.end())
cout << "Found Priya: " << it->second << endl;
// Frequency counting pattern
string text = "hello world";
map<char, int> freq;
for (char c : text) freq[c]++;
for (auto& [ch, cnt] : freq)
cout << ch << ":" << cnt << " ";
cout << endl;
// multimap: allows duplicate keys
multimap<string, int> scores;
scores.insert({"Arjun", 85});
scores.insert({"Arjun", 90});
cout << "Arjun count: " << scores.count("Arjun") << endl;
return 0;
}map stores key-value pairs sorted by key using a balanced BST. All operations are O(log n). The [] operator is dangerous because it inserts a default-constructed value if the key does not exist. Use find() or at() for safe access. The frequency counting pattern (freq[c]++) is one of the most common uses of map in competitive programming.#include <iostream>
#include <unordered_map>
#include <unordered_set>
using namespace std;
int main() {
// unordered_set: hash-based, O(1) average, no order
unordered_set<int> us = {5, 2, 8, 2, 1};
us.insert(3);
cout << "Contains 3? " << (us.count(3) ? "Yes" : "No") << endl;
// unordered_map: hash-based key-value, O(1) average
unordered_map<string, int> marks;
marks["Arjun"] = 85;
marks["Priya"] = 92;
marks["Kiran"] = 78;
// Fast frequency counting (faster than map for large inputs)
string text = "competitive programming";
unordered_map<char, int> freq;
for (char c : text) freq[c]++;
cout << "Frequency of 'e': " << freq['e'] << endl;
// Two Sum problem: classic unordered_map usage
vector<int> nums = {2, 7, 11, 15};
int target = 9;
unordered_map<int, int> seen;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (seen.count(complement)) {
cout << "Pair found: [" << seen[complement] << ", " << i << "]" << endl;
break;
}
seen[nums[i]] = i;
}
return 0;
}set/map for most cases but do not maintain sorted order. The Two Sum problem is a classic example where unordered_map gives an O(n) solution instead of O(n log n) with sorted containers.#include <iostream>
#include <stack>
#include <queue>
using namespace std;
int main() {
// Stack: LIFO
stack<int> st;
st.push(10);
st.push(20);
st.push(30);
cout << "Stack top: " << st.top() << endl;
st.pop();
cout << "After pop, top: " << st.top() << endl;
cout << "Stack size: " << st.size() << endl;
// Queue: FIFO
queue<int> q;
q.push(10);
q.push(20);
q.push(30);
cout << "Queue front: " << q.front() << ", back: " << q.back() << endl;
q.pop();
cout << "After pop, front: " << q.front() << endl;
// Priority Queue: max-heap by default
priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
cout << "Max-heap top: " << pq.top() << endl;
pq.pop();
cout << "After pop: " << pq.top() << endl;
// Min-heap using greater<int>
priority_queue<int, vector<int>, greater<int>> minPQ;
minPQ.push(30);
minPQ.push(10);
minPQ.push(50);
minPQ.push(20);
cout << "Min-heap top: " << minPQ.top() << endl;
return 0;
}stack is LIFO (push/pop/top), queue is FIFO (push/pop/front/back), and priority_queue is a max-heap by default. For a min-heap, pass greater<int> as the third template argument. All push/pop operations are O(log n) for priority_queue and O(1) for stack/queue.#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main() {
// Custom comparator: process by shortest distance first
// pair<distance, node>
using P = pair<int, int>;
priority_queue<P, vector<P>, greater<P>> pq;
pq.push({5, 1}); // node 1, distance 5
pq.push({2, 3}); // node 3, distance 2
pq.push({8, 2}); // node 2, distance 8
pq.push({1, 4}); // node 4, distance 1
cout << "Processing order (shortest distance first):" << endl;
while (!pq.empty()) {
auto [dist, node] = pq.top();
pq.pop();
cout << "Node " << node << ", distance " << dist << endl;
}
// Custom struct with lambda comparator
struct Task {
string name;
int priority;
};
auto cmp = [](const Task& a, const Task& b) {
return a.priority < b.priority; // higher priority first
};
priority_queue<Task, vector<Task>, decltype(cmp)> taskQ(cmp);
taskQ.push({"Debug", 3});
taskQ.push({"Deploy", 5});
taskQ.push({"Test", 4});
cout << "\nTask processing order:" << endl;
while (!taskQ.empty()) {
auto& t = taskQ.top();
cout << t.name << " (priority " << t.priority << ")" << endl;
taskQ.pop();
}
return 0;
}priority_queue with greater<pair<int,int>>. Pairs are compared lexicographically, so smallest distance comes first. The second example demonstrates a custom struct with a lambda comparator passed via decltype. This pattern appears in graph algorithms, task scheduling, and event-driven simulations.#include <iostream>
#include <list>
#include <deque>
using namespace std;
int main() {
// list: doubly-linked list, O(1) insert/erase anywhere with iterator
list<int> lst = {10, 20, 30};
lst.push_front(5);
lst.push_back(40);
auto it = lst.begin();
advance(it, 2); // move to 3rd element
lst.insert(it, 15); // insert before 3rd element
cout << "List: ";
for (int x : lst) cout << x << " ";
cout << endl;
lst.remove(20); // remove all occurrences of 20
cout << "After remove(20): ";
for (int x : lst) cout << x << " ";
cout << endl;
// deque: double-ended queue, O(1) push/pop at both ends + random access
deque<int> dq = {20, 30, 40};
dq.push_front(10);
dq.push_back(50);
cout << "Deque: ";
for (int x : dq) cout << x << " ";
cout << endl;
cout << "dq[2] = " << dq[2] << endl; // random access!
dq.pop_front();
dq.pop_back();
cout << "After pop front and back: ";
for (int x : dq) cout << x << " ";
cout << endl;
return 0;
}list is a doubly-linked list with O(1) insert/erase at any position (given an iterator) but no random access. A deque (double-ended queue) supports O(1) push/pop at both ends AND random access via []. Use list when you need frequent insertions/deletions in the middle. Use deque when you need fast operations at both ends plus random access.#include <iostream>
using namespace std;
int main() {
cout << "=== STL Container Time Complexity ===" << endl;
cout << endl;
cout << "SEQUENCE CONTAINERS:" << endl;
cout << "vector: access O(1) | push_back O(1)* | insert O(n) | find O(n)" << endl;
cout << "list: access O(n) | push_front O(1) | insert O(1) | find O(n)" << endl;
cout << "deque: access O(1) | push_front O(1) | insert O(n) | find O(n)" << endl;
cout << endl;
cout << "ASSOCIATIVE CONTAINERS (balanced BST):" << endl;
cout << "set: insert O(log n) | find O(log n) | erase O(log n) | sorted" << endl;
cout << "map: insert O(log n) | find O(log n) | []/at O(log n) | sorted" << endl;
cout << "multiset/multimap: same as set/map but allow duplicates" << endl;
cout << endl;
cout << "UNORDERED CONTAINERS (hash table):" << endl;
cout << "unordered_set: insert O(1)* | find O(1)* | erase O(1)* | no order" << endl;
cout << "unordered_map: insert O(1)* | find O(1)* | []/at O(1)* | no order" << endl;
cout << "* = amortized / average case" << endl;
cout << endl;
cout << "CONTAINER ADAPTORS:" << endl;
cout << "stack: push O(1) | pop O(1) | top O(1)" << endl;
cout << "queue: push O(1) | pop O(1) | front O(1)" << endl;
cout << "priority_queue: push O(log n) | pop O(log n) | top O(1)" << endl;
return 0;
}Common Mistakes
Using map[] for Lookup Instead of find()
map<string, int> m;
m["Arjun"] = 85;
if (m["Priya"] > 0)
cout << "Priya found" << endl;
// Bug: m["Priya"] was just created with value 0!map<string, int> m;
m["Arjun"] = 85;
auto it = m.find("Priya");
if (it != m.end())
cout << "Priya found: " << it->second << endl;
else
cout << "Priya not found" << endl;find() or count() to check existence without inserting. The [] operator should only be used when you want to create or overwrite an entry.Modifying a set Element Directly
set<int> s = {10, 20, 30};
auto it = s.find(20);
*it = 25; // Compilation error!set<int> s = {10, 20, 30};
s.erase(20);
s.insert(25);
// s = {10, 25, 30}Forgetting That priority_queue Is a Max-Heap by Default
priority_queue<int> pq;
pq.push(10);
pq.push(30);
pq.push(20);
cout << pq.top() << endl; // Prints 30, not 10!
// Programmer expected min-heap behaviorpriority_queue<int, vector<int>, greater<int>> pq; // min-heap
pq.push(10);
pq.push(30);
pq.push(20);
cout << pq.top() << endl; // 10 (smallest first)priority_queue<int> uses less<int> internally, which creates a max-heap. For a min-heap, specify all three template parameters: priority_queue<int, vector<int>, greater<int>>.Iterating and Erasing from a Map/Set Simultaneously (Wrong Way)
map<string, int> m = {{"a", 1}, {"b", 2}, {"c", 3}};
for (auto it = m.begin(); it != m.end(); ++it) {
if (it->second < 3)
m.erase(it); // Iterator invalidated!
}map<string, int> m = {{"a", 1}, {"b", 2}, {"c", 3}};
for (auto it = m.begin(); it != m.end(); ) {
if (it->second < 3)
it = m.erase(it); // erase returns next valid iterator
else
++it;
}erase() on associative containers returns an iterator to the next element. Use this return value instead of manually incrementing. Notice the ++it is in the else branch, not in the for loop header.Using unordered_map/unordered_set with Unhashable Types
unordered_set<pair<int,int>> s;
s.insert({1, 2}); // Compilation error!// Option 1: Use ordered set (simpler)
set<pair<int,int>> s;
s.insert({1, 2});
// Option 2: Custom hash for unordered_set
struct PairHash {
size_t operator()(const pair<int,int>& p) const {
return hash<long long>()(((long long)p.first << 32) | p.second);
}
};
unordered_set<pair<int,int>, PairHash> us;
us.insert({1, 2});pair, tuple, and custom structs do not. Either provide a custom hash functor or use the ordered variants (set, map) which only need operator<.Summary
- Sequence containers store elements in insertion order. vector provides O(1) random access and amortized O(1) push_back but O(n) insertion in the middle. list provides O(1) insertion/deletion anywhere with an iterator but no random access. deque provides O(1) push/pop at both ends plus random access.
- Associative containers (set, multiset, map, multimap) use balanced BSTs internally. All operations are O(log n). Elements are always sorted. set stores unique values, multiset allows duplicates, map stores unique key-value pairs, multimap allows duplicate keys.
- Unordered containers (unordered_set, unordered_map) use hash tables. Average-case operations are O(1), worst-case O(n). They do not maintain any order. Use them when you need fast lookups and do not care about order.
- Container adaptors wrap other containers with restricted interfaces. stack (LIFO), queue (FIFO), and priority_queue (heap). priority_queue is a max-heap by default; use greater<T> for a min-heap.
- The map [] operator inserts a default value if the key does not exist. Use find() or count() for safe lookups. The at() method throws out_of_range if the key is missing.
- For competitive programming: use vector for arrays, unordered_map for frequency counting, set for sorted unique elements, and priority_queue for greedy algorithms. Know the time complexity of every operation.
- vector::reserve() pre-allocates memory without changing size. vector::resize() changes the size. Use reserve() when you know the approximate number of elements to avoid reallocations.
- Elements in a set/map cannot be modified in place because it would break the sorted invariant. Erase the old element and insert the new one instead.
- When erasing from a map/set while iterating, use the iterator returned by erase() to avoid iterator invalidation. The pattern is: it = container.erase(it).
- Choose the right container based on your needs: sorted order with O(log n) -> set/map, O(1) average lookup -> unordered_set/unordered_map, LIFO -> stack, FIFO -> queue, priority processing -> priority_queue.