20 Essential Problem-Solving Patterns in C++
Habibur Rahman Shihab
Posted on October 25, 2024
Data Structures and Algorithms (DSA) are crucial for efficient problem-solving. Here are 20 key patterns, with use cases and examples, to help tackle real-world problems. This guide includes concise C++ examples to demonstrate each pattern in action.
1. Sliding Window Pattern
Efficiently solve problems involving subarrays in a linear array by sliding a window across the array.
Use Case: Find the maximum sum subarray of size k
.
Example: Find the maximum sum of any contiguous subarray of size k
.
int maxSumSubarray(vector<int>& arr, int k) {
int maxSum = 0, windowSum = 0;
for (int i = 0; i < k; i++) {
windowSum += arr[i];
}
maxSum = windowSum;
for (int i = k; i < arr.size(); i++) {
windowSum += arr[i] - arr[i - k];
maxSum = max(maxSum, windowSum);
}
return maxSum;
}
2. Two Pointer Pattern
Two pointers work towards a solution by converging from different ends of an array.
Use Case: Find pairs in a sorted array.
Example: Find two numbers that sum up to a target.
vector<int> twoSum(vector<int>& arr, int target) {
int left = 0, right = arr.size() - 1;
while (left < right) {
int sum = arr[left] + arr[right];
if (sum == target) return {left, right};
else if (sum < target) left++;
else right--;
}
return {};
}
3. Fast & Slow Pointers Pattern
Move two pointers at different speeds to detect cycles in linked lists or arrays.
Use Case: Detect a cycle in a linked list.
Example: Find if a linked list has a cycle.
bool hasCycle(ListNode* head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
4. Merge Intervals Pattern
Merge overlapping intervals in an array of intervals.
Use Case: Merge intervals in a list.
Example: Merge overlapping intervals in a list of meeting times.
vector<vector<int>> mergeIntervals(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
merged.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (merged.back()[1] >= intervals[i][0]) {
merged.back()[1] = max(merged.back()[1], intervals[i][1]);
} else {
merged.push_back(intervals[i]);
}
}
return merged;
}
5. Cyclic Sort Pattern
Sort a cyclically rotated array or group of elements.
Use Case: Sort an array of 1
to n
numbers.
Example: Find the missing number in an array of n
numbers.
int missingNumber(vector<int>& arr) {
int i = 0;
while (i < arr.size()) {
if (arr[i] != i + 1 && arr[i] <= arr.size()) {
swap(arr[i], arr[arr[i] - 1]);
} else {
i++;
}
}
for (int i = 0; i < arr.size(); i++) {
if (arr[i] != i + 1) return i + 1;
}
return -1;
}
6. In-place Reversal of Linked List Pattern
Reverse parts of a linked list in-place.
Use Case: Reverse a sub-list in a linked list.
Example: Reverse a part of the list between positions m
and n
.
ListNode* reverseBetween(ListNode* head, int m, int n) {
if (!head) return nullptr;
ListNode *prev = nullptr, *current = head;
while (m > 1) {
prev = current;
current = current->next;
m--; n--;
}
ListNode *connection = prev, *tail = current;
while (n > 0) {
ListNode *next = current->next;
current->next = prev;
prev = current;
current = next;
n--;
}
if (connection) connection->next = prev;
else head = prev;
tail->next = current;
return head;
}
7. Tree BFS Pattern
Traverse a tree level by level using BFS.
Use Case: Traverse a binary tree.
Example: Perform level-order traversal on a binary tree.
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if (!root) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int size = q.size();
vector<int> currentLevel;
for (int i = 0; i < size; i++) {
TreeNode* node = q.front();
q.pop();
currentLevel.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
result.push_back(currentLevel);
}
return result;
}
8. Tree DFS Pattern
Traverse a tree using Depth First Search.
Use Case: Explore all nodes in a tree.
Example: Perform pre-order traversal of a binary tree.
void preorder(TreeNode* root, vector<int>& result) {
if (!root) return;
result.push_back(root->val);
preorder(root->left, result);
preorder(root->right, result);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
preorder(root, result);
return result;
}
9. Binary Search Pattern
Efficiently search for elements in sorted arrays using binary search.
Use Case: Search in a sorted array.
Example: Find the index of a target value in a sorted array.
int binarySearch(vector<int>& arr, int target) {
int left = 0, right = arr.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
10. Backtracking Pattern
Explore all potential solutions using recursive backtracking.
Use Case: Solve constraint satisfaction problems.
Example: Solve the N-Queens problem.
bool isSafe(vector<string>& board, int row, int col, int n) {
for (int i = 0; i < col; i++)
if (board[row][i] == 'Q') return false;
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--)
if (board[i][j] == 'Q') return false;
for (int i = row, j = col; i < n && j >= 0; i++, j--)
if (board[i][j] == 'Q') return false;
return true;
}
void solveNQueens(int col, vector<string>& board, vector<vector<string>>& solutions, int n) {
if (col == n) {
solutions.push_back(board);
return;
}
for (int i = 0; i < n; i++) {
if (isSafe(board, i, col, n)) {
board[i][col] = 'Q';
solveNQueens(col + 1, board, solutions, n);
board[i][col] = '.';
}
}
}
11. Topological Sort Pattern
Sort nodes in a directed graph using topological ordering.
Use Case: Resolve task dependency problems.
Example: Find the order in which tasks should be completed given dependencies.
vector<int> topologicalSort(int vertices, vector<vector<int>>& edges) {
vector<int> sortedOrder;
if (vertices <= 0) return sortedOrder;
unordered_map<int, int> inDegree;
unordered_map<int, vector<int>> graph;
for (int i = 0; i < vertices; i++) {
inDegree[i] = 0;
graph[i] = vector<int>();
}
for (auto& edge : edges) {
int parent = edge[0], child = edge[1];
graph[parent].push_back(child);
inDegree[child]++;
}
queue<int> sources;
for (auto& entry : inDegree) {
if (entry.second == 0) sources.push(entry.first);
}
while (!sources.empty()) {
int vertex = sources.front();
sources.pop();
sortedOrder.push_back(vertex);
for (int child : graph[vertex]) {
inDegree[child]--;
if (inDegree[child] == 0) sources.push(child);
}
}
if (sortedOrder.size() != vertices) return {}; // cycle detected
return sortedOrder;
}
12. Trie Pattern
Use a trie (prefix tree) to solve problems involving word searches.
Use Case: Efficiently store and search strings.
Example: Implement a word search using a Trie.
class TrieNode {
public:
TrieNode* children[26];
bool isEndOfWord;
TrieNode() {
for (int i = 0; i < 26; i++) children[i] = nullptr;
isEndOfWord = false;
}
};
class Trie {
private:
TrieNode* root;
public:
Trie() {
root = new TrieNode();
}
void insert(string word) {
TrieNode* node = root;
for (char c : word) {
if (!node->children[c - 'a']) {
node->children[c - 'a'] = new TrieNode();
}
node = node->children[c - 'a'];
}
node->isEndOfWord = true;
}
bool search(string word) {
TrieNode* node = root;
for (char c : word) {
if (!node->children[c - 'a']) return false;
node = node->children[c - 'a'];
}
return node->isEndOfWord;
}
};
13. Greedy Pattern
Make the most optimal choice at each step to ensure the best overall solution.
Use Case: Solve optimization problems.
Example: Find the minimum number of coins required to make a specific amount.
int minCoins(vector<int>& coins, int amount) {
sort(coins.rbegin(), coins.rend());
int count = 0;
for (int coin : coins) {
if (amount == 0) break;
count += amount / coin;
amount %= coin;
}
return (amount == 0) ? count : -1;
}
14. Knapsack (Dynamic Programming) Pattern
Solve problems involving choices using dynamic programming (DP).
Use Case: Maximize value within weight constraints.
Example: Solve the 0/1 Knapsack problem.
int knapsack(vector<int>& weights, vector<int>& values, int capacity) {
int n = weights.size();
vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= capacity; w++) {
if (weights[i - 1] <= w) {
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][capacity];
}
15. Subsets Pattern
Solve problems by generating all subsets of a given set.
Use Case: Solve combination or subset problems.
Example: Generate all subsets of a set of numbers.
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result = {{}};
for (int num : nums) {
int n = result.size();
for (int i = 0; i < n; i++) {
vector<int> subset = result[i];
subset.push_back(num);
result.push_back(subset);
}
}
return result;
}
16. Bit Manipulation Pattern
Efficiently solve problems involving bitwise operations.
Use Case: Solve problems using bitwise operators.
Example: Find the single number in an array where every element appears twice except for one.
int singleNumber(vector<int>& nums) {
int result = 0;
for (int num : nums) {
result ^= num;
}
return result;
}
17. Union-Find Pattern
Efficiently solve problems involving connected components or dynamic connectivity.
Use Case: Solve problems involving finding connected components in graphs.
Example: Find if there is a cycle in an undirected graph.
class UnionFind {
public:
vector<int> parent, rank;
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int p) {
if (parent[p] != p) parent[p] = find(parent[p]);
return parent[p];
}
bool unionSets(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return false;
if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
else if (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
else {
parent[rootQ] = rootP;
rank[rootP]++;
}
return true;
}
};
18. Monotonic Stack Pattern
Use a stack to maintain a sequence of elements that are either increasing or decreasing to solve certain problems.
Use Case: Solve problems involving the next greater/smaller element.
Example: Find the next greater element for each element in an array.
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> result(nums.size(), -1);
stack<int> s;
for (int i = 0; i < nums.size(); i++) {
while (!s.empty() && nums[s.top()] < nums[i]) {
result[s.top()] = nums[i];
s.pop();
}
s.push(i);
}
return result;
}
19. Dynamic Programming Pattern
Solve problems by breaking them down into overlapping subproblems and storing the results of subproblems to avoid redundant computations.
Use Case: Solve problems involving optimal substructure and overlapping subproblems.
Example: Solve the longest increasing subsequence problem.
int longestIncreasingSubsequence(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int> dp(nums.size(), 1);
int maxLength = 1;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
maxLength = max(maxLength, dp[i]);
}
return maxLength;
}
20. Segment Tree Pattern
Efficiently solve range query problems by using a segment tree data structure.
Use Case: Solve problems involving range queries and updates on an array.
Example: Range sum query and update on an array.
class SegmentTree {
private:
vector<int> tree;
int n;
void buildTree(vector<int>& nums, int start, int end, int node) {
if (start == end) {
tree[node] = nums[start];
return;
}
int mid = start + (end - start) / 2;
buildTree(nums, start, mid, 2 * node + 1);
buildTree(nums, mid + 1, end, 2 * node + 2);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
void updateTree(int start, int end, int idx, int val, int node) {
if (start == end) {
tree[node] = val;
return;
}
int mid = start + (end - start) / 2;
if (idx <= mid) updateTree(start, mid, idx, val, 2 * node + 1);
else updateTree(mid + 1, end, idx, val, 2 * node + 2);
tree[node] = tree[2 * node + 1] + tree[2 * node + 2];
}
int rangeSum(int start, int end, int l, int r, int node) {
if (start > r || end < l) return 0;
if (start >= l && end <= r) return tree[node];
int mid = start + (end - start) / 2;
return rangeSum(start, mid, l, r, 2 * node + 1) + rangeSum(mid + 1, end, l, r, 2 * node + 2);
}
public:
SegmentTree(vector<int>& nums) {
n = nums.size();
tree.resize(4 * n);
buildTree(nums, 0, n - 1, 0);
}
void update(int idx, int val) {
updateTree(0, n - 1, idx, val, 0);
}
int sumRange(int l, int r) {
return rangeSum(0, n - 1, l, r, 0);
}
};
These 20 problem-solving patterns are crucial for mastering DSA. Each pattern can be applied to a wide range of real-world problems, providing an efficient path to optimal solutions.
Posted on October 25, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.