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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | 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.
01 02 03 04 05 06 07 08 09 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 40 41 42 43 44 45 46 47 | 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.
01 02 03 04 05 06 07 08 09 10 | 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.
01 02 03 04 05 06 07 08 09 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 40 41 42 43 44 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | 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.
01 02 03 04 05 06 07 08 09 10 | 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.
01 02 03 04 05 06 07 08 09 10 11 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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.