πŸ”’ Private Site

This site is password-protected.

Standard Template Library (STL)

The STL is C++’s killer feature β€” a library of generic, efficient, well-tested containers and algorithms. Mastering STL is what makes you fast in both competitive programming and production code.


Table of Contents


Glossary β€” Key Terms at a Glance

Term Meaning
Container Object that stores a collection of elements
Iterator Generalized pointer β€” uniform access to container elements
Algorithm Generic function operating on iterator ranges
Amortized O(1) Usually O(1), occasionally O(n) on reallocation, averages to O(1)
Red-Black Tree Self-balancing BST used by set/map β€” O(log n) guaranteed
Hash Table Array of buckets β€” O(1) average lookup, O(n) worst
RAII Containers manage their own memory β€” no manual delete
Cache Locality Contiguous memory means fewer cache misses β†’ faster iteration

1 β€” STL Architecture

1.1 The Three Pillars

Definition: The STL is built on three orthogonal pillars that compose together through iterators:

Containers  ←→  Iterators  ←→  Algorithms
(store data)    (access data)   (process data)

Iterators are the glue β€” they let algorithms work with any container without knowing its internal structure.


2 β€” Sequence Containers

2.1 vector β€” Dynamic Array (USE THIS BY DEFAULT)

#include <vector>

std::vector<int> v;                    // empty
std::vector<int> v(10);               // 10 elements, all 0
std::vector<int> v(10, -1);           // 10 elements, all -1
std::vector<int> v{1, 2, 3, 4, 5};   // initializer list

// Access
v[0];          // unchecked (fast, UB if out of bounds)
v.at(0);       // bounds-checked (throws std::out_of_range)
v.front();     // first element
v.back();      // last element
v.data();      // raw pointer to underlying array

// Modification
v.push_back(6);           // add to end β€” amortized O(1)
v.emplace_back(7);        // construct in-place (avoids copy)
v.pop_back();             // remove last β€” O(1)
v.insert(v.begin(), 0);   // insert at position β€” O(n)
v.erase(v.begin() + 2);   // erase at position β€” O(n)

// Size
v.size();      // current number of elements
v.capacity();  // allocated space (β‰₯ size)
v.empty();     // true if size == 0
v.reserve(100);// pre-allocate (avoid reallocations!)
v.shrink_to_fit(); // release excess capacity
v.clear();     // remove all elements (size=0, capacity unchanged)
Operation Complexity
push_back Amortized O(1)
pop_back O(1)
operator[] / at O(1)
insert / erase (middle) O(n)
find (linear search) O(n)

When to use vector: Almost always. Contiguous memory = cache-friendly = fast iteration. Use reserve() before bulk push_back to avoid costly reallocations.


2.2 deque β€” Double-Ended Queue

#include <deque>

std::deque<int> dq{1, 2, 3};
dq.push_front(0);    // O(1)
dq.push_back(4);     // O(1)
dq.pop_front();      // O(1)
dq.pop_back();       // O(1)
dq[2];               // O(1) random access

When to use: Need O(1) insertion at both ends. Slightly slower than vector for iteration due to non-contiguous internal blocks.


2.3 list β€” Doubly Linked List

#include <list>

std::list<int> lst{1, 2, 3, 4, 5};
lst.push_front(0);
lst.push_back(6);
lst.sort();            // member sort β€” O(n log n)
lst.reverse();
lst.unique();          // remove consecutive duplicates

auto it = std::next(lst.begin(), 2);
lst.insert(it, 99);   // O(1) insertion (if you have iterator)
lst.erase(it);         // O(1) deletion

⚠️ Rarely the right choice. Poor cache locality makes std::list slower than std::vector in nearly all real-world scenarios, even for frequent insertions.


2.4 array β€” Fixed-Size Array

#include <array>

std::array<int, 5> arr{1, 2, 3, 4, 5};
arr.size();    // 5 β€” constexpr, known at compile time
arr.fill(0);   // set all elements to 0

When to use: Size known at compile time. Zero overhead over C arrays, but type-safe with .size(), .at(), and range-for support.


3 β€” Associative Containers (Ordered)

All ordered containers use Red-Black Trees internally β†’ $O(\log n)$ guaranteed for insert, find, erase.

3.1 set β€” Unique Sorted Elements

#include <set>

std::set<int> s{5, 3, 1, 4, 2};
// s = {1, 2, 3, 4, 5} β€” automatically sorted

s.insert(6);               // O(log n)
s.erase(3);                // O(log n)
s.count(4);                // 1 (exists) or 0 (doesn't)
s.find(4) != s.end();      // true if found
s.contains(4);             // C++20: cleaner

// Range queries
auto it = s.lower_bound(3);  // iterator to first element β‰₯ 3
auto it2 = s.upper_bound(3); // iterator to first element > 3

πŸ” Why Range Queries Matter: lower_bound/upper_bound make set/map ideal for interval queries, order statistics, and sweepline algorithms.


3.2 map β€” Key-Value Sorted by Key

#include <map>

std::map<std::string, int> m;
m["alice"] = 90;
m["bob"] = 85;
m.insert({"charlie", 92});
m.emplace("dave", 88);

// Access
m["alice"];                   // 90 (WARNING: creates entry if key missing!)
m.at("alice");                // 90 (throws if key missing β€” safer)

// Check existence
if (m.count("alice")) { /* exists */ }
if (m.contains("alice")) { /* C++20 */ }

// Iterate (sorted by key)
for (const auto& [name, score] : m) {
    std::cout << name << ": " << score << "\n";
}

// C++17: insert_or_assign, try_emplace
m.insert_or_assign("alice", 95);  // insert or update
m.try_emplace("eve", 91);         // only insert if not present

⚠️ m[key] trap: Using operator[] with a non-existent key silently creates a default entry. Use .at() or .find() when you don’t want this behavior.


3.3 multiset / multimap

std::multiset<int> ms{1, 1, 2, 3, 3, 3};
ms.count(3);   // 3
ms.erase(ms.find(3)); // removes ONE occurrence of 3
ms.erase(3);           // removes ALL occurrences of 3

4 β€” Unordered Containers (Hash-Based)

Hash tables β†’ $O(1)$ average, $O(n)$ worst case. Use when you don’t need ordering.

4.1 unordered_set & unordered_map

#include <unordered_set>
#include <unordered_map>

std::unordered_set<int> us{1, 2, 3, 4, 5};
us.insert(6);          // O(1) average
us.count(3);           // O(1) average
us.erase(2);           // O(1) average

std::unordered_map<std::string, int> um;
um["key1"] = 10;
um.insert({"key2", 20});

4.2 Ordered vs Unordered

Container Ordered? Lookup Insert Use When
set/map βœ… Sorted O(log n) O(log n) Need order, range queries
unordered_set/map ❌ O(1) avg O(1) avg Need speed, no order needed

4.3 Custom Hash

struct PairHash {
    size_t operator()(const std::pair<int,int>& p) const {
        auto h1 = std::hash<int>{}(p.first);
        auto h2 = std::hash<int>{}(p.second);
        return h1 ^ (h2 << 32);  // combine hashes
    }
};

std::unordered_set<std::pair<int,int>, PairHash> ps;

πŸ” Why Custom Hash? STL doesn’t provide std::hash for pairs, tuples, or user types. You must supply your own hash functor for unordered containers to work with them.


5 β€” Container Adaptors

5.1 stack, queue, priority_queue

Container adaptors are wrappers on top of other containers with restricted interfaces.

std::stack β€” LIFO:

#include <stack>
std::stack<int> st;
st.push(1); st.push(2);
st.top();    // 2
st.pop();    // removes 2

std::queue β€” FIFO:

#include <queue>
std::queue<int> q;
q.push(1); q.push(2);
q.front();   // 1
q.back();    // 2
q.pop();     // removes 1

std::priority_queue β€” Max-Heap:

#include <queue>

// Max-heap (default)
std::priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(4);
pq.top();  // 4 (largest)
pq.pop();  // removes 4

// Min-heap
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
min_pq.push(3); min_pq.push(1); min_pq.push(4);
min_pq.top();  // 1 (smallest)

// Custom comparator
auto cmp = [](const auto& a, const auto& b) { return a.cost > b.cost; };
std::priority_queue<Node, std::vector<Node>, decltype(cmp)> pq(cmp);

6 β€” Iterators

6.1 Iterator Types & Categories

std::vector<int> v{10, 20, 30, 40, 50};

// Forward iteration
for (auto it = v.begin(); it != v.end(); ++it)
    std::cout << *it << " ";

// Reverse iteration
for (auto it = v.rbegin(); it != v.rend(); ++it)
    std::cout << *it << " ";

// Const iterators (read-only)
for (auto it = v.cbegin(); it != v.cend(); ++it) {
    // *it = 42;  // ERROR: const
}
Category Capabilities Containers
Input Read, forward only istream_iterator
Output Write, forward only ostream_iterator
Forward Read/write, forward forward_list, unordered_*
Bidirectional + backward list, set, map
Random Access + jump by n, compare vector, deque, array

7 β€” Algorithms

The real power of STL. All in <algorithm> and <numeric>. They operate on iterator ranges β€” completely container-agnostic.

7.1 Searching

#include <algorithm>

std::vector<int> v{1, 3, 5, 7, 9, 11};

// Linear search β€” O(n)
auto it = std::find(v.begin(), v.end(), 7);
if (it != v.end()) { /* found at position (it - v.begin()) */ }

// Binary search β€” O(log n) β€” REQUIRES SORTED DATA
bool found = std::binary_search(v.begin(), v.end(), 7);  // true

// Lower/upper bound β€” O(log n)
auto lb = std::lower_bound(v.begin(), v.end(), 5);  // first β‰₯ 5 β†’ points to 5
auto ub = std::upper_bound(v.begin(), v.end(), 5);  // first > 5 β†’ points to 7

// Find with predicate
auto it2 = std::find_if(v.begin(), v.end(), [](int x) { return x > 6; });
// points to 7

7.2 Sorting

std::vector<int> v{5, 2, 8, 1, 9};

std::sort(v.begin(), v.end());                     // ascending: {1, 2, 5, 8, 9}
std::sort(v.begin(), v.end(), std::greater<int>()); // descending: {9, 8, 5, 2, 1}

// Custom sort
std::sort(v.begin(), v.end(), [](int a, int b) {
    return abs(a) < abs(b);  // sort by absolute value
});

// Partial sort β€” only first k elements sorted
std::partial_sort(v.begin(), v.begin() + 3, v.end());

// nth_element β€” O(n) average, finds the nth element
std::nth_element(v.begin(), v.begin() + 2, v.end());
// v[2] is the median; elements before it are smaller, after are larger

// Stable sort (preserves order of equal elements)
std::stable_sort(v.begin(), v.end());

πŸ” When to use which: std::sort is $O(n \log n)$ introsort. Use nth_element for $O(n)$ median/selection. Use partial_sort when you need only the top $k$ elements sorted.


7.3 Modifying

// Transform
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> squared(5);
std::transform(v.begin(), v.end(), squared.begin(), [](int x) { return x * x; });
// squared = {1, 4, 9, 16, 25}

// Fill
std::fill(v.begin(), v.end(), 0);

// Replace
std::replace(v.begin(), v.end(), 0, -1);  // replace all 0s with -1

// Remove-erase idiom (pre-C++20)
v.erase(std::remove(v.begin(), v.end(), 3), v.end());  // remove all 3s

// C++20: cleaner
std::erase(v, 3);                    // remove all 3s
std::erase_if(v, [](int x) { return x < 0; });  // remove negatives

// Unique (remove consecutive duplicates β€” sort first for all duplicates)
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());

// Reverse & Rotate
std::reverse(v.begin(), v.end());
std::rotate(v.begin(), v.begin() + 2, v.end());  // {3,4,5,1,2}

7.4 Permutations

std::vector<int> v{1, 2, 3};

// Generate all permutations
do {
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
} while (std::next_permutation(v.begin(), v.end()));

7.5 Numeric Algorithms

#include <numeric>

std::vector<int> v{1, 2, 3, 4, 5};

int sum = std::accumulate(v.begin(), v.end(), 0);           // 15
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());  // 120

// Partial sums (prefix sum)
std::vector<int> prefix(5);
std::partial_sum(v.begin(), v.end(), prefix.begin());  // {1, 3, 6, 10, 15}

// Inner product (dot product)
std::vector<int> w{5, 4, 3, 2, 1};
int dot = std::inner_product(v.begin(), v.end(), w.begin(), 0);  // 35

// Iota (fill with incrementing values)
std::iota(v.begin(), v.end(), 1);  // {1, 2, 3, 4, 5}

// GCD / LCM (C++17)
int g = std::gcd(12, 8);   // 4
int l = std::lcm(12, 8);   // 24

// Min/Max
int mn = *std::min_element(v.begin(), v.end());
int mx = *std::max_element(v.begin(), v.end());
auto [lo, hi] = std::minmax_element(v.begin(), v.end());

πŸ” Prefix Sums: One of the most important patterns in competitive programming and DSA. partial_sum computes running totals, enabling $O(1)$ range-sum queries on static arrays.


7.5.1 Deep Dive β€” std::partial_sum

Definition: std::partial_sum computes the running (cumulative) result of a binary operation over a range. By default it uses addition, producing a prefix sum array β€” one of the most powerful primitives in algorithm design.

Header: #include <numeric>

Signatures

// (1) Default: uses operator+
template<class InputIt, class OutputIt>
OutputIt partial_sum(InputIt first, InputIt last, OutputIt d_first);

// (2) Custom binary operation
template<class InputIt, class OutputIt, class BinaryOp>
OutputIt partial_sum(InputIt first, InputIt last, OutputIt d_first, BinaryOp op);
  • Returns: Iterator to one past the last element written β€” i.e. if it wrote $n$ elements starting at d_first, it returns d_first + n. This follows the STL half-open range [begin, end) convention: the returned iterator marks the end of the written range, not the last written element. You can use it to chain into another algorithm or compute the count via result - d_first.

    std::vector<int> v{10, 20, 30};
    std::vector<int> out(3);
    auto end_it = std::partial_sum(v.begin(), v.end(), out.begin());
    // end_it == out.begin() + 3 == out.end()
    // *std::prev(end_it) == 60  (the last element written)
    // end_it - out.begin() == 3  (number of elements written)
    
  • Complexity: Exactly $(n - 1)$ applications of op, where $n$ = std::distance(first, last).
  • The input and output ranges may overlap β€” specifically, d_first may equal first (in-place operation is valid).

How It Works β€” Step by Step

Given input $[a_0, a_1, a_2, \ldots, a_{n-1}]$, the output is:

\[\begin{aligned} d_0 &= a_0 \\ d_1 &= a_0 + a_1 \\ d_2 &= a_0 + a_1 + a_2 \\ &\;\vdots \\ d_k &= \sum_{i=0}^{k} a_i \end{aligned}\]

Equivalent loop:

// What partial_sum does internally (simplified)
void my_partial_sum(auto first, auto last, auto d_first) {
    if (first == last) return;
    auto sum = *first;
    *d_first = sum;
    while (++first != last) {
        sum = sum + *first;   // or: sum = op(sum, *first)
        *++d_first = sum;
    }
}

⚠️ Key detail: The standard guarantees that sum is computed using the previously accumulated value, not re-reading from the output. This is why in-place usage (d_first == first) is safe and well-defined.

In-Place Usage

partial_sum can write directly back into the source range:

std::vector<int> v{3, 1, 4, 1, 5};
std::partial_sum(v.begin(), v.end(), v.begin());
// v = {3, 4, 8, 9, 14}  β€” modified in-place

This is memory-efficient and commonly used when you no longer need the original array.

Custom Binary Operations

The second overload accepts any binary operation, turning partial_sum into a generic scan (running fold):

std::vector<int> v{3, 1, 4, 1, 5};
std::vector<int> out(v.size());

// Running product
std::partial_sum(v.begin(), v.end(), out.begin(), std::multiplies<>());
// out = {3, 3, 12, 12, 60}

// Running maximum (prefix max)
std::partial_sum(v.begin(), v.end(), out.begin(),
    [](int a, int b) { return std::max(a, b); });
// out = {3, 3, 4, 4, 5}

// Running minimum (prefix min)
std::partial_sum(v.begin(), v.end(), out.begin(),
    [](int a, int b) { return std::min(a, b); });
// out = {3, 1, 1, 1, 1}

// Running XOR
std::partial_sum(v.begin(), v.end(), out.begin(), std::bit_xor<>());
// out = {3, 2, 6, 7, 2}

// Running GCD
std::partial_sum(v.begin(), v.end(), out.begin(),
    [](int a, int b) { return std::gcd(a, b); });

Key insight: partial_sum with a custom op computes a prefix scan for any associative operation. This generalizes far beyond addition β€” prefix max, prefix min, prefix XOR, prefix GCD are all useful in competitive programming.

The Prefix Sum Pattern β€” $O(1)$ Range Queries

This is the primary reason partial_sum matters. Given a prefix sum array $P$, any range sum $\sum_{i=l}^{r} a_i$ can be answered in $O(1)$:

\[\text{sum}(l, r) = \begin{cases} P[r] & \text{if } l = 0 \\ P[r] - P[l-1] & \text{if } l > 0 \end{cases}\]
std::vector<int> a{2, 5, 1, 4, 3};
std::vector<int> P(a.size());
std::partial_sum(a.begin(), a.end(), P.begin());
// P = {2, 7, 8, 12, 15}

// Query: sum of a[1..3] = a[1] + a[2] + a[3]
int range_sum = P[3] - P[0];  // 12 - 2 = 10 βœ“ (5 + 1 + 4 = 10)

// Query: sum of a[0..2]
int range_sum2 = P[2];  // 8 βœ“ (2 + 5 + 1 = 8)

Build: $O(n)$ β€” one call to partial_sum
Query: $O(1)$ per range sum
Use when: Array is static (no updates) and you need many range-sum queries.

1-Indexed Prefix Sum (Cleaner Boundaries)

A common trick to avoid the $l = 0$ special case β€” prepend a zero:

std::vector<int> a{2, 5, 1, 4, 3};
std::vector<int> P(a.size() + 1, 0);  // P[0] = 0 sentinel
std::partial_sum(a.begin(), a.end(), P.begin() + 1);
// P = {0, 2, 7, 8, 12, 15}

// sum(l, r) = P[r+1] - P[l]  β€” always, no special case
int sum_1_3 = P[4] - P[1];  // 12 - 2 = 10 βœ“

Difference Arrays β€” The Inverse of Prefix Sums

If prefix sum transforms $a \to P$, the difference array transforms $P \to a$. They are inverses:

\[d[i] = \begin{cases} a[0] & \text{if } i = 0 \\ a[i] - a[i-1] & \text{if } i > 0 \end{cases}\]

Applying partial_sum to a difference array recovers the original:

std::vector<int> a{2, 5, 1, 4, 3};

// Build difference array
std::vector<int> d(a.size());
std::adjacent_difference(a.begin(), a.end(), d.begin());
// d = {2, 3, -4, 3, -1}

// Recover original
std::vector<int> recovered(a.size());
std::partial_sum(d.begin(), d.end(), recovered.begin());
// recovered = {2, 5, 1, 4, 3} βœ“  (original restored)

Difference array trick for range updates:
To add $v$ to all elements in $a[l..r]$, instead of an $O(n)$ loop, do:

d[l] += v;
if (r + 1 < n) d[r + 1] -= v;

Then reconstruct with partial_sum. This makes batch range updates $O(1)$ each, followed by a single $O(n)$ reconstruction. Essential for problems with many range-update queries.

2D Prefix Sums

The concept extends to 2D grids for $O(1)$ sub-rectangle sum queries:

// Build 2D prefix sum
std::vector<std::vector<int>> grid(n, std::vector<int>(m));
std::vector<std::vector<long long>> P(n + 1, std::vector<long long>(m + 1, 0));

for (int i = 0; i < n; i++)
    for (int j = 0; j < m; j++)
        P[i+1][j+1] = grid[i][j] + P[i][j+1] + P[i+1][j] - P[i][j];

// Query: sum of sub-rectangle (r1,c1) to (r2,c2)  [0-indexed]
auto rect_sum = [&](int r1, int c1, int r2, int c2) -> long long {
    return P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1];
};

This uses inclusion-exclusion and is a staple in grid-based competitive programming problems.

Integer Overflow β€” A Critical Pitfall

⚠️ partial_sum accumulates into the same type as the input elements. If the input is vector<int>, the prefix sums are int β€” and can overflow silently for large inputs.

// DANGEROUS: int overflow for large arrays
std::vector<int> v(200000, 1000000);  // 2Γ—10⁡ elements, each 10⁢
std::vector<int> P(v.size());
std::partial_sum(v.begin(), v.end(), P.begin());
// P.back() should be 2Γ—10ΒΉΒΉ, but int overflows!

// SAFE: use long long output
std::vector<long long> P(v.size());
std::partial_sum(v.begin(), v.end(), P.begin());
// βœ“ partial_sum accumulates using the output iterator's value_type

Fix: Use long long for the output vector when input values or array sizes are large.

C++17 Parallel Alternatives: inclusive_scan & exclusive_scan

C++17 introduced execution-policy-aware scans in <numeric> that supersede partial_sum for parallel workloads:

Function Equivalent to Key Difference
std::inclusive_scan partial_sum (includes current element) Supports execution policies; op must be associative
std::exclusive_scan Prefix sum excluding current element Output shifted right, first element = init
#include <numeric>
#include <execution>  // for parallel policies

std::vector<int> v{3, 1, 4, 1, 5};
std::vector<int> inc(5), exc(5);

// inclusive_scan: same as partial_sum
std::inclusive_scan(v.begin(), v.end(), inc.begin());
// inc = {3, 4, 8, 9, 14}

// exclusive_scan: excludes current element
std::exclusive_scan(v.begin(), v.end(), exc.begin(), 0);
// exc = {0, 3, 4, 8, 9}

// Parallel inclusive scan (requires associative op)
std::inclusive_scan(std::execution::par, v.begin(), v.end(), inc.begin());

⚠️ partial_sum vs inclusive_scan: For partial_sum, the binary op doesn’t need to be associative (it always folds left-to-right). For inclusive_scan with parallel execution, the op must be associative since the work is split across threads. For sequential execution, they behave identically.

transform_inclusive_scan & transform_exclusive_scan

C++17 also provides fused transform-then-scan:

std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> out(5);

// Sum of squares prefix: transform each element to xΒ², then prefix sum
std::transform_inclusive_scan(v.begin(), v.end(), out.begin(),
    std::plus<>(),                    // scan operation
    [](int x) { return x * x; });    // transform
// out = {1, 5, 14, 30, 55}  (1, 1+4, 1+4+9, ...)

Common Applications Summary

Application Technique Complexity
Range sum queries (static) Prefix sum + $O(1)$ lookup Build $O(n)$, Query $O(1)$
Batch range updates Difference array + prefix sum Update $O(1)$, Rebuild $O(n)$
Sub-rectangle sum (2D) 2D prefix sum Build $O(nm)$, Query $O(1)$
Running max/min partial_sum with max/min $O(n)$
Counting frequencies in ranges Prefix sum on frequency array Build $O(n)$, Query $O(1)$
Equilibrium index Prefix sum from both directions $O(n)$
Maximum subarray sum Prefix sum + running minimum $O(n)$

Deep-Dive Practice:

P1. Given an array of $n$ integers and $q$ queries of the form $(l, r)$, answer each query with the sum $a_l + a_{l+1} + \cdots + a_r$ in $O(1)$ per query.

P2. You have an array of $n$ zeros. You receive $q$ updates, each adding value $v$ to all elements in range $[l, r]$. After all updates, output the final array. Solve in $O(n + q)$.

P3. Given a 2D grid of integers, answer $q$ sub-rectangle sum queries in $O(1)$ each after $O(nm)$ preprocessing.

P4. Use partial_sum with a custom operation to compute the prefix GCD of an array. Then answer queries: β€œWhat is $\gcd(a_l, a_{l+1}, \ldots, a_r)$?” Can you do this in $O(1)$? Why or why not?

P5. Explain why std::inclusive_scan with std::execution::par requires an associative operation but std::partial_sum does not. Give an example of a non-associative operation that produces different results.


7.6 String Algorithms

#include <string>
#include <sstream>

std::string s = "Hello World";

// Case conversion
std::transform(s.begin(), s.end(), s.begin(), ::tolower);

// Split string by delimiter
std::istringstream iss("one,two,three");
std::string token;
std::vector<std::string> tokens;
while (std::getline(iss, token, ',')) {
    tokens.push_back(token);
}

// Trim whitespace (C++ doesn't have built-in trim)
auto ltrim = [](std::string& s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(),
        [](char c) { return !std::isspace(c); }));
};

8 β€” Container Complexity Summary

8.1 Master Complexity Table

Operation vector deque list set/map unordered
Access [i] O(1) O(1) O(n) O(log n) O(1) avg
push_back O(1)* O(1)* O(1) β€” β€”
push_front O(n) O(1) O(1) β€” β€”
Insert O(n) O(n) O(1)† O(log n) O(1) avg
Erase O(n) O(n) O(1)† O(log n) O(1) avg
Find O(n) O(n) O(n) O(log n) O(1) avg

* amortized † with iterator

⚠️ Know these complexities by heart. Using the wrong container can turn an $O(n \log n)$ solution into $O(n^2)$. In interviews and contests, container choice is often the deciding factor.


Practice Questions

Q1. Explain the STL architecture. What are the three pillars and how do iterators connect containers and algorithms?

Q2. You need a container that supports O(1) random access, O(1) amortized insertion at the end, and iteration over all elements. Which container do you choose and why?

Q3. When would you use std::map over std::unordered_map? Give three concrete scenarios.

Q4. Write code to find the 5th largest element in a vector of 1 million integers as efficiently as possible. What is the time complexity?

Q5. Explain the remove-erase idiom. Why doesn’t std::remove actually remove elements? How does C++20 improve this?

Q6. What is iterator invalidation? Give examples of operations on std::vector that invalidate iterators. How do you handle this safely?

Q7. Compare push_back vs emplace_back. When does emplace_back provide a real advantage?

Q8. Implement a custom hash function for std::pair<std::string, int> and use it with std::unordered_set.

Q9. Given an array of integers, use STL algorithms to: (a) remove all duplicates, (b) find the median, (c) compute the prefix sum.

Q10. Design a leaderboard system using STL containers that supports: insert score O(log n), get rank O(log n), get top-k O(k). Which containers do you use?


Key Takeaways

  1. std::vector is your default container β€” contiguous memory, cache-friendly
  2. reserve() before bulk push_back β€” avoids costly reallocations
  3. Use emplace_back over push_back β€” constructs in-place, avoids copies
  4. Prefer unordered_map over map unless you need ordering or range queries
  5. Know the complexity of every operation you use β€” it’s the difference between AC and TLE
  6. Use STL algorithms β€” they’re optimized, tested, and readable
  7. Use auto with iterators β€” avoids verbose type names

← Back to C++ Notes