Master LeetCode with These 10 Proven Patterns
LeetCode is a popular platform for practicing coding problems and preparing for technical interviews. While many find LeetCode challenging, understanding common problem-solving patterns can significantly improve your ability to tackle these problems effectively. This article will introduce you to 10 proven patterns that can help you master LeetCode and become a more proficient programmer.
By familiarizing yourself with these patterns, you’ll gain a deeper understanding of problem-solving techniques, develop a more systematic approach to coding, and increase your confidence in tackling complex LeetCode challenges.
Let’s Master LeetCode with These 10 Proven Patterns
1. Two Pointers
Definition: The two-pointers pattern involves using two pointers to iterate through a data structure, often an array or linked list, to solve problems by narrowing down the search space.
Common Use Cases:
- Finding pairs of elements that satisfy a condition.
- Merging sorted arrays or linked lists.
- Validating palindromes.
Example:
Problem: Given a sorted array of integers, find the pair of elements that sum to a given target value.
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int currentSum = nums[left] + nums[right];
if (currentSum == target) {
return new int[] { left, right };
} else if (currentSum < target) {
left++;
} else {
right--;
}
}
return new int[] {}; // Return empty if no pair is found
}
2. Sliding Window
Definition: The sliding window pattern involves maintaining a window of a fixed or variable size that slides over the data structure to solve problems involving subarrays or substrings.
Common Use Cases:
- Finding maximum or minimum subarrays.
- Counting occurrences of elements within a window.
Example:
Problem: Given an array of integers and a window size, find the maximum sum of a subarray of that size.
public int maxSumSubarray(int[] nums, int k) {
int maxSum = 0, windowSum = 0;
for (int i = 0; i < k; i++) {
windowSum += nums[i];
}
maxSum = windowSum;
for (int i = k; i < nums.length; i++) {
windowSum += nums[i] - nums[i - k];
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
3. Binary Search
Definition: The binary search pattern is used to efficiently search for a target value in a sorted array or list by dividing the array in half repeatedly.
Common Use Cases:
- Searching for a specific element in a sorted array.
- Finding the first or last occurrence of an element.
Example:
Problem: Given a sorted array of integers and a target value, find the index of the target value in the array.
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // Return -1 if target not found
}
4. Divide and Conquer
Definition: Divide and conquer involves breaking down a problem into smaller subproblems, solving the subproblems recursively, and combining the solutions to solve the original problem.
Common Use Cases:
- Sorting algorithms (e.g., merge sort, quicksort).
- Searching algorithms (e.g., binary search).
Example:
Problem: Implement merge sort to sort an array of integers.
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] L = new int[n1];
int[] R = new int[n2];
for (int i = 0; i < n1; ++i) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; ++j) {
R[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
5. Dynamic Programming
Definition: Dynamic programming involves storing the results of subproblems to avoid recomputation.
Common Use Cases:
- Fibonacci sequence.
- Knapsack problem.
- Longest common subsequence.
Example:
Problem: Given a staircase with n steps, find the number of distinct ways to climb to the top, given that you can take either 1 or 2 steps at a time.
public int climbStairs(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
6. Backtracking
Definition: Backtracking involves exploring all possible solutions and discarding those that don’t lead to the desired solution.
Common Use Cases:
- Solving puzzles (e.g., Sudoku, N-Queens).
- Generating permutations and combinations.
Example:
Problem: Solve the N-Queens problem for an n x n chessboard.
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions = new ArrayList<>();
char[][] board = new char[n][n];
for (char[] row : board) {
Arrays.fill(row, '.');
}
solve(0, board, solutions);
return solutions;
}
private void solve(int col, char[][] board, List<List<String>> solutions) {
if (col == board.length) {
solutions.add(construct(board));
return;
}
for (int row = 0; row < board.length; row++) {
if (isValid(board, row, col)) {
board[row][col] = 'Q';
solve(col + 1, board, solutions);
board[row][col] = '.';
}
}
}
private boolean isValid(char[][] board, int row, int col) {
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 < board.length && j >= 0; i++, j--) {
if (board[i][j] == 'Q') return false;
}
return true;
}
private List<String> construct(char[][] board) {
List<String> path = new ArrayList<>();
for (char[] row : board) {
path.add(new String(row));
}
return path;
}
7. Greedy Algorithm
Definition: The greedy algorithm pattern involves making locally optimal choices at each step, hoping to arrive at a globally optimal solution.
Common Use Cases:
- Activity selection problem.
- Dijkstra’s algorithm for shortest path.
Example:
Problem: Given a set of activities with start and finish times, find the maximum number of activities that can be scheduled without overlapping.
public int maxActivities(int[] start, int[] finish) {
int n = start.length;
int[][] activities = new int[n][2];
for (int i = 0; i < n; i++) {
activities[i][0] = start[i];
activities[i][1] = finish[i];
}
Arrays.sort(activities, Comparator.comparingInt(a -> a[1]));
int count = 1;
int lastFinishTime = activities[0][1];
for (int i = 1; i < n; i++) {
if (activities[i][0] >= lastFinishTime) {
count++;
lastFinishTime = activities[i][1];
}
}
return count;
}
8. Two Pointers (Reverse)
Definition: This pattern is similar to the Two Pointers pattern but involves moving the pointers in opposite directions.
Common Use Cases:
- Reversing arrays or linked lists.
- Finding pairs of elements that satisfy a certain condition.
Example:
Problem: Given an array of integers, reverse the array in place.
public void reverseArray(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
9. Hash Table
Definition: The hash table pattern involves using a hash table to store key-value pairs efficiently.
Common Use Cases:
- Checking for duplicates in an array.
- Implementing frequency counters.
Example:
Problem: Given an array of integers, find the two numbers that add up to a given target value.
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
return new int[] {}; // Return empty if no pair found
}
10. Graph Algorithms
Definition: Graph algorithms solve problems on graphs (e.g., directed or undirected).
Common Use Cases:
- Breadth-first search (BFS) for finding shortest paths.
- Depth-first search (DFS) for exploring graphs.
Example:
Problem: Given a graph, find all connected components using DFS.
public void dfs(int node, List<List<Integer>> graph, boolean[] visited) {
visited[node] = true;
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor, graph, visited);
}
}
}
public List<List<Integer>> findConnectedComponents(int n, List<List<Integer>> graph) {
boolean[] visited = new boolean[n];
List<List<Integer>> components = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!visited[i]) {
List<Integer> component = new ArrayList<>();
dfs(i, graph, visited);
components.add(component);
}
}
return components;
}
Conclusion
In this article, we explored 10 proven problem-solving patterns essential for mastering LeetCode and improving algorithmic thinking. These patterns—such as Two Pointers, Sliding Window, Binary Search, Divide and Conquer, and others—are widely applicable to a variety of coding problems. Understanding these patterns helps in breaking down complex challenges into simpler steps, enabling the efficient development of solutions.
Each pattern was accompanied by a Java code example, demonstrating how to implement these strategies in real-world scenarios. Whether you’re tackling problems involving arrays, graphs, or dynamic programming, recognizing the appropriate pattern can significantly enhance your problem-solving speed and accuracy. Mastering these patterns is key to excelling in technical interviews and becoming a stronger software engineer.


