Complete Notes — From Zero to Solving Problems
How to use these notes: Read each section once top to bottom. Then close the notes and try to recall the patterns. Come back only to verify. Retention comes from recall, not re-reading.
1. The Mindset
This is not a general C++ course. The goal is one thing:
Algorithm idea → immediate, clean C++ implementation
Every concept in these notes exists to eliminate the friction between knowing how to solve a problem and actually writing the code. If a topic does not appear in DSA solutions, it is not here.
Two rules to follow while studying:
- After reading a concept, type it out yourself. Don’t copy-paste. Ever.
- When you see a new pattern in someone else’s solution, immediately connect it back to a section here.
2. Boilerplate
Every competitive programming solution starts with the same skeleton. Memorize this. It should take under 10 seconds to type from scratch.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// your code here
return 0;
}
Why each line exists:
#include <bits/stdc++.h> — includes every standard library header at once. You never need to include <vector>, <map>, <algorithm> separately. Only works in GCC (standard in competitive programming).
using namespace std; — lets you write vector instead of std::vector, cout instead of std::cout everywhere.
ios::sync_with_stdio(false) — by default, C++ syncs its I/O with C’s I/O. This disables that sync. Result: cin and cout become 5–10x faster. Critical for problems with large input.
cin.tie(nullptr) — unties cin from cout. Without this, every time you use cin, it first flushes cout. Disabling this saves unnecessary flushes.
One rule: After adding these lines, never mix scanf/printf with cin/cout. Pick one system and stay with it.
Useful constants — define these at the top:
const int INF = 1e9; // safe "infinity" for int
const int MOD = 1e9 + 7; // standard modulo in problems
const int MAXN = 1e5 + 5; // max array size + buffer
// When int isn't enough
long long big = 1e18; // ~9.2 × 10^18
3. Block 1 — C++ Features That Reduce Friction
These are not extra features. They are the default way experienced programmers write C++ for DSA. You will use every single one of these in almost every solution you write.
3.1 auto — Stop Writing Long Type Names
auto tells the compiler to figure out the type. The type is still there — you just don’t have to type it.
auto x = 5; // int
auto y = 3.14; // double
auto n = v.size(); // size_t — you don't care what this is
// Where it really helps — long iterator types
map<string, vector<int>>::iterator it = m.begin(); // painful
auto it = m.begin(); // clean
The rule: Use auto whenever writing the type is longer than writing auto or makes the code harder to read.
The trap: auto without & makes a copy. Always think about whether you need a reference.
for (auto x : v) // copies each element — fine for int
for (auto& x : v) // reference — use for objects, strings, pairs
3.2 Range-Based For Loop — Iterate Without Indices
When you don’t need the index, this is cleaner than a traditional loop.
vector<int> v = {1, 2, 3, 4, 5};
// Traditional — use when you need the index
for (int i = 0; i < v.size(); i++)
cout << v[i] << " ";
// Range-based — use when you just need each value
for (auto& x : v)
cout << x << " ";
// Read-only — when you won't modify elements
for (const auto& x : v)
cout << x << " ";
The & rule — applies everywhere, not just here:
| You want | Write |
|---|---|
| Read and modify | auto& x |
| Read only, no copy | const auto& x |
| A copy you can modify | auto x (but ask yourself why) |
For anything larger than a primitive (int, char, bool), always use &. Copying strings, pairs, vectors inside a loop is a silent performance killer.
3.3 Structured Bindings — Unpack Pairs with Real Names
pair.first and pair.second tell you nothing about what those values mean. Structured bindings let you unpack a pair into named variables.
pair<int,int> p = {5, 3};
// Old way — what does first and second mean here?
cout << p.first << " " << p.second;
// Structured binding — meaning is clear
auto& [distance, node] = p;
cout << distance << " " << node;
Most common use — looping over maps and vectors of pairs:
map<string, int> scores = {{"alice", 90}, {"bob", 85}};
// Old way
for (auto& e : scores)
cout << e.first << ": " << e.second << "\n";
// With structured bindings — immediately clear
for (auto& [name, score] : scores)
cout << name << ": " << score << "\n";
With priority queue in Dijkstra — the clearest form:
auto [dist, u] = pq.top();
pq.pop();
for (auto& [v, w] : adj[u]) {
if (dist + w < d[v]) { ... }
}
The & rule again: Write auto& [a, b] by default. Without & you get copies.
3.4 References — The Most Important Concept for Performance
A reference is an alias. It is not a copy. It refers to the original variable.
int x = 5;
int& ref = x; // ref is x — same memory location
ref = 10;
cout << x; // prints 10 — x changed through ref
Why this matters in DSA — function parameters:
Every time you pass a container to a function, C++ copies it by default. A vector of 100,000 integers copied on every recursive DFS call = guaranteed TLE.
// Copies the entire vector every call — O(n) per call
void dfs(int node, vector<vector<int>> adj) // ❌
// Passes reference — zero copy cost
void dfs(int node, vector<vector<int>>& adj) // ✅
The three patterns you will write constantly:
// Modify the container inside the function
void fill(vector<int>& v)
// Read-only — protect from accidental modification
void print(const vector<int>& v)
// Return by reference (careful — don't return local variable references)
int& getElement(vector<int>& v, int i) { return v[i]; }
The one mistake that crashes programs:
int& bad() {
int x = 5;
return x; // ❌ x dies when function returns — dangling reference
}
Never return a reference to a local variable. The variable dies when the function ends. The reference becomes garbage.
3.5 const — Two Uses That Matter in DSA
Forget everything about const except these two patterns:
// Pattern 1 — fixed values (use these in almost every solution)
const int INF = 1e9;
const int MOD = 1e9 + 7;
// Pattern 2 — read-only function parameters
void solve(const vector<int>& v) {
v[0] = 5; // ❌ compiler error — cannot modify
cout << v[0]; // ✅ reading is fine
}
const in a function parameter says: “I promise not to modify this. I just need to read it.” It’s both documentation and a safeguard against bugs.
3.6 pair and tuple — Bundling Values
DSA constantly involves values that come in twos or threes: edges (u, v, weight), coordinates (row, col), frequencies (value, count). pair and tuple bundle these into a single unit.
pair — for two values:
pair<int, int> p = {3, 7};
p.first; // 3
p.second; // 7
// In vectors — very common
vector<pair<int,int>> edges;
edges.push_back({0, 1});
edges.push_back({1, 2});
// Comparison is lexicographic — first element compared first
pair<int,int> a = {1, 5};
pair<int,int> b = {1, 3};
a > b; // true — first elements equal, 5 > 3
tuple — for three or more values:
// Edge with weight — 3 values
tuple<int,int,int> edge = {weight, u, v};
// Unpack with structured binding
auto [w, u, v] = edge;
// Or access by index (uglier)
get<0>(edge); // weight
get<1>(edge); // u
get<2>(edge); // v
The classic DSA pattern — value with original index:
vector<int> nums = {40, 10, 30, 20};
vector<pair<int,int>> indexed;
for (int i = 0; i < nums.size(); i++)
indexed.push_back({nums[i], i}); // {value, original_index}
sort(indexed.begin(), indexed.end());
// Now sorted by value, but you still know original positions
3.7 Lambda Expressions — Inline Logic for Comparators
A lambda is a function you define right where you need it. In DSA, they appear almost exclusively as custom comparators for sort and priority_queue.
Anatomy:
[capture](parameters) { body }
// Examples:
auto add = [](int a, int b) { return a + b; };
cout << add(3, 4); // 7
The only part you need to understand for DSA right now — the comparator rule:
A comparator returns
trueifashould come beforeb. That’s the entire rule.
// Sort ascending (default behavior)
sort(v.begin(), v.end(), [](auto& a, auto& b) { return a < b; });
// Sort descending
sort(v.begin(), v.end(), [](auto& a, auto& b) { return a > b; });
// Sort pairs by second element ascending
sort(v.begin(), v.end(), [](auto& a, auto& b) {
return a.second < b.second;
});
// Sort by multiple keys — grade descending, name ascending on tie
sort(v.begin(), v.end(), [](auto& a, auto& b) {
if (a.grade != b.grade)
return a.grade > b.grade;
return a.name < b.name;
});
Capture brackets — when your lambda needs an outside variable:
int threshold = 5;
// [&] — capture all outside variables by reference
auto isAbove = [&](int x) { return x > threshold; };
| Capture | Meaning |
|---|---|
[] |
capture nothing |
[&] |
all outside variables by reference |
[=] |
all by value (copy) |
[&x, y] |
x by ref, y by value |
The fatal comparator mistake — never use >= or <=:
// ❌ undefined behavior — strict weak ordering violated
sort(v.begin(), v.end(), [](auto& a, auto& b) { return a >= b; });
// ✅ always strict: < or >
sort(v.begin(), v.end(), [](auto& a, auto& b) { return a > b; });
4. Block 2 — Core STL Containers
These are the data structures you will use in every problem. For each container: understand what problem it solves, how to use it, and when to prefer it over alternatives.
4.1 vector — Your Default Array
A vector is a dynamic array. It handles its own sizing. Use it everywhere you would use a plain array.
Declaring and initializing:
vector<int> v; // empty
vector<int> v(5); // [0, 0, 0, 0, 0]
vector<int> v(5, 3); // [3, 3, 3, 3, 3]
vector<int> v = {1, 2, 3, 4, 5}; // initializer list
// 2D vector — replaces int grid[MAXN][MAXN]
vector<vector<int>> grid(rows, vector<int>(cols, 0));
Core operations:
v.push_back(x); // add to end — O(1) amortized
v.pop_back(); // remove from end — O(1)
v[i]; // access by index — O(1)
v.size(); // number of elements — O(1)
v.empty(); // true if size == 0
v.clear(); // remove all elements
// Safe size usage — avoids sign comparison warning
int n = v.size(); // store as int, not auto
for (int i = 0; i < n; i++) { ... }
Passing to functions — always by reference:
void process(vector<int>& v) // modify
void process(const vector<int>& v) // read only
reserve — avoid reallocations when size is known:
vector<int> v;
v.reserve(100000); // allocate space upfront
for (int i = 0; i < 100000; i++)
v.push_back(i); // no reallocation happens
Without reserve, a vector doubles its capacity every time it fills up, triggering a copy of all elements. With reserve, you pay the cost once.
4.2 stack — Last In, First Out
Use a stack when you need to process the most recently added item first.
Problems that need a stack: valid parentheses, next greater element, undo operations, DFS iterative.
stack<int> st;
st.push(10); // add to top
st.push(20);
st.top(); // see top — 20 (does not remove)
st.pop(); // remove top
st.empty(); // true if empty
st.size(); // number of elements
Critical rule: Always check !st.empty() before calling top() or pop(). Calling either on an empty stack is undefined behavior (crash).
if (!st.empty())
cout << st.top(); // safe
Classic pattern — valid parentheses:
stack<char> st;
for (char c : s) {
if (c == '(' || c == '{' || c == '[') {
st.push(c);
} else {
if (st.empty()) return false;
char top = st.top(); st.pop();
if (c == ')' && top != '(') return false;
if (c == '}' && top != '{') return false;
if (c == ']' && top != '[') return false;
}
}
return st.empty();
4.3 queue — First In, First Out
Use a queue when you process items in arrival order. The primary use in DSA is BFS.
queue<int> q;
q.push(10); // add to back
q.front(); // see front — does not remove
q.back(); // see back — does not remove
q.pop(); // remove from front
q.empty();
q.size();
The most important distinction: stack uses top(), queue uses front(). Mixing these up is one of the most common bugs.
4.4 deque — Double-Ended Queue
Like a vector and a queue combined. Efficient push/pop at both ends.
deque<int> dq;
dq.push_back(x); // add to back
dq.push_front(x); // add to front
dq.pop_back(); // remove from back
dq.pop_front(); // remove from front
dq.front();
dq.back();
dq[i]; // random access like vector
When to use: Sliding window maximum problem — you need to add to one end and remove from both ends efficiently. That’s the main DSA use case.
4.5 priority_queue — Always Get the Most Important Element
A priority queue (heap) lets you always extract the maximum (or minimum) element in O(log n), regardless of insertion order.
Problems: Dijkstra, k largest elements, greedy algorithms, merge k sorted lists.
// Max-heap — default, largest element on top
priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(4);
pq.top(); // 4 (largest)
pq.pop(); // removes 4
Min-heap — smallest element on top:
priority_queue<int, vector<int>, greater<int>> pq;
// Same API — just smallest comes out first
With pairs — extremely common:
// Max-heap of pairs — compares by first element by default
priority_queue<pair<int,int>> pq;
// Min-heap of pairs — for Dijkstra
priority_queue<pair<int,int>,
vector<pair<int,int>>,
greater<pair<int,int>>> pq;
pq.push({dist, node});
auto [d, u] = pq.top(); pq.pop();
The three differences from stack:
stack |
priority_queue |
|
|---|---|---|
| Order | LIFO | By priority |
| Access | top() — most recent |
top() — highest priority |
| Insertion | push() |
push() |
Both use top() and pop(). The difference is only in what top() returns.
4.6 set — Sorted Unique Elements
A set stores elements in sorted order with no duplicates. Internal structure is a balanced BST (red-black tree).
Use when: You need to know if something exists, get unique elements, or iterate in sorted order.
set<int> s;
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(1); // duplicate — silently ignored
// s = {1, 3, 4} — sorted, no duplicates
s.count(3); // 1 if exists, 0 if not — use for existence check
s.find(3); // iterator to element, or s.end() if not found
s.erase(3); // remove element
s.size();
// Build from vector — instant dedup + sort
set<int> s(v.begin(), v.end());
Existence check — two ways:
// count — simpler for existence only
if (s.count(x)) { /* exists */ }
// find — when you need to do something with the element too
auto it = s.find(x);
if (it != s.end()) { /* *it is the element */ }
One critical rule: You cannot modify an element in a set. Set elements are immutable. If you need to change a value, erase it and insert the new value.
s.erase(3);
s.insert(5); // replace 3 with 5
4.7 multiset — Sorted Elements with Duplicates
Like set but allows duplicates. Use when you need a sorted structure where you insert and delete individual occurrences.
multiset<int> ms;
ms.insert(3);
ms.insert(3); // kept — ms = {3, 3}
ms.insert(1); // ms = {1, 3, 3}
// ⚠️ THE CLASSIC BUG:
ms.erase(3); // ❌ removes ALL 3s — ms = {1}
// ✅ CORRECT — remove only one occurrence:
ms.erase(ms.find(3)); // removes one 3 — ms = {1, 3}
Burn ms.erase(ms.find(x)) into memory. It is the only correct way to remove a single element from a multiset.
4.8 unordered_set — Fast Existence Check, No Order
Same as set but uses hashing internally instead of a BST. No sorted order. O(1) average for all operations.
unordered_set<int> us;
us.insert(x);
us.count(x); // 0 or 1
us.erase(x);
us.find(x); // same as set
When to use which:
| Need | Use |
|---|---|
| Fast existence check, don’t need order | unordered_set |
| Sorted iteration or range queries | set |
| Allow duplicates | multiset |
Default choice for existence checking: unordered_set. Only switch to set if you need sorted traversal.
4.9 map — Key-Value Lookup
A map stores key-value pairs, sorted by key. Like an array but the index can be anything — string, char, pair, etc.
Problems: Frequency counting, grouping elements, memoization, any “look up X, return Y” scenario.
map<string, int> freq;
freq["apple"] = 3; // insert/update
freq["apple"]++; // increment (creates with 0 if missing)
// ⚠️ Critical: m[key] CREATES the key if it doesn't exist
// Never use m[key] to check existence — use count()
freq.count("apple"); // 1 if exists, 0 if not ✅
freq["banana"]; // creates "banana" with value 0 ❌ for checking
auto it = freq.find("apple");
if (it != freq.end()) {
cout << it->first << ": " << it->second;
}
freq.erase("apple");
freq.size();
Frequency counting — the most common map pattern:
vector<int> nums = {1, 3, 2, 1, 3, 3};
map<int, int> freq;
for (auto x : nums)
freq[x]++; // auto-initializes to 0, then increments
for (auto& [val, cnt] : freq)
cout << val << " appears " << cnt << " times\n";
// Output is in sorted key order
Map always keeps keys sorted. Iterating a map gives you pairs in ascending key order. This is useful when you need sorted output.
4.10 unordered_map — Fast Map, No Order
Same as map but O(1) average instead of O(log n). Identical API.
unordered_map<int, int> um;
um[key]++;
um.count(key);
um.find(key);
// everything same as map
When to use which:
| Situation | Use |
|---|---|
| Need sorted keys | map |
| Just need fast lookup/counting | unordered_map |
| Not sure | unordered_map (it’s faster) |
One warning: In rare contest scenarios with adversarial inputs, unordered_map can degrade to O(n) per operation due to hash collisions. If you’re getting TLE with unordered_map, switch to map.
4.11 string — Character Sequences
Strings in C++ behave like vectors of characters with extra operations.
Core operations:
string s = "helloworld";
s.length(); // 10, same as s.size()
s[i]; // access character by index
s[0] = 'H'; // modify character
s.push_back('!'); // append character
s.pop_back(); // remove last character
s + " world"; // concatenation (creates new string)
substr(start, length) — second argument is LENGTH, not end index:
string s = "helloworld";
// 0123456789
s.substr(5); // "world" — from index 5 to end
s.substr(0, 5); // "hello" — from 0, length 5
s.substr(2, 3); // "llo" — from 2, length 3
// ⚠️ Common mistake:
s.substr(2, 5); // "llowo" — NOT s[2..5]
// Second arg is how many chars to take, not where to stop
find — locate a substring:
int pos = s.find("world"); // returns index, or string::npos if not found
if (s.find("world") != string::npos)
cout << "found at " << pos << "\n";
// string::npos is a special sentinel value meaning "not found"
// Just like map.end() means "key not found"
Splitting a string by spaces:
string sentence = "the cat sat on the mat";
string word;
vector<string> words;
stringstream ss(sentence);
while (ss >> word)
words.push_back(word);
// words = {"the", "cat", "sat", "on", "the", "mat"}
stringstream treats a string like cin. The >> operator reads one whitespace-delimited token at a time. This pattern appears in almost every string manipulation problem.
Character frequency — the c - 'a' trick:
// Maps lowercase letters to indices 0-25
// 'a' - 'a' = 0, 'b' - 'a' = 1, ..., 'z' - 'a' = 25
int freq[26] = {0};
for (char c : s)
freq[c - 'a']++;
// Reverse: index back to character
char ch = (char)('a' + i);
This avoids a map for simple character frequency problems. O(1) access instead of O(log n).
String ↔ number conversion:
string s = "42";
int n = stoi(s); // string to int
long long n = stoll(s); // string to long long
double d = stod(s); // string to double
int n = 42;
string s = to_string(n); // int to string
Comparison — lexicographic by default:
string a = "apple", b = "banana";
a == b; // false
a < b; // true — 'a' < 'b' alphabetically
Sorting a vector<string> with sort works automatically because < is defined on strings.
5. Block 3 — Essential STL Algorithms
All STL algorithms operate on ranges defined by two iterators: begin() and end(). Think of begin() as a pointer to the first element, and end() as a pointer to one-past-the-last element.
sort(v.begin(), v.end());
// ^^^^^^^^^^ ^^^^^^^^
// start here stop before here
5.1 sort — The Most Used Algorithm
// Ascending (default)
sort(v.begin(), v.end());
// Descending — using built-in greater
sort(v.begin(), v.end(), greater<int>());
// Custom — using lambda
sort(v.begin(), v.end(), [](auto& a, auto& b) {
return a.second < b.second; // by second element of pair
});
// Sort only part of the vector
sort(v.begin(), v.begin() + k); // sort first k elements
sort(v.begin() + l, v.begin() + r + 1); // sort range [l, r]
sort is O(n log n). It uses introsort internally (hybrid of quicksort, heapsort, insertion sort).
stable_sort — same signature, preserves relative order of equal elements. O(n log n) but slightly slower. Use only when the problem explicitly requires stability.
5.2 binary_search, lower_bound, upper_bound
All three require a sorted array. Always sort first.
binary_search — does value exist?
bool found = binary_search(v.begin(), v.end(), target);
// Returns true or false only. Does not give position.
lower_bound — first position where value could be inserted (first element >= target):
vector<int> v = {1, 2, 4, 4, 5, 6};
// 0 1 2 3 4 5
auto it = lower_bound(v.begin(), v.end(), 4);
int idx = it - v.begin(); // → 2 (first index of 4)
upper_bound — position after all occurrences (first element > target):
auto it = upper_bound(v.begin(), v.end(), 4);
int idx = it - v.begin(); // → 4 (one past last 4)
Together — count occurrences:
auto lo = lower_bound(v.begin(), v.end(), x);
auto hi = upper_bound(v.begin(), v.end(), x);
int count = hi - lo; // number of times x appears
This is O(log n) and replaces a linear scan for counting in sorted arrays.
Converting iterator to index:
// Subtract begin() from any iterator to get the index
auto it = lower_bound(v.begin(), v.end(), x);
int index = it - v.begin();
5.3 Other Algorithms
reverse:
reverse(v.begin(), v.end()); // reverses vector in place
reverse(s.begin(), s.end()); // reverses string in place
min_element and max_element:
auto it = min_element(v.begin(), v.end());
cout << *it; // value — dereference the iterator
int idx = it - v.begin(); // index of minimum
auto it = max_element(v.begin(), v.end());
cout << *it;
accumulate — sum (or any reduction):
#include <numeric> // or just use bits/stdc++.h
int sum = accumulate(v.begin(), v.end(), 0);
// Third argument is the starting value — almost always 0
next_permutation — generate all permutations:
vector<int> v = {1, 2, 3};
sort(v.begin(), v.end()); // must start sorted to get all permutations
do {
for (int x : v) cout << x << " ";
cout << "\n";
} while (next_permutation(v.begin(), v.end()));
// Prints all 6 permutations of {1, 2, 3} in lexicographic order
6. Block 4 — Structures for DSA
6.1 struct — Custom Data Bundling
When you need more than two related values, use a struct instead of a pair.
struct Edge {
int from, to, weight;
};
// Create and use
Edge e = {0, 1, 5};
cout << e.from << " " << e.to << " " << e.weight;
// Vector of structs
vector<Edge> edges;
edges.push_back({0, 1, 4});
edges.push_back({1, 2, 3});
Functions inside struct:
struct Student {
string name;
int grade;
bool isPass() {
return grade >= 75;
}
};
Student s = {"alice", 80};
s.isPass(); // true
6.2 Constructor and Initializer List
A constructor is a special function that runs automatically when you create an object.
struct ListNode {
int val;
ListNode* next;
// Constructor
ListNode(int x) : val(x), next(nullptr) {}
// ^^^^^^ ^^^^^^^^^^^^
// set val=x set next=nullptr
};
// Usage
ListNode* node = new ListNode(5);
// node->val = 5, node->next = nullptr — set by constructor
The : val(x), next(nullptr) part is the initializer list — it sets member variables during construction. It is exactly equivalent to writing this inside the {}:
ListNode(int x) {
val = x;
next = nullptr;
}
The initializer list version is preferred — it’s more compact and slightly more efficient.
6.3 operator< Overloading — Custom Default Sort Order
When you call sort on a vector of your custom struct, C++ doesn’t know how to compare your structs. You either provide a lambda comparator, or you define operator< inside the struct.
Defining operator< means: “when C++ needs to know if a < b, use this logic.”
struct Edge {
int from, to, weight;
bool operator<(const Edge& other) const {
return weight < other.weight; // sort by weight ascending
}
// `other` = the right side of <
// `this` = the left side of <
};
sort(edges.begin(), edges.end()); // uses operator< — no lambda needed
Once operator< is defined, sort, set, map, and priority_queue all use it automatically.
Lambda vs operator< — when to use which:
| Situation | Use |
|---|---|
| One-off sort in one place | Lambda comparator |
| Same struct sorted the same way everywhere | operator< |
Need to put struct in a set or map |
operator< required |
6.4 Node Structures — Linked List and Tree
These exact structs appear in LeetCode/competitive programming problems. You need to be able to write them from memory.
Linked List Node:
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
// Build 1 -> 2 -> 3
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
Binary Tree Node:
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// Build:
// 1
// / \
// 2 3
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
Graph Edge Struct:
struct Edge {
int to, weight;
};
vector<vector<Edge>> adj(n); // adjacency list with weights
adj[u].push_back({v, w});
7. Block 5 — Pointers and Memory
Pointers are required for linked lists, trees, and any dynamically allocated node-based structure. Understanding them is non-negotiable for DSA.
7.1 What is a Pointer?
Every variable lives at some address in memory. A pointer is a variable that stores that address.
int x = 42;
// x lives at address 1000 (example)
// memory at address 1000 holds the value 42
int* p = &x;
// p holds the value 1000 (the address of x)
// the * in declaration means "this is a pointer to int"
// the & in &x means "give me the address of x"
7.2 The Two Uses of * and & — Read This Carefully
Both symbols have two completely different meanings depending on context. This is the single most confusing thing about pointers for beginners.
* has two meanings:
int* p = &x; // Meaning 1: DECLARATION — "p is a pointer to int"
cout << *p; // Meaning 2: DEREFERENCE — "value at the address p holds"
& has two meanings:
int& ref = x; // Meaning 1: DECLARATION — "ref is a reference to int"
int* p = &x; // Meaning 2: ADDRESS-OF — "give me the address of x"
In summary:
| Symbol | In a declaration (int* p, int& r) |
On a variable (*p, &x) |
|---|---|---|
* |
“this is a pointer to…” | “value at address” (dereference) |
& |
“this is a reference to…” | “address of this variable” |
Putting it together:
int x = 42;
int* p = &x; // p = address of x (say, 1000)
cout << p; // prints 1000 — the address (rarely useful)
cout << *p; // prints 42 — the value at that address
*p = 99; // change the value at that address
cout << x; // prints 99 — x changed because p pointed to it
7.3 new — Allocating on the Heap
// Stack allocation — automatic cleanup when scope ends
int x = 5; // lives until } of current scope
// Heap allocation — lives until you delete it
int* p = new int(5); // allocate one int, initialize to 5
int* arr = new int[100]; // allocate array of 100 ints
For DSA, you mainly use new to create nodes:
ListNode* node = new ListNode(5);
// Allocates a ListNode on the heap
// node holds the address of that node
// node->val = 5, node->next = nullptr
In competitive programming, you almost never call delete. Memory leaks don’t matter in contest submissions — the process ends and the OS reclaims everything. In production code, you would always delete what you new.
7.4 nullptr — The Empty Pointer
nullptr means “this pointer points to nothing.” Like null in Java or None in Python.
ListNode* p = nullptr; // p points to nothing
// Always check before dereferencing
if (p != nullptr) {
cout << p->val; // safe
}
// Shorter — pointers evaluate to false when null
if (p) {
cout << p->val; // same thing
}
Accessing a nullptr pointer is undefined behavior — on most systems, an immediate crash. This is the most common runtime error in pointer code.
7.5 The -> Operator
When you have a pointer to a struct, use -> to access members.
When you have the struct directly, use .
ListNode node(5); // direct struct
cout << node.val; // use dot
ListNode* p = &node; // pointer to struct
cout << p->val; // use arrow
// -> is just shorthand: p->val == (*p).val
// The arrow dereferences and accesses in one step
Memory rule: Pointer → use ->. Direct object → use .
7.6 Linked List Patterns
The linked list traversal template. Write this until it is automatic:
// Traverse — print all values
ListNode* curr = head;
while (curr != nullptr) { // condition: curr not null
cout << curr->val << " ";
curr = curr->next; // move to next node
}
Why curr != nullptr and NOT curr->next != nullptr:
curr != nullptr — stops AFTER the last node (processes every node including last).
curr->next != nullptr — stops AT the last node (misses the last node’s value).
Always use curr != nullptr for traversal.
Reverse a linked list — the canonical interview problem:
ListNode* reverse(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* next = nullptr;
while (curr != nullptr) {
next = curr->next; // 1. save next before we overwrite it
curr->next = prev; // 2. flip the pointer
prev = curr; // 3. advance prev
curr = next; // 4. advance curr
}
return prev; // prev is the new head
}
The four steps in the loop — in order, every time: save, flip, advance prev, advance curr.
7.7 Tree Traversals
Inorder (left → root → right) — gives sorted order for BST:
void inorder(TreeNode* root) {
if (root == nullptr) return; // base case
inorder(root->left);
cout << root->val << " ";
inorder(root->right);
}
Preorder (root → left → right):
void preorder(TreeNode* root) {
if (!root) return;
cout << root->val << " ";
preorder(root->left);
preorder(root->right);
}
Postorder (left → right → root):
void postorder(TreeNode* root) {
if (!root) return;
postorder(root->left);
postorder(root->right);
cout << root->val << " ";
}
The pattern: always check if (!root) return; first. This is the base case that stops recursion.
8. Block 6 — Performance Knowledge
You don’t need to understand computer architecture. You need to know which operations are fast and which are slow.
8.1 STL Complexity Reference
| Container | Operation | Complexity |
|---|---|---|
vector |
push_back |
O(1) amortized |
vector |
insert at middle |
O(n) — avoid in hot loops |
vector |
operator[] |
O(1) |
stack / queue |
push, pop, top/front |
O(1) |
priority_queue |
push, pop |
O(log n) |
priority_queue |
top |
O(1) |
set / map |
insert, find, erase |
O(log n) |
unordered_set / unordered_map |
insert, find, erase |
O(1) avg |
sort |
— | O(n log n) |
binary_search |
— | O(log n) |
lower_bound / upper_bound |
— | O(log n) |
The critical comparison: If you’re doing 100,000 lookups, unordered_map takes 100,000 × O(1) = O(n). map takes 100,000 × O(log n) = O(n log n). For n = 10^5, that’s the difference between 10^5 and ~1.7 × 10^6 operations.
8.2 Avoiding Unnecessary Copies
Copies are silent performance killers. They don’t cause errors — they just make your code slower.
// ❌ All of these make copies
for (auto v : matrix) // copies each row
for (string s : words) // copies each string
void solve(vector<int> v) // copies entire vector
// ✅ References — no copy
for (auto& v : matrix)
for (auto& s : words)
void solve(vector<int>& v)
Rule of thumb: If the object is larger than a pointer (8 bytes on 64-bit), use a reference.
8.3 Move Semantics — One Paragraph
When you return a vector from a function, you might expect it to be copied. In modern C++ (C++11 and later), the compiler applies Return Value Optimization (RVO) — it constructs the vector directly in the caller’s memory, with zero copies. If RVO doesn’t apply, the compiler uses move semantics: instead of copying all the data, it “moves” ownership (just copies a pointer). The result: returning large containers from functions is cheap. Write clean code; don’t sacrifice clarity to avoid returning vectors.
vector<int> buildResult() {
vector<int> result;
// ... fill result
return result; // zero or near-zero cost — compiler optimizes this
}
9. Block 7 — Coding Templates
These are the exact skeletons you will use in real problems. Learn the shape of each one. When you encounter a problem, recognize which template fits, then fill in the logic.
9.1 Frequency Counting
unordered_map<int, int> freq;
for (auto x : nums)
freq[x]++;
// Find most frequent
int maxFreq = 0;
int result = -1;
for (auto& [val, cnt] : freq) {
if (cnt > maxFreq) {
maxFreq = cnt;
result = val;
}
}
9.2 Two Pointers
Use when: array is sorted and you’re looking for pairs satisfying a condition.
sort(nums.begin(), nums.end()); // sort first if not already sorted
int l = 0, r = n - 1;
while (l < r) {
int sum = nums[l] + nums[r];
if (sum == target) {
// found a pair
l++; r--;
} else if (sum < target) {
l++; // need larger sum
} else {
r--; // need smaller sum
}
}
9.3 Sliding Window — Fixed Size
Use when: you need maximum/minimum/sum of all subarrays of exactly size k.
int win = 0, best = 0;
// Build first window
for (int i = 0; i < k; i++)
win += nums[i];
best = win;
// Slide — add right element, remove left element
for (int i = k; i < n; i++) {
win += nums[i]; // add new right element
win -= nums[i - k]; // remove element that left the window
best = max(best, win);
}
9.4 Sliding Window — Variable Size
Use when: you need the longest/shortest subarray satisfying a condition.
int l = 0, best = 0;
unordered_map<int, int> window;
for (int r = 0; r < n; r++) {
window[nums[r]]++; // expand right
while (/* condition violated */) {
window[nums[l]]--; // shrink left
if (window[nums[l]] == 0)
window.erase(nums[l]);
l++;
}
best = max(best, r - l + 1); // window size = r - l + 1
}
9.5 Binary Search Template
int l = 0, r = n - 1;
while (l <= r) { // note: <=, not <
int mid = l + (r - l) / 2; // avoids integer overflow
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1; // not found
Why l + (r - l) / 2 instead of (l + r) / 2: If l and r are both close to INT_MAX, their sum overflows. The first form never overflows.
9.6 Prefix Sum
Use when: you need to answer multiple range sum queries efficiently.
int n = nums.size();
vector<int> prefix(n + 1, 0);
// Build — O(n)
for (int i = 0; i < n; i++)
prefix[i + 1] = prefix[i] + nums[i];
// Query sum from index l to r inclusive — O(1)
int rangeSum = prefix[r + 1] - prefix[l];
Without prefix sum: each range query is O(n). With prefix sum: O(1) per query after O(n) preprocessing.
9.7 DFS — Recursive Graph Traversal
vector<vector<int>> adj(n); // build this from input edges
vector<bool> visited(n, false);
void dfs(int u) {
visited[u] = true;
// process u here (before neighbors = preorder)
for (auto v : adj[u]) {
if (!visited[v]) {
dfs(v);
}
}
// process u here (after neighbors = postorder)
}
// Call from main
dfs(startNode);
9.8 BFS — Level-Order Graph Traversal
vector<vector<int>> adj(n);
vector<bool> visited(n, false);
vector<int> dist(n, -1); // optional: track distance from source
queue<int> q;
q.push(start);
visited[start] = true;
dist[start] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
dist[v] = dist[u] + 1; // one step further
q.push(v);
}
}
}
BFS gives shortest path (in number of edges) in unweighted graphs.
9.9 Graph — Building Adjacency List
int n, m; // n nodes (0-indexed), m edges
cin >> n >> m;
vector<vector<int>> adj(n); // unweighted
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u); // remove for directed graph
}
// Weighted graph
vector<vector<pair<int,int>>> adj(n); // adj[u] = {v, weight}
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
9.10 Dijkstra’s Shortest Path
For weighted graphs — finds shortest distance from source to all nodes.
vector<vector<pair<int,int>>> adj(n); // adj[u] = {v, weight}
vector<int> dist(n, INT_MAX);
priority_queue<pair<int,int>,
vector<pair<int,int>>,
greater<pair<int,int>>> pq; // min-heap
dist[src] = 0;
pq.push({0, src}); // {distance, node}
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) continue; // outdated entry — skip
for (auto [v, w] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
// dist[i] = shortest distance from src to i
// INT_MAX means unreachable
9.11 Memoization (Top-Down DP)
unordered_map<int, long long> memo;
long long solve(int n) {
// Base case
if (n <= 1) return n;
// Return cached result if already computed
if (memo.count(n))
return memo[n];
// Compute, store, return
return memo[n] = solve(n - 1) + solve(n - 2);
}
The three-step pattern: base case → check cache → compute and store.
9.12 Sorting Pairs and Structs
// Sort vector of pairs by second element descending
sort(v.begin(), v.end(), [](auto& a, auto& b) {
return a.second > b.second;
});
// Sort structs with multiple keys
sort(students.begin(), students.end(), [](auto& a, auto& b) {
if (a.grade != b.grade)
return a.grade > b.grade; // grade descending
return a.name < b.name; // name ascending on tie
});
// Sort edges by weight (using operator<)
sort(edges.begin(), edges.end()); // uses operator< if defined
10. Common Mistakes Quick Reference
These are mistakes that produce incorrect output or crashes with no compiler error. Silent bugs.
| Mistake | Wrong | Correct |
|---|---|---|
| Map existence check | if (m[key]) — creates key |
if (m.count(key)) |
| Multiset erase one | ms.erase(x) — removes ALL |
ms.erase(ms.find(x)) |
| Linked list loop | curr->next != nullptr — misses last |
curr != nullptr |
| Comparator | return a >= b — UB |
return a > b (strict) |
| Priority queue access | pq.front() — no such function |
pq.top() |
| substr second arg | s.substr(2, 5) — means length 5 |
second arg is length |
| Loop copy | for (auto x : v) for strings/pairs |
for (auto& x : v) |
| Sign comparison | for (int i = 0; i < v.size(); ...) |
int n = v.size(); i < n |
| Overflow | (l + r) / 2 |
l + (r - l) / 2 |
| Null dereference | p->val without checking |
if (p) p->val |
| Redundant includes | #include <bits/stdc++.h> + others |
only bits/stdc++.h |
| Slow output | cout << endl in tight loop |
cout << "\n" |
endl vs "\n": endl flushes the output buffer every call. "\n" does not. In problems with 100,000+ output lines, using endl can cause TLE by itself. Always use "\n".
11. Complexity Cheat Sheet
Big-O intuition for contest constraints:
| n (input size) | Max acceptable complexity |
|---|---|
| n ≤ 10 | O(n!) — permutations fine |
| n ≤ 20 | O(2^n) — bitmask DP fine |
| n ≤ 500 | O(n³) |
| n ≤ 5,000 | O(n²) |
| n ≤ 10^6 | O(n log n) |
| n ≤ 10^8 | O(n) |
Container operations:
vector push_back O(1) access O(1) insert-mid O(n)
stack push/pop O(1) top O(1)
queue push/pop O(1) front O(1)
pq push/pop O(logn) top O(1)
set/map insert O(logn) find O(logn) erase O(logn)
uo_set/map insert O(1)* find O(1)* erase O(1)*
sort O(n logn)
bin_search O(logn)
*average case, O(n) worst case for unordered containers
These notes cover everything required to implement standard DSA topics in C++ cleanly and quickly. The templates in Block 7 are the payoff — the rest of the notes exist to make those templates readable and writable without hesitation.