Common Algorithm Patterns#
Each pattern has only two parts: Explanation + Code Framework.
1) Framework Thinking (Traversal First)#
Explanation#
Most problems are traversal problems in disguise: arrays, linked lists, trees, graphs.
Identify the data structure first, then pick traversal order, then fill business logic into the skeleton.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| #include <vector>
using namespace std;
struct ListNode {
int val;
ListNode* next;
};
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
};
void processValue(int x) {
// TODO: business logic
(void)x;
}
void traverseArray(const vector<int>& nums) {
for (int i = 0; i < (int)nums.size(); ++i) {
processValue(nums[i]);
}
}
void traverseList(ListNode* head) {
for (ListNode* p = head; p != nullptr; p = p->next) {
processValue(p->val);
}
}
void traverseTree(TreeNode* root) {
if (root == nullptr) return;
// Pre-order
processValue(root->val);
traverseTree(root->left);
traverseTree(root->right);
// Post-order
}
|
2) Binary Search (Shrink Search Space)#
Explanation#
Use binary search when monotonicity exists.
The key is consistency: interval definition + boundary update rules.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <vector>
using namespace std;
int binarySearch(const vector<int>& nums, int target) {
int left = 0, right = (int)nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
int leftBound(const vector<int>& nums, int target) {
int left = 0, right = (int)nums.size(); // [left, right)
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
if (left >= (int)nums.size() || nums[left] != target) return -1;
return left;
}
|
3) BFS (Minimum Steps / Layer Expansion)#
Explanation#
BFS explores level by level, so it naturally solves minimum-step problems on unweighted graphs.
The main difficulty is usually state modeling, not BFS itself.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| #include <queue>
#include <unordered_set>
#include <vector>
using namespace std;
vector<int> neighbors(int state) {
// TODO: generate adjacent states
return {};
}
int bfsMinStep(int start, int target) {
queue<int> q;
unordered_set<int> visited;
q.push(start);
visited.insert(start);
int step = 0;
while (!q.empty()) {
int sz = (int)q.size();
for (int i = 0; i < sz; ++i) {
int cur = q.front();
q.pop();
if (cur == target) return step;
for (int nxt : neighbors(cur)) {
if (!visited.count(nxt)) {
visited.insert(nxt);
q.push(nxt);
}
}
}
++step;
}
return -1;
}
|
4) Bidirectional BFS#
Explanation#
Expand from both start and target. Stop when frontiers meet.
Usually faster than single-source BFS in large state spaces.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <unordered_set>
#include <vector>
using namespace std;
vector<int> neighbors(int state) {
// TODO: generate adjacent states
return {};
}
int bidirectionalBfs(int start, int target) {
if (start == target) return 0;
unordered_set<int> front{start}, back{target}, visited{start, target};
int step = 0;
while (!front.empty() && !back.empty()) {
if (front.size() > back.size()) swap(front, back);
unordered_set<int> nextFront;
for (int cur : front) {
if (back.count(cur)) return step;
for (int nxt : neighbors(cur)) {
if (!visited.count(nxt)) {
visited.insert(nxt);
nextFront.insert(nxt);
}
}
}
front = move(nextFront);
++step;
}
return -1;
}
|
5) Backtracking (Decision Tree Enumeration)#
Explanation#
Core cycle: choose -> recurse -> undo.
Best for permutations/combinations/subsets/constraint search.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <vector>
using namespace std;
vector<vector<int>> result;
vector<int> path;
vector<int> used;
bool isValid(int choice) {
// TODO: pruning rule
(void)choice;
return true;
}
void backtrack(const vector<int>& nums) {
if ((int)path.size() == (int)nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < (int)nums.size(); ++i) {
if (used[i]) continue;
if (!isValid(nums[i])) continue;
path.push_back(nums[i]);
used[i] = 1;
backtrack(nums);
used[i] = 0;
path.pop_back();
}
}
|
6) Partition Backtracking (Element View / Bucket View)#
Explanation#
Two common models:
- assign each element to one bucket;
- fill buckets one by one.
Combine with pruning (sorting, overflow cutoff, symmetry pruning).
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include <algorithm>
#include <vector>
using namespace std;
bool dfsPartition(int i, const vector<int>& nums, vector<int>& bucket, int target) {
if (i == (int)nums.size()) {
for (int s : bucket) {
if (s != target) return false;
}
return true;
}
for (int j = 0; j < (int)bucket.size(); ++j) {
if (bucket[j] + nums[i] > target) continue;
if (j > 0 && bucket[j] == bucket[j - 1]) continue;
bucket[j] += nums[i];
if (dfsPartition(i + 1, nums, bucket, target)) return true;
bucket[j] -= nums[i];
}
return false;
}
bool canPartitionKSubsets(vector<int> nums, int k) {
int sum = 0;
for (int x : nums) sum += x;
if (k <= 0 || sum % k != 0) return false;
int target = sum / k;
sort(nums.rbegin(), nums.rend());
vector<int> bucket(k, 0);
return dfsPartition(0, nums, bucket, target);
}
|
7) Sliding Window#
Explanation#
Maintain a dynamic window [left, right): expand right to find feasible answers, shrink left to optimize.
Works well for contiguous substring/subarray constraints.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <climits>
#include <string>
#include <unordered_map>
using namespace std;
int slidingWindowTemplate(const string& s, const string& t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
int bestLen = INT_MAX;
while (right < (int)s.size()) {
char c = s[right++];
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) valid++;
}
while (valid == (int)need.size()) {
// TODO: update your answer
bestLen = min(bestLen, right - left);
char d = s[left++];
if (need.count(d)) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return bestLen == INT_MAX ? -1 : bestLen;
}
|
8) Two Pointers (Fast/Slow and Left/Right)#
Explanation#
- Fast/slow pointers: same direction, in-place filtering and linked-list tricks.
- Left/right pointers: opposite direction, pairing/palindrome/reverse style problems.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #include <string>
#include <vector>
using namespace std;
bool isValidValue(int x) {
// TODO: business condition
return x >= 0;
}
int fastSlowFilter(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < (int)nums.size(); ++fast) {
if (isValidValue(nums[fast])) {
nums[slow++] = nums[fast];
}
}
return slow;
}
bool isPalindrome(const string& s) {
int left = 0, right = (int)s.size() - 1;
while (left < right) {
if (s[left] != s[right]) return false;
++left;
--right;
}
return true;
}
|
9) Prefix Sum#
Explanation#
Precompute accumulations once, answer range sum queries in O(1).
Classic space-for-time optimization.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <vector>
using namespace std;
struct PrefixSum1D {
vector<long long> pre;
explicit PrefixSum1D(const vector<int>& nums) {
pre.assign(nums.size() + 1, 0);
for (int i = 1; i <= (int)nums.size(); ++i) {
pre[i] = pre[i - 1] + nums[i - 1];
}
}
long long rangeSum(int l, int r) const { // [l, r]
return pre[r + 1] - pre[l];
}
};
struct PrefixSum2D {
vector<vector<long long>> pre;
explicit PrefixSum2D(const vector<vector<int>>& mat) {
int m = (int)mat.size();
int n = m ? (int)mat[0].size() : 0;
pre.assign(m + 1, vector<long long>(n + 1, 0));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + mat[i - 1][j - 1];
}
}
}
};
|
10) Difference Array#
Explanation#
Best when range updates are frequent and final array is needed once.
For adding val on [l, r], only modify two endpoints in diff.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <vector>
using namespace std;
struct Difference {
vector<long long> diff;
explicit Difference(int n) : diff(n, 0) {}
void add(int l, int r, long long val) {
diff[l] += val;
if (r + 1 < (int)diff.size()) diff[r + 1] -= val;
}
vector<long long> result() const {
vector<long long> nums(diff.size(), 0);
if (diff.empty()) return nums;
nums[0] = diff[0];
for (int i = 1; i < (int)diff.size(); ++i) {
nums[i] = nums[i - 1] + diff[i];
}
return nums;
}
};
|
11) Union-Find (Disjoint Set Union)#
Explanation#
For dynamic connectivity: union sets and query connectivity.
Typical optimizations: path compression + union by size/rank.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <vector>
using namespace std;
class UnionFind {
public:
explicit UnionFind(int n) : parent(n), sz(n, 1) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return;
if (sz[ra] < sz[rb]) swap(ra, rb);
parent[rb] = ra;
sz[ra] += sz[rb];
}
bool connected(int a, int b) {
return find(a) == find(b);
}
private:
vector<int> parent;
vector<int> sz;
};
|
12) Bit Operations#
Explanation#
Great for state compression and parity/frequency tricks.
Common identities: n & (n - 1), x ^ x = 0, bit masking.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| #include <vector>
using namespace std;
bool isBitOne(int x, int k) {
return ((x >> k) & 1) == 1;
}
int setBit(int x, int k) {
return x | (1 << k);
}
int clearBit(int x, int k) {
return x & ~(1 << k);
}
int hammingWeight(unsigned int x) {
int cnt = 0;
while (x != 0) {
x &= (x - 1);
++cnt;
}
return cnt;
}
int singleNumber(const vector<int>& nums) {
int ans = 0;
for (int v : nums) ans ^= v;
return ans;
}
|
Explanation#
Two high-frequency patterns:
- geometric transforms (transpose/reverse) for rotation;
- boundary contraction (
top/bottom/left/right) for spiral traversal.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| #include <algorithm>
#include <vector>
using namespace std;
void rotateClockwise90(vector<vector<int>>& matrix) {
int n = (int)matrix.size();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
for (auto& row : matrix) {
reverse(row.begin(), row.end());
}
}
vector<int> spiralOrder(const vector<vector<int>>& mat) {
vector<int> ans;
if (mat.empty() || mat[0].empty()) return ans;
int top = 0, bottom = (int)mat.size() - 1;
int left = 0, right = (int)mat[0].size() - 1;
while (top <= bottom && left <= right) {
for (int j = left; j <= right; ++j) ans.push_back(mat[top][j]);
++top;
for (int i = top; i <= bottom; ++i) ans.push_back(mat[i][right]);
--right;
if (top <= bottom) {
for (int j = right; j >= left; --j) ans.push_back(mat[bottom][j]);
--bottom;
}
if (left <= right) {
for (int i = bottom; i >= top; --i) ans.push_back(mat[i][left]);
++left;
}
}
return ans;
}
|
14) String Multiplication (Grade-School Simulation)#
Explanation#
When integers are too large for built-in numeric types, simulate multiplication digit by digit.
The core is index mapping + carry handling.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <string>
#include <vector>
using namespace std;
string multiplyStrings(const string& num1, const string& num2) {
if (num1 == "0" || num2 == "0") return "0";
int m = (int)num1.size(), n = (int)num2.size();
vector<int> res(m + n, 0);
for (int i = m - 1; i >= 0; --i) {
for (int j = n - 1; j >= 0; --j) {
int mul = (num1[i] - '0') * (num2[j] - '0');
int p1 = i + j, p2 = i + j + 1;
int sum = mul + res[p2];
res[p2] = sum % 10;
res[p1] += sum / 10;
}
}
string ans;
int i = 0;
while (i < (int)res.size() && res[i] == 0) ++i;
for (; i < (int)res.size(); ++i) ans.push_back(char('0' + res[i]));
return ans.empty() ? "0" : ans;
}
|
15) Fisher-Yates Shuffle#
Explanation#
To make all permutations equally likely, at step i choose random index in [i, n - 1] and swap.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <random>
#include <vector>
using namespace std;
void shuffleArray(vector<int>& nums) {
static random_device rd;
static mt19937 gen(rd());
for (int i = 0; i < (int)nums.size(); ++i) {
uniform_int_distribution<int> dist(i, (int)nums.size() - 1);
int j = dist(gen);
swap(nums[i], nums[j]);
}
}
|
16) Monte Carlo Validation#
Explanation#
For randomized algorithms, run many trials and inspect frequency distribution.
Not a formal proof, but very useful for sanity checks.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| #include <iostream>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
void shuffleArrayForCheck(vector<int>& nums) {
static random_device rd;
static mt19937 gen(rd());
for (int i = 0; i < (int)nums.size(); ++i) {
uniform_int_distribution<int> dist(i, (int)nums.size() - 1);
int j = dist(gen);
swap(nums[i], nums[j]);
}
}
string stateKey(const vector<int>& arr) {
string key;
for (int x : arr) key += to_string(x) + ",";
return key;
}
void monteCarloCheck(vector<int> arr, int T) {
unordered_map<string, int> counter;
for (int i = 0; i < T; ++i) {
vector<int> tmp = arr;
shuffleArrayForCheck(tmp);
counter[stateKey(tmp)]++;
}
// TODO: analyze distribution in counter
cout << "distinct states = " << counter.size() << "\n";
}
|
17) Pancake Sort (Constructive Recursion)#
Explanation#
Move current maximum to front, then to final position, and recurse on remaining prefix.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| #include <algorithm>
#include <vector>
using namespace std;
void flip(vector<int>& cakes, int i, int j) {
while (i < j) swap(cakes[i++], cakes[j--]);
}
int indexOfMax(const vector<int>& cakes, int n) {
int idx = 0;
for (int i = 1; i < n; ++i) {
if (cakes[i] > cakes[idx]) idx = i;
}
return idx;
}
void pancakeSortDfs(vector<int>& cakes, int n, vector<int>& ops) {
if (n <= 1) return;
int idx = indexOfMax(cakes, n);
flip(cakes, 0, idx);
ops.push_back(idx + 1);
flip(cakes, 0, n - 1);
ops.push_back(n);
pancakeSortDfs(cakes, n - 1, ops);
}
|
18) Probability Modeling (Sample-Space First)#
Explanation#
Avoid intuition traps.
Define random process clearly, then define sample space/events, then compute with conditional/total probability.
Code Framework#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <vector>
using namespace std;
struct EventCase {
double pCase;
double pEventGivenCase;
};
double totalProbability(const vector<EventCase>& cases) {
double ans = 0.0;
for (const auto& c : cases) {
ans += c.pCase * c.pEventGivenCase;
}
return ans;
}
double conditionalProbability(double pEandC, double pC) {
if (pC == 0.0) return 0.0;
return pEandC / pC;
}
|