DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Sequence ContainersAssociative Containers — OrderedUnordered Containers — Hash-BasedContainer AdaptersString & `string_view`IteratorsSTL AlgorithmsUtility Types — `pair`, `tuple`, `optional`Custom Comparators & Lambdas in STLChoosing the Right Container — Master Decision Guide
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
02

C++ STL Deep Dive


Chapter 2 ·  Part 1: FOUNDATIONS ·  Est. 37 min read

Chapter Goal: Master every STL container and algorithm that appears in FAANG interviews. By the end of this chapter you will know exactly which container to reach for in any scenario, how it works internally, all operations it supports, and the time complexity of each — with zero hesitation.


2.1 Sequence Containers

Sequence containers store elements in a linear order. The order of insertion is maintained.


vector — The Workhorse of DSA

vector is a dynamically-resizing contiguous array. It is the most frequently used STL container in interview solutions. When in doubt, reach for vector.

Internals

Internally, a vector allocates a raw block of heap memory. It tracks two values: size (elements currently stored) and capacity (space allocated). When size == capacity and you push a new element, the vector allocates a new block — typically 2× the old capacity — copies all elements, then frees the old block. This doubling strategy guarantees amortized O(1) for push_back.

size     = 4   (elements in use)
capacity = 8   (total allocated slots)

[ 10 | 20 | 30 | 40 | __ | __ | __ | __ ]
  ^                    ^
  data                 size boundary

Construction

vector<int> a;                         // Empty vector
vector<int> b(5);                      // 5 elements, all zero-initialized
vector<int> c(5, -1);                  // 5 elements, all -1
vector<int> d = {1, 2, 3, 4, 5};      // Initializer list
vector<int> e(d);                      // Copy of d
vector<int> f(d.begin(), d.begin()+3); // {1,2,3} — range constructor
 
// 2D vector (n×m filled with 0)
int n = 3, m = 4;
vector<vector<int>> grid(n, vector<int>(m, 0));

Core Operations & Complexity

Operation Code Time Notes
Access by index v[i], v.at(i) O(1) at() does bounds check
First / Last element v.front(), v.back() O(1)
Insert at end v.push_back(x), v.emplace_back(x) O(1) amortized
Remove from end v.pop_back() O(1)
Insert at position v.insert(it, x) O(n) Shifts elements right
Erase at position v.erase(it) O(n) Shifts elements left
Erase range v.erase(it1, it2) O(n)
Size v.size() O(1) Returns size_t (unsigned!)
Capacity v.capacity() O(1)
Reserve space v.reserve(n) O(n) Avoids repeated reallocations
Resize v.resize(n) O(n) Pads with 0 if larger
Clear v.clear() O(n) size→0, capacity unchanged
Empty check v.empty() O(1)
Sort sort(v.begin(), v.end()) O(n log n)
Reverse reverse(v.begin(), v.end()) O(n)
#include <bits/stdc++.h>
using namespace std;
 
int main() {
    vector<int> v = {3, 1, 4, 1, 5, 9};
 
    // --- Access ---
    cout << v[0];           // 3 — no bounds check
    cout << v.at(0);        // 3 — throws std::out_of_range if invalid
 
    // --- Modify ---
    v.push_back(2);         // {3,1,4,1,5,9,2}
    v.pop_back();           // {3,1,4,1,5,9}
    v.insert(v.begin(), 0); // {0,3,1,4,1,5,9} — O(n)!
    v.erase(v.begin());     // {3,1,4,1,5,9}   — O(n)!
 
    // --- Info ---
    cout << v.size();       // 6
    cout << v.capacity();   // ≥6 (implementation-defined)
    cout << v.empty();      // 0 (false)
 
    // --- Reserve to avoid reallocations ---
    vector<int> big;
    big.reserve(100000);    // Allocate space upfront — avoids N reallocations
    for (int i = 0; i < 100000; i++) big.push_back(i);
 
    // --- Resize ---
    v.resize(10);           // Extends to size 10, new elements = 0
    v.resize(3);            // Shrinks to first 3 elements
 
    // --- Clear ---
    v.clear();              // v is now empty, but capacity is unchanged
}

The Erase-Remove Idiom

To remove ALL occurrences of a value from a vector in O(n):

vector<int> v = {1, 2, 3, 2, 4, 2, 5};
 
// std::remove shifts non-matching elements to front, returns new end iterator
// v.erase then removes the "garbage" tail
v.erase(remove(v.begin(), v.end(), 2), v.end());
// v = {1, 3, 4, 5}
 
// Remove by condition
v.erase(remove_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; }), v.end());
// Removes all even numbers

Common DSA Patterns with vector

// --- Prefix sum array ---
vector<int> arr = {1, 2, 3, 4, 5};
vector<int> prefix(arr.size() + 1, 0);
for (int i = 0; i < (int)arr.size(); i++) {
    prefix[i+1] = prefix[i] + arr[i];
}
// Sum from index l to r (0-indexed, inclusive):
int l = 1, r = 3;
int rangeSum = prefix[r+1] - prefix[l];   // 2+3+4 = 9
 
// --- Adjacency list for graphs ---
int n = 5;  // 5 nodes
vector<vector<int>> adj(n);
adj[0].push_back(1);
adj[0].push_back(2);
adj[1].push_back(3);
 
// --- DP table ---
vector<vector<int>> dp(n, vector<int>(n, 0));
 
// --- Flatten 2D index to 1D ---
int rows = 4, cols = 5;
int idx = row * cols + col;   // 2D → 1D
int r = idx / cols, c = idx % cols;  // 1D → 2D

array — Fixed-Size Stack Array

std::array is a thin wrapper over a C-style array. Its size is fixed at compile time. It lives on the stack (fast), has no reallocation, and is fully compatible with STL algorithms.

#include <array>
 
array<int, 5> a = {1, 2, 3, 4, 5};  // Size must be a compile-time constant
array<int, 5> b;                     // Uninitialized (unlike vector!)
b.fill(0);                           // Zero all elements
 
cout << a.size();    // 5 (constexpr)
cout << a[2];        // 3
cout << a.front();   // 1
cout << a.back();    // 5
 
// Works with all STL algorithms
sort(a.begin(), a.end());

When to use array vs. vector:

array vector
Size Fixed at compile time Dynamic
Memory Stack Heap
Speed Faster (no indirection) Slightly slower
STL compat Full Full
Use in DSA Direction arrays (dx[], dy[]) Almost everything else
// Classic direction arrays — use array or C-style arrays
const int dx[] = {0, 0,  1, -1};
const int dy[] = {1, -1, 0,  0};
// Or
array<int,4> DX = {0, 0, 1, -1};
array<int,4> DY = {1, -1, 0, 0};

deque — Double-Ended Queue

std::deque (pronounced "deck") supports O(1) push and pop at both ends. Unlike vector, it is not contiguous in memory — it's implemented as a sequence of fixed-size chunks. Random access is O(1) but slightly slower than vector.

#include <deque>
 
deque<int> dq;
 
dq.push_back(3);    // [3]
dq.push_front(1);   // [1, 3]
dq.push_back(5);    // [1, 3, 5]
dq.push_front(0);   // [0, 1, 3, 5]
 
dq.pop_front();     // [1, 3, 5]
dq.pop_back();      // [1, 3]
 
cout << dq.front(); // 1
cout << dq.back();  // 3
cout << dq[0];      // 1 — O(1) random access
cout << dq.size();  // 2

Operations & Complexity:

Operation Time Notes
push_front, push_back O(1) amortized Key advantage
pop_front, pop_back O(1) amortized
operator[], at() O(1) Slightly slower than vector
insert, erase (middle) O(n) Still O(n) like vector

When to use deque:

  • When you need O(1) insertions/removals at both ends.
  • Underlying container for stack and queue.
  • Sliding window problems (though deque is often used directly for monotonic deque).
// Monotonic deque — sliding window maximum (preview of Chapter 9)
deque<int> monoDeque;  // Stores indices
// Maintains decreasing order of values

list and forward_list — Linked Lists in STL

std::list is a doubly linked list. std::forward_list is a singly linked list. Both support O(1) insertion and deletion anywhere given an iterator, but have no random access.

#include <list>
 
list<int> l = {1, 2, 3, 4, 5};
 
l.push_back(6);     // [1,2,3,4,5,6]
l.push_front(0);    // [0,1,2,3,4,5,6]
l.pop_back();       // [0,1,2,3,4,5]
l.pop_front();      // [1,2,3,4,5]
 
auto it = l.begin();
advance(it, 2);         // Move iterator forward 2 steps (O(n) for list)
l.insert(it, 99);       // [1,2,99,3,4,5] — O(1) once we have the iterator
l.erase(it);            // Removes the element at it — O(1)
 
l.reverse();            // Reverses in-place — O(n)
l.sort();               // Sorts in-place — O(n log n) — must use member sort!
l.unique();             // Removes consecutive duplicates
 
// For range-based for:
for (int x : l) cout << x << " ";

Operations & Complexity:

Operation vector list
Random access O(1) Not supported
Push/pop back O(1) amortized O(1)
Push/pop front O(n) O(1)
Insert/erase (given iterator) O(n) O(1)
Memory Contiguous Non-contiguous
Cache performance Excellent Poor

Warning: In DSA interviews, you rarely need list from STL. Most linked list problems involve custom ListNode structs (as given by LeetCode). The STL list is more useful in system programming.


2.2 Associative Containers — Ordered

Ordered associative containers use a Red-Black Tree (a self-balancing BST) internally. All operations are O(log n). Elements are always stored in sorted order by key.


map — Sorted Key-Value Store

std::map<Key, Value> stores (key, value) pairs sorted by key. No duplicate keys allowed.

#include <map>
 
map<string, int> freq;
 
// --- Insert ---
freq["apple"] = 3;               // Assignment (inserts if not present)
freq["banana"] = 1;
freq.insert({"cherry", 5});      // Insert pair
freq.emplace("date", 2);         // Construct in-place (preferred)
 
// --- Access ---
cout << freq["apple"];           // 3 — inserts with default value if missing!
cout << freq.at("apple");        // 3 — throws std::out_of_range if missing
 
// Warning: operator[] is DANGEROUS for lookup — creates entry if key not found!
if (freq.count("elderberry")) {  // ✅ Safe check first
    cout << freq["elderberry"];
}
 
// --- Find ---
auto it = freq.find("banana");
if (it != freq.end()) {
    cout << it->first;           // "banana"
    cout << it->second;          // 1
}
 
// --- Iteration (always sorted by key) ---
for (auto& [key, val] : freq) {
    cout << key << ": " << val << "\n";
}
// Output: apple:3, banana:1, cherry:5, date:2 (alphabetical)
 
// --- Erase ---
freq.erase("apple");             // Remove by key
freq.erase(freq.begin());        // Remove by iterator
 
// --- Size ---
cout << freq.size();             // Number of entries
cout << freq.empty();            // Is it empty?
 
// --- Count (0 or 1 since no duplicates) ---
cout << freq.count("banana");    // 1 (exists), 0 (not found)

Lower/Upper Bound — Unique to Ordered Containers

map<int, string> m = {{1,"a"},{3,"b"},{5,"c"},{7,"d"},{10,"e"}};
 
// lower_bound(k): first key >= k
auto lo = m.lower_bound(4);    // Points to {5,"c"}
cout << lo->first;             // 5
 
// upper_bound(k): first key > k
auto hi = m.upper_bound(5);    // Points to {7,"d"}
cout << hi->first;             // 7
 
// Find range [3, 7]
auto from = m.lower_bound(3);  // {3,"b"}
auto to   = m.upper_bound(7);  // Points past {7,"d"} → to {10,"e"}
for (auto it = from; it != to; ++it) {
    cout << it->first << " ";  // 3 5 7
}

Complexity Summary:

Operation Time
find, count, [], at O(log n)
insert, emplace, erase O(log n)
lower_bound, upper_bound O(log n)
Iteration O(n) total
size, empty O(1)

multimap — Map with Duplicate Keys

multimap<int, string> mm;
mm.insert({1, "one"});
mm.insert({1, "uno"});    // Duplicate key allowed!
mm.insert({2, "two"});
 
cout << mm.count(1);      // 2
 
// Iterate all values for a key
auto [lo, hi] = mm.equal_range(1);
for (auto it = lo; it != hi; ++it) {
    cout << it->second << " ";  // "one" "uno"
}

set — Sorted Unique Elements

std::set<T> is like map but stores only keys (no mapped values). All elements are unique and sorted.

#include <set>
 
set<int> s = {5, 1, 4, 2, 3};  // Stored as: {1, 2, 3, 4, 5}
 
s.insert(3);                    // Already exists — no change
s.insert(6);                    // {1, 2, 3, 4, 5, 6}
s.erase(4);                     // {1, 2, 3, 5, 6}
 
cout << s.count(3);             // 1 (exists)
cout << s.count(4);             // 0 (not found)
 
auto it = s.find(3);
if (it != s.end()) cout << *it; // 3
 
// Lower/upper bound
auto lo = s.lower_bound(3);     // Iterator to 3
auto hi = s.upper_bound(5);     // Iterator past 5 (points to 6)
 
// Iteration — always sorted
for (int x : s) cout << x << " ";  // 1 2 3 5 6
 
// Min and max in O(log n)
cout << *s.begin();             // 1 (smallest)
cout << *s.rbegin();            // 6 (largest)

DSA Patterns with set:

// --- Check duplicates efficiently ---
vector<int> nums = {1, 2, 3, 2, 4};
set<int> seen(nums.begin(), nums.end());
bool hasDuplicate = seen.size() != nums.size();  // true
 
// --- Ordered unique elements ---
set<int> unique_set(v.begin(), v.end());
 
// --- k-th smallest element (not built in, but we can count) ---
// For frequent k-th queries, use an Order Statistics Tree or sorted vector
 
// --- Find closest value in a set ---
set<int> s = {1, 5, 10, 20};
int target = 7;
auto it = s.lower_bound(target);   // ≥ target: points to 10
// Check predecessor
if (it != s.begin()) {
    auto prev_it = prev(it);       // 5
    // Compare *prev_it and *it with target
}

multiset — Sorted Elements with Duplicates

multiset<int> ms = {1, 2, 2, 3, 3, 3};
cout << ms.count(3);     // 3
 
ms.erase(ms.find(3));    // ✅ Removes only ONE occurrence of 3
ms.erase(3);             // ❌ Removes ALL occurrences of 3!

Warning: Critical multiset Pitfall: erase(value) removes ALL elements with that value. To remove just one, use erase(find(value)).


2.3 Unordered Containers — Hash-Based

Unordered containers use a hash table internally. Average case is O(1) for most operations, but worst case (hash collisions) is O(n). Elements are stored in no particular order.


unordered_map — Hash Map

The most important container in interview problem-solving after vector. Use it for frequency counting, memoization, grouping, and fast lookups.

#include <unordered_map>
 
unordered_map<string, int> freq;
 
// --- Insert / Update ---
freq["apple"]++;               // Inserts with 0, then increments
freq["banana"] = 5;
freq.insert({"cherry", 3});
freq.emplace("date", 2);
 
// --- Access ---
cout << freq["apple"];         // 1 (use carefully — inserts if absent!)
auto it = freq.find("banana");
if (it != freq.end()) {
    cout << it->second;        // 5
}
 
// --- Check existence ---
cout << freq.count("apple");   // 1 (exists), 0 (absent)
cout << freq.contains("apple"); // C++20: true/false
 
// --- Erase ---
freq.erase("apple");
 
// --- Iteration (order not guaranteed) ---
for (auto& [key, val] : freq) {
    cout << key << ": " << val << "\n";
}
 
// --- Useful info ---
cout << freq.size();           // Number of entries
cout << freq.load_factor();    // Elements / Buckets
freq.reserve(1000);            // Pre-size to avoid rehashing

Frequency Count Pattern (Most Common):

vector<int> nums = {1, 2, 2, 3, 3, 3, 4};
unordered_map<int, int> cnt;
for (int x : nums) cnt[x]++;
// cnt = {1:1, 2:2, 3:3, 4:1}
 
// Find element with max frequency
int maxFreq = 0, maxElem = -1;
for (auto& [elem, freq] : cnt) {
    if (freq > maxFreq) {
        maxFreq = freq;
        maxElem = elem;
    }
}
// maxElem = 3, maxFreq = 3

Complexity:

Operation Average Worst Case
find, count, [], at O(1) O(n)
insert, emplace, erase O(1) O(n)
Iteration O(n) O(n)
size, empty O(1) O(1)

Warning: Why can worst case be O(n)? If many keys hash to the same bucket (collision), operations degrade to O(n). In FAANG interviews this is rarely an issue, but anti-hash tests in competitive programming can trigger it. Use map instead if adversarial inputs are expected.


unordered_set — Hash Set

#include <unordered_set>
 
unordered_set<int> s;
 
s.insert(1);
s.insert(2);
s.insert(1);         // Duplicate — ignored
 
cout << s.count(1);  // 1
cout << s.count(5);  // 0
 
s.erase(2);
 
// --- Pattern: O(1) duplicate detection ---
vector<int> nums = {1, 2, 3, 4, 5, 3};
unordered_set<int> seen;
for (int x : nums) {
    if (seen.count(x)) {
        cout << x << " is a duplicate\n";
        break;
    }
    seen.insert(x);
}
 
// --- Pattern: Two Sum ---
vector<int> arr = {2, 7, 11, 15};
int target = 9;
unordered_set<int> visited;
for (int x : arr) {
    if (visited.count(target - x)) {
        cout << "Found: " << x << " and " << target - x << "\n";
    }
    visited.insert(x);
}

map vs. unordered_map — When to Use Which

Criterion map unordered_map
Time (avg) O(log n) O(1)
Time (worst) O(log n) O(n)
Key order Sorted Not sorted
lower_bound / upper_bound Available Not available
Custom key type Needs operator< Needs hash function
Memory Lower Higher (hash table overhead)
Use when Need sorted order, range queries, predecessor/successor Pure lookup, frequency count, memoization

2.4 Container Adapters

Adapters wrap a sequence container to provide a restricted interface. They do not expose iterators.


stack — LIFO

Adapts deque by default. Only allows access to the top element.

#include <stack>
 
stack<int> st;
 
st.push(1);         // [1]
st.push(2);         // [1, 2]
st.push(3);         // [1, 2, 3]
 
cout << st.top();   // 3 (last pushed)
st.pop();           // [1, 2] — removes top, returns void
cout << st.top();   // 2
cout << st.size();  // 2
cout << st.empty(); // false
 
// Warning: Always check empty() before top() or pop()!
while (!st.empty()) {
    cout << st.top();
    st.pop();
}

Using stack for DFS (iterative):

// Iterative DFS on a graph
stack<int> dfsStack;
vector<bool> visited(n, false);
 
dfsStack.push(start);
while (!dfsStack.empty()) {
    int node = dfsStack.top();
    dfsStack.pop();
    if (visited[node]) continue;
    visited[node] = true;
    // process node
    for (int neighbor : adj[node]) {
        if (!visited[neighbor])
            dfsStack.push(neighbor);
    }
}

Monotonic Stack Template (One of the Most Important Patterns):

// Find the Next Greater Element for each position
vector<int> nums = {2, 1, 2, 4, 3};
int n = nums.size();
vector<int> nge(n, -1);    // Next Greater Element (-1 if none)
 
stack<int> st;             // Stores indices
for (int i = 0; i < n; i++) {
    while (!st.empty() && nums[st.top()] < nums[i]) {
        nge[st.top()] = nums[i];
        st.pop();
    }
    st.push(i);
}
// nge = [4, 2, 4, -1, -1]

queue — FIFO

Adapts deque by default. Only allows access to front (oldest) and back (newest).

#include <queue>
 
queue<int> q;
 
q.push(1);          // [1]
q.push(2);          // [1, 2]
q.push(3);          // [1, 2, 3]
 
cout << q.front();  // 1 (first in)
cout << q.back();   // 3 (last in)
q.pop();            // Removes front → [2, 3]
cout << q.front();  // 2
cout << q.size();   // 2
cout << q.empty();  // false

BFS Template using queue:

// BFS on a graph — finds shortest path in unweighted graph
vector<int> bfs(int start, int n, vector<vector<int>>& adj) {
    vector<int> dist(n, -1);
    queue<int> q;
 
    dist[start] = 0;
    q.push(start);
 
    while (!q.empty()) {
        int node = q.front();
        q.pop();
 
        for (int neighbor : adj[node]) {
            if (dist[neighbor] == -1) {      // Not visited
                dist[neighbor] = dist[node] + 1;
                q.push(neighbor);
            }
        }
    }
    return dist;
}

priority_queue — Heap

Adapts vector by default. Maintains a max-heap — the largest element is always at the top. This is the go-to data structure for:

  • K largest/smallest problems
  • Dijkstra's shortest path
  • Median of a stream
  • Scheduling problems
#include <queue>
 
// --- MAX-HEAP (default) ---
priority_queue<int> maxPQ;
maxPQ.push(3);
maxPQ.push(1);
maxPQ.push(4);
maxPQ.push(1);
maxPQ.push(5);
 
cout << maxPQ.top();   // 5 (largest)
maxPQ.pop();
cout << maxPQ.top();   // 4
 
// --- MIN-HEAP using greater<T> ---
priority_queue<int, vector<int>, greater<int>> minPQ;
minPQ.push(3);
minPQ.push(1);
minPQ.push(4);
 
cout << minPQ.top();   // 1 (smallest)
 
// --- Pairs in priority_queue (sorted by first element) ---
priority_queue<pair<int,int>> pq;  // Max by first element
pq.push({3, 10});
pq.push({1, 20});
pq.push({5, 5});
 
auto [dist, node] = pq.top();  // {5, 5}
pq.pop();

Custom Comparator (Lambda):

// Min-heap ordered by second element of pair
auto cmp = [](const pair<int,int>& a, const pair<int,int>& b) {
    return a.second > b.second;  // Note: > for min-heap
};
priority_queue<pair<int,int>,
               vector<pair<int,int>>,
               decltype(cmp)> customPQ(cmp);
 
customPQ.push({1, 30});
customPQ.push({2, 10});
customPQ.push({3, 20});
 
cout << customPQ.top().second;   // 10 (minimum second value)

Dijkstra's Algorithm using priority_queue:

vector<int> dijkstra(int src, int n, vector<vector<pair<int,int>>>& adj) {
    vector<int> dist(n, INT_MAX);
    priority_queue<pair<int,int>,
                   vector<pair<int,int>>,
                   greater<>> minPQ;  // {distance, node}
 
    dist[src] = 0;
    minPQ.push({0, src});
 
    while (!minPQ.empty()) {
        auto [d, u] = minPQ.top();
        minPQ.pop();
 
        if (d > dist[u]) continue;  // Stale entry
 
        for (auto [w, v] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                minPQ.push({dist[v], v});
            }
        }
    }
    return dist;
}

Operations & Complexity:

Operation Time
push O(log n)
pop O(log n)
top O(1)
size, empty O(1)
Build from n elements O(n)

Key fact for interviews: Building a heap from n elements using make_heap or constructing a priority_queue from a range is O(n), not O(n log n). This can matter in some optimal solutions.


2.5 String & string_view

std::string is one of the most frequently manipulated types in interviews. Master every operation.

#include <string>
 
string s = "hello world";
 
// --- Size & Access ---
cout << s.length();          // 11 (or s.size() — same)
cout << s.empty();           // false
cout << s[0];                // 'h'
cout << s.front();           // 'h'
cout << s.back();            // 'd'
 
// --- Modification ---
s.push_back('!');            // "hello world!"
s.pop_back();                // "hello world"
s += " again";               // "hello world again"
s.append(" more");           // "hello world again more"
s.insert(5, " dear");        // Inserts at index 5
s.erase(5, 5);               // Erases 5 characters starting at index 5
s.replace(0, 5, "bye");      // Replaces first 5 chars with "bye"
 
// --- Substring ---
string sub = s.substr(0, 5);  // First 5 characters
string end = s.substr(6);     // From index 6 to end
 
// --- Search ---
size_t pos = s.find("world");     // First occurrence, npos if not found
size_t rpos = s.rfind("l");       // Last occurrence of 'l'
if (pos != string::npos) {
    cout << "Found at " << pos;
}
 
// Find first character in/not in a set
size_t p1 = s.find_first_of("aeiou");    // First vowel
size_t p2 = s.find_first_not_of("aeiou"); // First non-vowel
 
// --- Comparison ---
if (s == "hello") { ... }
if (s < "world") { ... }   // Lexicographic
int cmp = s.compare("hello");  // 0 if equal, <0 if less, >0 if greater
 
// --- Conversion ---
string ns = to_string(42);      // "42"
int n = stoi("42");              // 42
long long ll = stoll("123456789012");
 
// --- Case manipulation ---
for (char& c : s) c = tolower(c);   // To lowercase
for (char& c : s) c = toupper(c);   // To uppercase
 
// --- Reverse ---
reverse(s.begin(), s.end());
 
// --- Split by delimiter (no built-in — use this pattern) ---
string line = "a,b,c,d";
vector<string> parts;
stringstream ss(line);
string token;
while (getline(ss, token, ',')) {
    parts.push_back(token);
}
// parts = {"a", "b", "c", "d"}

Critical String Patterns:

// --- Palindrome check ---
bool isPalindrome(const string& s) {
    int l = 0, r = s.size() - 1;
    while (l < r) {
        if (s[l] != s[r]) return false;
        l++; r--;
    }
    return true;
}
 
// --- Anagram check ---
bool isAnagram(const string& s, const string& t) {
    if (s.size() != t.size()) return false;
    int freq[26] = {};
    for (char c : s) freq[c - 'a']++;
    for (char c : t) freq[c - 'a']--;
    for (int f : freq) if (f != 0) return false;
    return true;
}
 
// --- Build string efficiently (avoid O(n²) concatenation) ---
// ❌ Slow: O(n²) due to repeated allocation
string result = "";
for (int i = 0; i < n; i++) result += chars[i];
 
// ✅ Fast: use ostringstream or reserve
string result;
result.reserve(n);   // Reserve space upfront
for (int i = 0; i < n; i++) result += chars[i];   // Now O(n) amortized

string_view — Non-Owning String Reference (C++17)

#include <string_view>
 
// string_view is a lightweight, read-only reference to a string
// No copying — very fast for function parameters
 
void printPrefix(string_view sv, int len) {
    cout << sv.substr(0, len);  // No copy!
}
 
string s = "hello world";
printPrefix(s, 5);    // "hello"
printPrefix("hello world", 5);  // Works with string literals too
 
// Use string_view for read-only string parameters instead of const string&
// It can be constructed from both std::string and char* without copying

2.6 Iterators

Iterators are generalized pointers into a container. Understanding them is essential for using STL algorithms correctly.

Iterator Categories

Category Supports Containers
Input Read, advance istream_iterator
Output Write, advance ostream_iterator
Forward Read/write, advance forward_list, unordered_*
Bidirectional All above + -- list, set, map
Random Access All above + +n, -n, [], < vector, deque, array

Basic Iterator Usage

vector<int> v = {1, 2, 3, 4, 5};
 
// --- Forward iteration ---
for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";    // Dereference to get value
}
 
// --- Reverse iteration ---
for (auto it = v.rbegin(); it != v.rend(); ++it) {
    cout << *it << " ";    // 5 4 3 2 1
}
 
// --- Const iterators (read-only) ---
for (auto it = v.cbegin(); it != v.cend(); ++it) {
    // *it = 10;  ← ❌ Compile error: can't modify through const_iterator
}
 
// --- Iterator arithmetic (random-access only) ---
auto it = v.begin();
it += 2;               // Advance 2 steps: points to 3
cout << *it;           // 3
cout << *(it + 1);     // 4
cout << it - v.begin();// 2 (index)
 
// --- Useful iterator functions ---
auto mid = v.begin();
advance(mid, v.size()/2);      // advance() works for ALL iterators (O(n) for non-RA)
 
auto next_it = next(v.begin(), 2);      // Returns iterator 2 steps ahead
auto prev_it = prev(v.end(), 1);        // Returns iterator 1 step back
auto gap = distance(v.begin(), v.end()); // = 5 (O(n) for non-RA, O(1) for RA)

Iterators in STL Algorithms

vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
 
// Sort the entire vector
sort(v.begin(), v.end());
 
// Sort only elements from index 2 to 5 (inclusive)
sort(v.begin() + 2, v.begin() + 6);
 
// Find element
auto it = find(v.begin(), v.end(), 5);
if (it != v.end()) cout << "Found at index " << distance(v.begin(), it);
 
// Lower bound on sorted range
sort(v.begin(), v.end());
auto lb = lower_bound(v.begin(), v.end(), 4);  // First element >= 4
cout << *lb;    // 4
cout << distance(v.begin(), lb);    // Index of first ≥4 element

Insert Iterators

vector<int> src = {1, 2, 3};
vector<int> dst = {10, 20, 30};
 
// back_inserter: calls push_back
copy(src.begin(), src.end(), back_inserter(dst));
// dst = {10, 20, 30, 1, 2, 3}
 
// front_inserter: calls push_front (for deque/list)
deque<int> dq = {4, 5, 6};
copy(src.begin(), src.end(), front_inserter(dq));
// dq = {3, 2, 1, 4, 5, 6}

2.7 STL Algorithms

All STL algorithms live in <algorithm>. They operate on ranges defined by [begin, end) iterators. #include <bits/stdc++.h> brings them all in.


Sorting & Ordering

vector<int> v = {5, 3, 1, 4, 2};
 
// --- std::sort — O(n log n), NOT stable ---
sort(v.begin(), v.end());                    // Ascending: {1,2,3,4,5}
sort(v.begin(), v.end(), greater<int>());    // Descending: {5,4,3,2,1}
sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // Same
 
// --- std::stable_sort — O(n log n), STABLE (preserves equal element order) ---
vector<pair<int,int>> pairs = {{3,1},{1,2},{3,2},{1,1}};
stable_sort(pairs.begin(), pairs.end());  // Stable by first element
 
// --- std::partial_sort — Sort only the first k elements: O(n log k) ---
partial_sort(v.begin(), v.begin() + 3, v.end());  // First 3 are sorted
 
// --- std::nth_element — Partition such that v[k] is what it would be: O(n) avg ---
nth_element(v.begin(), v.begin() + 2, v.end());  // v[2] is the median
 
// --- std::sort on custom struct ---
struct Job { int start, end, weight; };
vector<Job> jobs = {{0,6,60},{1,4,20},{3,5,30},{3,8,30}};
sort(jobs.begin(), jobs.end(), [](const Job& a, const Job& b) {
    return a.end < b.end;   // Sort by end time (Greedy interval scheduling)
});

Binary Search on Sorted Ranges

These all require the range to be sorted first.

vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
 
// --- binary_search: returns true/false only ---
cout << binary_search(v.begin(), v.end(), 5);  // true
cout << binary_search(v.begin(), v.end(), 9);  // false
 
// --- lower_bound: first element >= target ---
auto lb = lower_bound(v.begin(), v.end(), 4);
cout << *lb;                        // 4
cout << lb - v.begin();             // Index: 3
 
// --- upper_bound: first element > target ---
auto ub = upper_bound(v.begin(), v.end(), 4);
cout << *ub;                        // 5
cout << ub - v.begin();             // Index: 4
 
// --- Count occurrences of a value in sorted array ---
vector<int> sv = {1, 2, 2, 2, 3, 4};
int count = upper_bound(sv.begin(), sv.end(), 2)
          - lower_bound(sv.begin(), sv.end(), 2);  // 3
 
// --- equal_range: returns {lower_bound, upper_bound} ---
auto [lo, hi] = equal_range(sv.begin(), sv.end(), 2);
int cnt = hi - lo;   // 3

The Key Insight — lower_bound as an Index:

// In competitive programming, lower_bound on a sorted vector
// gives you the index of the first element >= x.
// Very useful for coordinate compression and LIS (O(n log n)).
 
// Coordinate compression example:
vector<int> arr = {100, 200, 50, 300, 50};
vector<int> sorted_unique = arr;
sort(sorted_unique.begin(), sorted_unique.end());
sorted_unique.erase(unique(sorted_unique.begin(), sorted_unique.end()),
                    sorted_unique.end());
// sorted_unique = {50, 100, 200, 300}
 
// Map each value to 0-indexed rank
for (int& x : arr) {
    x = lower_bound(sorted_unique.begin(), sorted_unique.end(), x)
        - sorted_unique.begin();
}
// arr = {1, 2, 0, 3, 0}

Searching & Querying

vector<int> v = {1, 2, 3, 4, 5, 3, 2};
 
// --- find: first occurrence — O(n) ---
auto it = find(v.begin(), v.end(), 3);   // Points to first 3
 
// --- find_if: first matching condition ---
auto it2 = find_if(v.begin(), v.end(), [](int x){ return x > 3; });
cout << *it2;   // 4
 
// --- count: number of occurrences ---
cout << count(v.begin(), v.end(), 3);   // 2
 
// --- count_if: count matching condition ---
cout << count_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; });  // 3
 
// --- min/max element ---
cout << *min_element(v.begin(), v.end());  // 1
cout << *max_element(v.begin(), v.end());  // 5
 
auto [minIt, maxIt] = minmax_element(v.begin(), v.end());
cout << *minIt << " " << *maxIt;           // 1 5
 
// --- Boolean queries ---
bool allPos  = all_of(v.begin(), v.end(), [](int x){ return x > 0; });   // true
bool anyEven = any_of(v.begin(), v.end(), [](int x){ return x % 2 == 0; }); // true
bool noneNeg = none_of(v.begin(), v.end(), [](int x){ return x < 0; });  // true

Modifying Algorithms

vector<int> v = {1, 2, 3, 4, 5};
 
// --- reverse --- O(n)
reverse(v.begin(), v.end());    // {5,4,3,2,1}
 
// --- fill: set all elements to a value ---
fill(v.begin(), v.end(), 0);    // {0,0,0,0,0}
fill(v.begin(), v.begin()+3, -1);  // First 3 become -1
 
// --- iota: fill with incrementing values ---
iota(v.begin(), v.end(), 1);    // {1,2,3,4,5} (starting from 1)
 
// --- transform: apply function to each element ---
transform(v.begin(), v.end(), v.begin(), [](int x){ return x * 2; });
// v = {2,4,6,8,10}
 
// Transform two ranges into one
vector<int> a = {1,2,3}, b = {4,5,6}, res(3);
transform(a.begin(), a.end(), b.begin(), res.begin(),
          [](int x, int y){ return x + y; });
// res = {5,7,9}
 
// --- replace ---
replace(v.begin(), v.end(), 4, 99);  // Replace all 4s with 99
 
// --- unique: remove consecutive duplicates (sort first!) ---
vector<int> dup = {1,1,2,2,3,3,1};
sort(dup.begin(), dup.end());        // {1,1,1,2,2,3,3}
auto newEnd = unique(dup.begin(), dup.end());  // {1,2,3,?,?,?,?}
dup.erase(newEnd, dup.end());        // {1,2,3}
 
// --- rotate ---
vector<int> r = {1,2,3,4,5};
rotate(r.begin(), r.begin()+2, r.end());  // {3,4,5,1,2} (rotate left by 2)
 
// --- copy ---
vector<int> src = {1,2,3}, dst(3);
copy(src.begin(), src.end(), dst.begin());
 
// --- shuffle ---
vector<int> deck(52);
iota(deck.begin(), deck.end(), 0);
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
shuffle(deck.begin(), deck.end(), rng);

Numeric Algorithms (<numeric>)

#include <numeric>
 
vector<int> v = {1, 2, 3, 4, 5};
 
// --- accumulate: sum (or any fold operation) ---
int sum = accumulate(v.begin(), v.end(), 0);         // 15
int product = accumulate(v.begin(), v.end(), 1,
              [](int acc, int x){ return acc * x; });  // 120
 
// --- partial_sum: running totals ---
vector<int> ps(v.size());
partial_sum(v.begin(), v.end(), ps.begin());  // {1,3,6,10,15}
 
// --- gcd / lcm (C++17) ---
cout << __gcd(12, 8);         // 4 — classic C way
cout << gcd(12, 8);           // 4 — C++17 <numeric>
cout << lcm(4, 6);            // 12 — C++17 <numeric>

Permutation Algorithms

vector<int> v = {1, 2, 3};
 
// --- next_permutation: advances to next lexicographic permutation ---
do {
    for (int x : v) cout << x;
    cout << "\n";
} while (next_permutation(v.begin(), v.end()));
// Prints all 6 permutations: 123, 132, 213, 231, 312, 321
 
// --- prev_permutation: goes to previous permutation ---
vector<int> w = {3, 2, 1};
prev_permutation(w.begin(), w.end());  // {3, 1, 2}
 
// Warning: next_permutation starts from current arrangement — sort first for all perms
sort(v.begin(), v.end());
do { ... } while (next_permutation(v.begin(), v.end()));

2.8 Utility Types — pair, tuple, optional

pair

#include <utility>
 
// Construction
pair<int, string> p1 = {42, "hello"};
pair<int, string> p2 = make_pair(42, "hello");  // Same
auto p3 = make_pair(42, "hello");               // auto deduction
 
// Access
cout << p1.first;   // 42
cout << p1.second;  // "hello"
 
// Structured binding (C++17)
auto [num, str] = p1;
 
// Comparison: lexicographic (by first, then second)
pair<int,int> a = {1, 3};
pair<int,int> b = {1, 5};
cout << (a < b);    // true (same first, compare second)
 
// Common DSA usage
vector<pair<int,int>> edges;     // Weighted graph edges: {weight, node}
map<int, pair<int,int>> coords;  // Map from id to (x,y) coordinate
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> minPQ;

tuple

#include <tuple>
 
// Construction
tuple<int, string, double> t1 = {1, "hello", 3.14};
auto t2 = make_tuple(1, "hello", 3.14);
 
// Access — by index (compile-time!)
cout << get<0>(t1);   // 1
cout << get<1>(t1);   // "hello"
cout << get<2>(t1);   // 3.14
 
// Structured binding (C++17) — much cleaner
auto [id, name, score] = t1;
 
// Comparison: lexicographic
tuple<int,int,int> a = {1, 2, 3};
tuple<int,int,int> b = {1, 2, 4};
cout << (a < b);   // true
 
// DSA usage: edges with source, destination, weight
vector<tuple<int,int,int>> edges;  // {u, v, weight}
edges.emplace_back(0, 1, 5);
edges.emplace_back(1, 2, 3);
 
// Sort edges by weight (for Kruskal's MST)
sort(edges.begin(), edges.end(), [](const auto& a, const auto& b) {
    return get<2>(a) < get<2>(b);
});

optional (C++17)

#include <optional>
 
// Represents a value that may or may not be present
optional<int> findFirst(const vector<int>& v, int target) {
    for (int i = 0; i < (int)v.size(); i++) {
        if (v[i] == target) return i;   // Return found index
    }
    return nullopt;   // Return "nothing"
}
 
auto result = findFirst({3, 1, 4, 1, 5}, 4);
if (result.has_value()) {
    cout << result.value();   // 2
    // Or shorthand:
    cout << *result;          // 2
}

2.9 Custom Comparators & Lambdas in STL

Custom comparators control ordering in sort, set, map, and priority_queue. Mastery of this is essential for greedy problems, interval scheduling, and graph algorithms.

The Comparator Contract

A comparator cmp(a, b) must return true if and only if a should come before b (i.e., a is "less than" b by your ordering). This is called a Strict Weak Ordering.

cmp(a, a) must be false           (irreflexivity)
if cmp(a, b) then !cmp(b, a)      (asymmetry)
if cmp(a, b) && cmp(b, c) then cmp(a, c) (transitivity)

Violating these causes undefined behavior (likely a crash or infinite loop).

// ❌ Wrong: using <= instead of < causes undefined behavior in sort!
sort(v.begin(), v.end(), [](int a, int b) { return a <= b; });
 
// ✅ Correct: strict <
sort(v.begin(), v.end(), [](int a, int b) { return a < b; });

Comparators for sort

// Sort by multiple criteria
struct Student {
    string name;
    int grade;
    double gpa;
};
vector<Student> students;
 
// Sort: by grade descending, then gpa descending, then name ascending
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
    if (a.grade != b.grade) return a.grade > b.grade;  // Grade desc
    if (a.gpa != b.gpa)     return a.gpa > b.gpa;      // GPA desc
    return a.name < b.name;                             // Name asc
});
 
// Shorthand using tie (lexicographic)
sort(students.begin(), students.end(), [](const Student& a, const Student& b) {
    return tie(b.grade, b.gpa, a.name) < tie(a.grade, a.gpa, b.name);
    //     ^ swapped for desc          ^ normal for asc
});

Comparators for set and map

// Custom-ordered set
auto cmp = [](int a, int b) { return abs(a) < abs(b); };  // Sort by absolute value
set<int, decltype(cmp)> absSet(cmp);
absSet.insert(-3);
absSet.insert(1);
absSet.insert(-2);
// Iteration order: 1, -2, -3 (by absolute value)
 
// Custom-keyed map (key comparison)
struct Point { int x, y; };
auto ptCmp = [](const Point& a, const Point& b) {
    return a.x < b.x || (a.x == b.x && a.y < b.y);
};
map<Point, string, decltype(ptCmp)> ptMap(ptCmp);

Comparators for priority_queue

The critical rule: In sort, cmp(a, b) = true means a comes first. In priority_queue, cmp(a, b) = true means b has higher priority (b is closer to the top). This is because the comparator defines the "less-than" relation, and the top is the greatest element.

// MAX-heap (default behavior):
// priority_queue<int> is equivalent to:
priority_queue<int, vector<int>, less<int>> maxPQ;
// less<int>(a, b) = a < b: elements where a < b → b is on top
 
// MIN-heap:
priority_queue<int, vector<int>, greater<int>> minPQ;
// greater<int>(a, b) = a > b: elements where a > b → b is on top (b is smaller)
 
// Custom: min-heap ordered by distance (first element of pair)
auto distCmp = [](const pair<int,int>& a, const pair<int,int>& b) {
    return a.first > b.first;   // a "less than" b when a.first > b.first
                                // → b (smaller distance) ends up on top
};
priority_queue<pair<int,int>,
               vector<pair<int,int>>,
               decltype(distCmp)> distMinPQ(distCmp);

2.10 Choosing the Right Container — Master Decision Guide

Use this guide every time you start a problem.

What do I need to do with the data?
│
├─ Simple sequence, indexed by position?
│   ├─ Size known at compile time? → array<T, N>
│   └─ Dynamic size?               → vector<T>
│
├─ Fast push/pop at BOTH ends?
│   └─ → deque<T>
│
├─ Key → Value mapping?
│   ├─ Need sorted order / range queries? → map<K,V>  (O(log n))
│   └─ Need pure O(1) lookup?             → unordered_map<K,V>
│
├─ Unique elements only?
│   ├─ Need sorted order / lower_bound?  → set<T>    (O(log n))
│   └─ Need pure O(1) membership check?  → unordered_set<T>
│
├─ Duplicates allowed, need sorted order? → multiset<T>
│
├─ LIFO (undo, DFS, matching brackets)?  → stack<T>
│
├─ FIFO (BFS, scheduling)?               → queue<T>
│
└─ Always need min or max quickly?
    ├─ Need largest? → priority_queue<T>           (max-heap)
    └─ Need smallest? → priority_queue<T, vector<T>, greater<T>> (min-heap)

Complexity Cheat Sheet — All Containers

Container Access Search Insert (end) Insert (mid) Delete
vector O(1) O(n) O(1) amort. O(n) O(n)
array O(1) O(n) N/A (fixed) N/A N/A
deque O(1) O(n) O(1) amort. O(n) O(n)
list O(n) O(n) O(1) O(1)* O(1)*
set N/A O(log n) O(log n) O(log n) O(log n)
map O(log n) O(log n) O(log n) O(log n) O(log n)
unordered_set N/A O(1) avg O(1) avg O(1) avg O(1) avg
unordered_map O(1) avg O(1) avg O(1) avg O(1) avg O(1) avg
stack O(1) top N/A O(1) push N/A O(1) pop
queue O(1) front/back N/A O(1) push N/A O(1) pop
priority_queue O(1) top N/A O(log n) push N/A O(log n) pop

* O(1) given an iterator to the position.


← PreviousChapter 1: C++ Essentials for DSANext →Chapter 3: Complexity Analysis
Chapter 2 — Progress
0 / 36 COMPLETE
Sequence Containers
  • Know the internal memory layout of `vector` (contiguous, doubling capacity)
  • Use `reserve()` when the final size is known upfront to avoid reallocations
  • Use the erase-remove idiom to delete all occurrences of a value in O(n)
  • Know when `deque` is better than `vector` (O(1) front push/pop)
  • Understand why `list` is rarely used in interviews (poor cache, no random access)
  • Build a 2D grid as `vector<vector<int>>` with correct initialization syntax
Ordered Associative Containers
  • Use `map::find()` instead of `map::operator[]` for safe lookup
  • Know `lower_bound()` and `upper_bound()` on `map` and `set`
  • Use `set` to find the closest value to a target (check `lower_bound` and `prev`)
  • Know the `multiset` pitfall: `erase(val)` removes ALL — use `erase(find(val))` for one
  • Know that `map` and `set` iteration is always in sorted key order
Unordered Containers
  • Know when to prefer `unordered_map` over `map` (pure O(1) lookup, no order needed)
  • Know the three common patterns: frequency count, two-sum, duplicate detection
  • Use `reserve()` on `unordered_map` to avoid rehashing during bulk inserts
  • Know that worst-case is O(n) due to hash collisions (use `map` if adversarial)
Container Adapters
  • Always check `empty()` before calling `top()` or `front()` / `back()`
  • Know that `priority_queue` is a max-heap by default
  • Build a min-heap using `greater<T>` or a custom comparator
  • Write a BFS template using `queue` and a DFS template using `stack`
  • Write Dijkstra's shortest path using `priority_queue<pair<int,int>, vector<...>, greater<>>`
String
  • Know `find()` returns `string::npos` when not found — always check!
  • Know `substr(pos, len)` syntax and that `len` is length, not end index
  • Split a string by delimiter using `stringstream` + `getline`
  • Build strings efficiently using `reserve()` or `ostringstream`
  • Convert char to int with `c - '0'` and letter index with `c - 'a'`
Iterators & Algorithms
  • Know all four iterator types: `begin/end`, `rbegin/rend`, `cbegin/cend`
  • Use `advance()`, `next()`, `prev()`, `distance()` correctly
  • Use `lower_bound` / `upper_bound` on sorted vectors for O(log n) search
  • Use erase-remove to remove elements matching a condition
  • Use `accumulate` for sum, product, or any fold operation
  • Know `next_permutation` for generating all permutations (sort first!)
  • Use `iota` to fill a vector with consecutive values
Comparators
  • Know the strict weak ordering contract (irreflexivity, asymmetry, transitivity)
  • Understand the direction flip: sort uses `<`, priority_queue needs `>` for min-heap
  • Sort a `vector<struct>` by multiple fields using a lambda
  • Build a custom-ordered `set` or `map` using a lambda comparator
Notes▸