Data Structures & Algorithms
The structures the STL is built on, implemented by hand.
Advanced
std::vector, std::map, and
std::sort almost always — but understanding what those are actually doing
underneath, and being able to build a linked list, a tree, or a graph traversal by hand, is
the difference between using the STL and knowing when its performance characteristics don't
fit your problem. This is also, frankly, interview territory: these are the structures that
come up.
struct Node {
int value;
Node *next = nullptr;
};
class LinkedList {
Node *head = nullptr;
public:
void pushFront(int v) {
Node *n = new Node{v, head};
head = n;
}
~LinkedList() { // without this, every node leaks
while (head) {
Node *next = head->next;
delete head;
head = next;
}
}
};
Node* with
std::unique_ptr<Node> and chaining ownership node-to-node deletes the
entire list automatically when the head goes out of scope, no destructor loop required.
| Order | Sequence | Typical use |
|---|---|---|
| Preorder | Root → Left → Right | copying/serializing a tree — root written before its subtrees |
| Inorder | Left → Root → Right | on a binary search tree specifically, this visits keys in sorted order |
| Postorder | Left → Right → Root | deleting a tree — children freed before their parent |
struct TreeNode { int val; TreeNode *left = nullptr, *right = nullptr; };
void inorder(TreeNode *node) {
if (!node) return;
inorder(node->left);
std::cout << node->val << " ";
inorder(node->right);
}
visited array to
avoid infinite loops on cyclic graphs. They differ in exploration order and in which data
structure drives that order: BFS explores level by level using a queue (FIFO); DFS dives as
deep as possible before backtracking, using either recursion (the call stack) or an explicit
stack.
void BFS(int start, const std::vector<std::list<int>> &adj) {
std::vector<bool> visited(adj.size(), false);
std::queue<int> q;
visited[start] = true;
q.push(start);
while (!q.empty()) {
int v = q.front(); q.pop();
std::cout << v << " ";
for (int neighbor : adj[v]) {
if (!visited[neighbor]) { visited[neighbor] = true; q.push(neighbor); }
}
}
}
void DFS(int v, const std::vector<std::list<int>> &adj, std::vector<bool> &visited) {
visited[v] = true;
std::cout << v << " ";
for (int neighbor : adj[v]) {
if (!visited[neighbor]) DFS(neighbor, adj, visited); // recursion = implicit stack
}
}
int binarySearch(const std::vector<int> &sorted, int target) {
int lo = 0, hi = (int)sorted.size() - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // avoids (lo+hi) overflow on very large indices
if (sorted[mid] == target) return mid;
if (sorted[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
std::lower_bound/
std::upper_bound (see STL Algorithms
& Iterators) do the same search via the standard library rather than by hand.
void merge(std::vector<int> &v, int lo, int mid, int hi) {
std::vector<int> tmp(v.begin() + lo, v.begin() + hi + 1);
int i = lo, j = mid + 1, k = 0;
while (i <= mid && j <= hi)
v[lo + k++] = (tmp[i - lo] <= tmp[j - lo]) ? tmp[i++ - lo] : tmp[j++ - lo];
while (i <= mid) v[lo + k++] = tmp[i++ - lo];
while (j <= hi) v[lo + k++] = tmp[j++ - lo];
}
void mergeSort(std::vector<int> &v, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergeSort(v, lo, mid);
mergeSort(v, mid + 1, hi);
merge(v, lo, mid, hi);
}
| Implementation | find() | union() |
|---|---|---|
| Quick find (each element stores its set's root directly) | O(1) | O(n) — updating every element in a merged set |
| Quick union (each element stores its immediate parent) | O(n) worst case — may walk a long chain to the root | O(1) |
class QuickUnion {
std::vector<int> parent;
public:
QuickUnion(int n) : parent(n) { std::iota(parent.begin(), parent.end(), 0); } // each element starts as its own root
int find(int p) {
while (parent[p] != p) p = parent[p];
return p;
}
void unite(int p, int q) {
int rootP = find(p), rootQ = find(q);
if (rootP != rootQ) parent[rootQ] = rootP;
}
};
find directly to the root it discovers,
flattening future lookups. Combined, both operations become effectively O(α(n)) —
the inverse Ackermann function, so close to constant time that it's treated as O(1) in
practice.
std::vector<int> topoSort(int n, const std::vector<std::list<int>> &adj) {
std::vector<int> inDegree(n, 0);
for (int v = 0; v < n; v++)
for (int neighbor : adj[v]) inDegree[neighbor]++;
std::queue<int> ready;
for (int v = 0; v < n; v++) if (inDegree[v] == 0) ready.push(v);
std::vector<int> order;
while (!ready.empty()) {
int v = ready.front(); ready.pop();
order.push_back(v);
for (int neighbor : adj[v]) if (--inDegree[neighbor] == 0) ready.push(neighbor);
}
return order; // if order.size() < n, the graph has a cycle — no valid topological order exists
}