Core Java

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

leetcode logo

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:

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.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button