Home/Blog/Interview Prep/10+ top LeetCode patterns (2025) to ace FAANG coding interviews
Home/Blog/Interview Prep/10+ top LeetCode patterns (2025) to ace FAANG coding interviews

10+ top LeetCode patterns (2025) to ace FAANG coding interviews

18 min read
Jul 29, 2025
content
Why patterns beat problem-grinding
LeetCode tag frequency (July 2025)
Cheat-sheet of LeetCode patterns
How each pattern maps to LeetCode categories
LeetCode Pattern playbook
1. Two Pointers
2. Sliding Window
3. Fast and Slow Pointers (Cycle detection)
4. Depth-First Search (DFS)
5. Breadth-First Search (BFS)
6. Binary Search
7. Merge Intervals
8. Topological Sort
9. Backtracking
10. Union-Find (Disjoint Set)
11. Monotonic Stack
12. Dynamic Programming (DP) - Advanced
Common coding interview mistakes
Next steps
Recommended resources

Coding interviews are structured problem-solving challenges. Designed to evaluate how you think, communicate, and write code under pressure, these interviews simulate real-world engineering challenges in a time-constrained environment. It’s no wonder that many candidates fall into the trap of trying to power through their preparation.

Grokking the Coding Interview Patterns

Cover
Grokking the Coding Interview Patterns

With thousands of potential questions to account for, preparing for the coding interview can feel like an impossible challenge. Yet with a strategic approach, coding interview prep doesn’t have to take more than a few weeks. Stop drilling endless sets of practice problems, and prepare more efficiently by learning coding interview patterns. This course teaches you the underlying patterns behind common coding interview questions. By learning these essential patterns, you will be able to unpack and answer any problem the right way — just by assessing the problem statement. This approach was created by FAANG hiring managers to help you prepare for the typical rounds of interviews at major tech companies like Apple, Google, Meta, Microsoft, and Amazon. Before long, you will have the skills you need to unlock even the most challenging questions, grok the coding interview, and level up your career with confidence. This course is also available in JavaScript, Python, Go, and C++ — with more coming soon!

85hrs
Intermediate
411 Challenges
412 Quizzes

Anyone aiming for a top-tier tech interview has probably heard the classic advice: 'grind LeetCode.' Most developers think the more problems they solve, the better their chances. As someone who has coached many engineers through this process, I can confidently tell you that this approach is ineffective.

Stop solving 2,500+ LeetCode problems.

Instead master these 10+ patterns.

Trusted by over 4,200 engineers on our platform who landed FAANG+ offers in 2025.

The FAANG interviews are structured around a core set of repeating patterns. Recent data from hundreds of real interviews at Google, Meta, Apple, Netflix, and Amazon shows that about 87% of questions are built around only 10 to 12 core problem‑solving patterns. Interviewers are not testing your memory of specific problems. They want to see how well you recognize underlying patterns and apply the correct algorithm quickly.

What to expect in this blog:

You’ll first see why patterns matter, then a quick reference cheat sheet of the most important patterns. After that, you’ll see a heatmap of where these patterns apply across categories, then dive into detailed explanations, examples, and live code examples for each pattern. Finally, you’ll find common pitfalls, additional patterns, and recommended resources.

Learn how to:

  • Understand each core pattern with clear examples.

  • Apply each pattern with clear explanations on when to use it.

  • Reinforce your intuition with practical examples.

  • Illustrate each pattern with variations of LeetCode problems.

Why patterns beat problem-grinding#

You’ve seen how overwhelming LeetCode can be. Jumping between random problems might feel productive, but it rarely builds the deep intuition you need. Many developers fall into randomly grinding these problems, hoping they’ll encounter similar problems in the interview room.

The problem with this approach is simple: there’s no finish line, and without a framework, you don’t retain much.

This shift to a pattern-based approach isn’t just a study hack; it aligns with how modern interviews and search algorithms evaluate expertise. For instance, the Google core algorithm update in March 2025 significantly elevated content demonstrating practical knowledge and fresh insights. Similarly, top-tier companies aren’t seeking candidates who can recall memorized solutions. They want engineers who demonstrate a transparent, structured problem-solving process. So, the more innovative way is to group problems by the patterns they share. Once you learn a pattern deeply, you can apply it to dozens of similar problems without starting from scratch every time.

Benefits of focusing on patterns:

  1. You cut prep time dramatically by targeting high‑leverage ideas.

  2. You get faster at identifying the right approach under interview pressure.

  3. You build a reusable mental model that works across many problem types.

LeetCode tag frequency (July 2025)#

To put this into perspective, look at the distribution of problem categories on LeetCode right now.

The chart above shows the raw counts of problems tagged under each category on LeetCode as of July 2025. Some tags, like Array and Dynamic Programming, dominate. Others, like Union‑Find or Topological sort, appear less frequently but still appear in specialized interview questions. There’s no way to solve them all individually, and you don’t need to. So, when you see the sheer scale of these numbers and know how a pattern applies, you can pick representative problems for each pattern instead of grinding through everything.

Note: Cover more ground with less effort by solving a few representative problems for each core pattern.

Cheat-sheet of LeetCode patterns#

Before we discuss the patterns in detail, here’s a quick-reference summary of the key LeetCode patterns commonly tested in coding interviews at MAANG companies. Use this table for rapid review or as a convenient reference during your interview prep.

Pattern

Core Idea

Canonical Problem

Two Pointers

Move two pointers toward each other or in the same direction to efficiently solve array or linked list problems.

Sliding Window

Use a dynamic or fixed-size window over arrays or strings to solve problems related to subarrays and substrings.

Fast and Slow Pointers

Detect cycles using two pointers moving at different speeds, typically in linked lists.

Depth-First Search (DFS)

Traverse deeply along paths, often used for exhaustive searches in trees and graphs.

Breadth-First Search (BFS)

Traverse level-by-level through graphs or trees to find shortest paths or minimum distances.

Binary Search

Quickly find targets in sorted data by repeatedly dividing the search interval in half.

Merge Intervals

Merges overlapping intervals when handling overlapping ranges like time slots or intervals on a number line.

Topological Sort

Sequence tasks or nodes based on dependencies, crucial for scheduling problems.

Dynamic Programming (DP)

Solve complex problems by breaking them into simpler, overlapping subproblems, storing and reusing their solutions.

Union-Find

Quickly check if two elements are in the same set and merge sets efficiently, which is useful for detecting cycles and grouping connected components in graphs

Monotonic Stack and Queue

Maintain data in a strict increasing or decreasing order to efficiently find the next greater or smaller elements.

Backtracking

Explore all candidate solutions, pruning invalid options early, which is often used in permutation problems.

Quick tip: First, focus on the patterns that frequently appear at your target companies (check out our specific guides for Google and Amazon) or patterns you find most challenging.

How each pattern maps to LeetCode categories#

Not all patterns are equally common across all types of problems. So, to help you focus your practice, it’s helpful to see which patterns appear most often across common LeetCode categories. The pattern-to-category relevance heatmap below shows the strength of each high‑leverage pattern (rows) across LeetCode problem categories where it most often applies, like Arrays, Strings, Graphs, and Trees (columns), based on trends from real FAANG/MAANG interview questions.

Note: You can scroll horizontally to see all categories. This way, you won’t miss any part of the mapping.

Pattern

Array

String

Linked List

Graph

Tree

Matrix

Sorting

Searching

DP

Stack

Two Pointers

🟩🟩🟩🟩🟩

🟩🟩🟩🟩

🟩🟩🟩🟩

⬜️

⬜️

⬜️

🟩🟩

⬜️

⬜️

⬜️

Sliding Window

🟩🟩🟩🟩🟩

🟩🟩🟩🟩🟩

⬜️

⬜️

⬜️

🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

Fast and Slow Pointers

🟩🟩🟩

⬜️

🟩🟩🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

Depth‑First Search (DFS)

⬜️

⬜️

⬜️

🟩🟩🟩🟩🟩

🟩🟩🟩🟩🟩

🟩🟩🟩

⬜️

🟩🟩🟩

⬜️

⬜️

Breadth‑First Search (BFS)

⬜️

⬜️

⬜️

🟩🟩🟩🟩🟩

🟩🟩🟩🟩🟩

🟩🟩🟩

⬜️

🟩🟩

⬜️

⬜️

Binary Search

🟩🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

🟩🟩🟩

⬜️

🟩🟩🟩🟩🟩

⬜️

⬜️

Topological Sort

⬜️

⬜️

⬜️

🟩🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

Dynamic Programming

🟩🟩🟩🟩

🟩🟩🟩🟩

⬜️

⬜️

⬜️

🟩🟩🟩🟩

⬜️

⬜️

🟩🟩🟩🟩🟩

⬜️

Union‑Find

⬜️

⬜️

⬜️

🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

Monotonic Stack and Queue

🟩🟩🟩🟩

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

⬜️

🟩🟩🟩🟩

Backtracking (Advanced)

🟩🟩🟩

🟩🟩🟩🟩

⬜️

🟩🟩

🟩🟩

⬜️

⬜️

⬜️

⬜️

⬜️

Legend:

  • 🟩🟩🟩🟩🟩 Very frequent

  • 🟩🟩🟩🟩 Common

  • 🟩🟩🟩 Moderate

  • ⬜️ Rare or uncommon

Quick tip: Solve a handful of well‑chosen problems for each pattern. This approach makes your practice sessions more effective and prepares you smarter rather than harder.

LeetCode Pattern playbook#

In this section, you’ll learn the top LeetCode patterns with visual intuitions, classic examples, runnable code, and real-world case studies from FAANG interviews.

1. Two Pointers#

When to use

Use the Two Pointers pattern when you’re dealing with sorted arrays or linked lists, and you need to find pairs, detect duplicates, or solve problems involving subarray boundaries.

Visual intuition

Imagine standing at the two ends of a sorted array. You have one pointer starting from the left, and the other from the right. Depending on your goal, such as finding a specific sum, you strategically move the pointers toward each other. If the combined values at your pointers are larger than your target, you shift the right pointer to a smaller number. If they’re smaller, you move the left pointer to a larger number. You converge on your solution by systematically adjusting your pointers without unnecessary checks.

Canonical problem: 3Sum—Find all unique triplets in an array that sum to zero.

def three_sum(nums):
# Sort the input array in ascending order
nums.sort()
# Create an empty array to store the unique triplets
result = []
# Store the length of the array in a variable
n = len(nums)
# Iterate over the array till n - 2
for i in range(n - 2):
# If the current number is greater than 0, break the loop
if nums[i] > 0:
break
# The current number is either the first element or not a duplicate of the previous element
if i == 0 or nums[i] != nums[i - 1]:
# Initialize two pointers
low, high = i + 1, n - 1
# Run a loop as long as low is less than high
while low < high:
# Calculate the sum of the triplet
sum = nums[i] + nums[low] + nums[high]
# If the sum is less than 0, move the low pointer forward
if sum < 0:
low += 1
# If the sum is greater than 0, move the high pointer backward
elif sum > 0:
high -= 1
else:
# Add the triplet to result as this triplet sums to 0
result.append([nums[i], nums[low], nums[high]])
# Move both pointers to the next distinct values to avoid duplicate triplets
low += 1
high -= 1
while low < high and nums[low] == nums[low - 1]:
low += 1
while low < high and nums[high] == nums[high + 1]:
high -= 1
# Return result, which contains all unique triplets that sum to zero
return result
# Driver code
def main():
nums_arrs = [
[-1, 0, 1, 2, -1, -4],
[1, 2, 3, 4, 5],
[0, 0, 0, 0],
[-4, -1, -1, 0, 1, 2, 2],
[-10, -7, -3, -1, 0, 3, 7, 10],
[-3, -5, -7, -9]
]
for i, nums in enumerate(nums_arrs):
print(f"{i + 1}.\tnums: [{', '.join(map(str, nums))}]\n")
print(f"\tTriplets: {three_sum(nums)}")
print('-' * 100)
if __name__ == "__main__":
main()
3Sum using Two Pointers

MAANG mini-case study: A recent Google interview question asked candidates to find pairs in a sorted array whose sum was closest to a target number. Candidates who recognized it as a Two Pointers problem solved it in minutes, while others struggled with less efficient brute-force approaches.

Related variations: Sort Colors, Container With Most Water

2. Sliding Window#

When to use

The Sliding Window technique is ideal for problems involving contiguous sequences like arrays or strings, especially when finding the longest or shortest substring, subarray, or sequence that meets specific conditions.

Visual intuition

Think of a sliding window like a virtual “frame” that moves over an array or string. Start by setting the window boundaries (usually two pointers at the beginning), then slide these boundaries forward step-by-step. Depending on your problem constraints, you either expand the window, shrink it, or maintain a fixed window size.

Canonical problem: Longest Substring Without Repeating Characters—Given a string, find the length of the longest substring without repeating characters.

def find_longest_substring(input_str):
# Check the length of input_str
if len(input_str) == 0:
return 0
window_start, longest, window_length = 0, 0, 0
last_seen_at = {}
# Traverse str to find the longest substring
# without repeating characters.
for index, val in enumerate(input_str):
# If the current element is not present in the hash map,
# then store it in the hash map with the current index as the value.
if val not in last_seen_at:
last_seen_at[val] = index
else:
# If the current element is present in the hash map,
# it means that this element may have appeared before.
# Check if the current element occurs before or after `window_start`.
if last_seen_at[val] >= window_start:
window_length = index - window_start
if longest < window_length:
longest = window_length
window_start = last_seen_at[val] + 1
# Update the last occurrence of
# the element in the hash map
last_seen_at[val] = index
index += 1
# Update the longest substring's
# length and starting index.
if longest < index - window_start:
longest = index - window_start
return longest
# Driver code
def main():
string = [
"abcabcbb",
"pwwkew",
"bbbbb",
"ababababa",
"",
"ABCDEFGHI",
"ABCDEDCBA",
"AAAABBBBCCCCDDDD",
]
for i in range(len(string)):
print(i + 1, ". \t Input String: ", string[i], sep="")
print("\t Length of longest substring: ",
(find_longest_substring(string[i])))
print("-" * 100)
if __name__ == "__main__":
main()
Longest Substring without Repeating Characters using Sliding Window

MAANG mini-case study: A recent Meta interview question involved finding the smallest subarray whose sum exceeded a given number. Candidates who identified this as a Sliding Window problem solved it quickly, while others struggled with more complex solutions.

Related variations: Minimum Window Substring, Minimum Size Subarray Sum

3. Fast and Slow Pointers (Cycle detection)#

When to use

Use the Fast and Slow Pointers technique to detect cycles or loops, typically in linked lists or circular data structures. It can also help identify midpoints or specific positions without needing extra space.

Visual intuition

Imagine two runners on a circular track: one runner moves slowly, steadily, and the other moves twice as fast. If the track forms a closed loop, eventually the fast runner will lap the slower one. Similarly, using two pointers moving at different speeds through a linked list will reveal whether or not a loop exists. If the two pointers ever meet, you’ve detected a cycle.

Canonical Problem: Linked List Cycle—Given a linked list, determine if it contains a cycle. Return true if there’s a cycle; otherwise, return false.

Solution.py
ListNode.py
LinkedList.py
PrintList.py
def detect_cycle(head):
if head is None:
return False
# Initialize two pointers, slow and fast, to the head of the linked list
slow, fast = head, head
# Run the loop until we reach the end of the
# linked list or find a cycle
while fast and fast.next:
# Move the slow pointer one step at a time
slow = slow.next
# Move the fast pointer two steps at a time
fast = fast.next.next
# If there is a cycle, the slow and fast pointers will meet
if slow == fast:
return True
# If we reach the end of the linked list and haven't found a cycle, return False
return False
# Driver code
def main():
input = (
[2, 4, 6, 8, 10, 12],
[1, 3, 5, 7, 9, 11],
[0, 1, 2, 3, 4, 6],
[3, 4, 7, 9, 11, 17],
[5, 1, 4, 9, 2, 3],
)
pos = [0, -1, 1, -1, 2]
j = 1
for i in range(len(input)):
input_linked_list = LinkedList()
input_linked_list.create_linked_list(input[i])
print(f"{j}.\tInput: ", sep="", end="")
if pos[i] == -1:
print_list_with_forward_arrow(input_linked_list.head)
else:
print_list_with_forward_arrow_loop(input_linked_list.head)
print("\n\tpos:", pos[i])
if pos[i] != -1:
length = input_linked_list.get_length(input_linked_list.head)
last_node = input_linked_list.get_node(input_linked_list.head, length - 1)
last_node.next = input_linked_list.get_node(input_linked_list.head, pos[i])
print(f"\n\tDetected cycle = {detect_cycle(input_linked_list.head)}")
j += 1
print("-"*100, "\n")
if __name__ == "__main__":
main()
Linked List Cycle using Fast and Slow Pointers

MAANG mini-case study: Apple often asks candidates to detect or remove cycles in linked lists during coding interviews. Candidates recognizing the Fast & Slow Pointers approach can confidently deliver an efficient solution without relying on additional data structures, impressing interviewers by demonstrating clear, optimized thinking.

Related variations: Middle of the Linked List, Happy Number

4. Depth-First Search (DFS)#

When to use

Depth-first search (DFS) is ideal when exploring deep paths before considering alternative options. It is intuitive and straightforward to implement and is commonly used in scenarios where exhaustive exploration or discovery of all possible solutions is required. It works particularly well for problems involving backtracking, pathfinding, connected components, or detecting cycles.

Visual intuition

Imagine exploring a graph or tree: you start at one node and follow a single branch as far as it goes before backtracking to explore other branches. Depth‑First Search is precisely that. In trees and graphs, it dives deep along one path, using recursion or a stack, before returning to handle the remaining neighbors.

Canonical problem: Diameter of Binary Tree—Given a binary tree, you need to compute the length of the tree’s diameter.

main.py
BinaryTree.py
TreeNode.py
from TreeNode import *
from BinaryTree import *
# function to recursively calculate the height of the tree
# and update the diameter
def diameter_helper(node, diameter):
if node is None:
return 0, diameter
else:
# Compute the height of each subtree
lh, diameter = diameter_helper(node.left, diameter)
rh, diameter = diameter_helper(node.right, diameter)
# update the result with the max of the previous and current diameter value
diameter = max(diameter, lh + rh)
# Use the larger one
return max(lh, rh) + 1, diameter
def diameter_of_binaryTree(root):
# variable for diameter
diameter = 0
if not root:
return 0
# compute the height of the tree and the max diameter
_, diameter = diameter_helper(root, diameter)
# return the diameter
return diameter
# Driver code
def main():
list_of_trees = [ [TreeNode(2), TreeNode(1), TreeNode(4), TreeNode(3), TreeNode(5), TreeNode(6), TreeNode(7)],
[TreeNode(1), TreeNode(2), TreeNode(3), TreeNode(4), TreeNode(5), TreeNode(6), TreeNode(7), TreeNode(8), TreeNode(9)],
[TreeNode(45), TreeNode(32), TreeNode(23), TreeNode(21), TreeNode(18), TreeNode(1)],
[TreeNode(5), TreeNode(3), TreeNode(4), TreeNode(1), TreeNode(2), TreeNode(6), TreeNode(7), TreeNode(8), TreeNode(9)],
[TreeNode(-1), TreeNode(-5), TreeNode(-8), TreeNode(-3), TreeNode(1), TreeNode(5), TreeNode(3)],
[TreeNode(9), TreeNode(7), None, None, TreeNode(1), TreeNode(8), TreeNode(10), None, TreeNode(12)]
]
# Create the binary trees using the BinaryTree class
input_trees = []
for list_of_nodes in list_of_trees:
tree = BinaryTree(list_of_nodes)
input_trees.append(tree)
y = 1
for tree in input_trees:
print(y, ".\tInput Tree:", sep = "")
display_tree(tree.root)
print("\tDiameter of the tree: ", diameter_of_binaryTree(tree.root), sep="")
print("-"*100)
y += 1
if __name__ == '__main__':
main()
Diameter of Binary Tree using Depth-First Search

MAANG mini-case study: Meta regularly asks candidates to solve graph and tree traversal problems that require DFS. For instance, a typical interview problem is counting connected groups of friends (similar to islands) in a social network graph. Candidates recognizing DFS can quickly identify connected components, deliver efficient implementations, and impress interviewers with clear and concise solutions.

Related variations: Validate Binary Search Tree, Serialize and Deserialize Binary Tree

5. Breadth-First Search (BFS)#

When to use

Use Breadth-First Search (BFS) when you need the shortest path or the minimum steps to reach a goal, especially in graph or tree structures. BFS explores nodes level-by-level and is ideal for problems involving minimum depth, shortest paths, or closest distances.

Visual intuition

Picture a tree or graph where you want to visit all nodes in order of their distance from the root or start node. Breadth‑First Search explores level by level, visiting all neighbors of a node before moving on to the next layer. It typically uses a queue to keep track of the traversal.

Canonical problem: Binary Tree Level Order Traversal—Given a binary tree, return the level order traversal of its nodes’ values (i.e., from left to right, level by level).

main.py
TreeNode.py
BinaryTree.py
from collections import deque
from BinaryTree import *
from TreeNode import *
# Perform level-order (breadth-first) traversal on a binary tree.
# Returns a string representation of node values level by level.
def level_order_traversal(root):
result = []
# If the tree is empty, return "None"
if not root:
result = "None"
return result
# Initialize a queue for BFS starting with the root
current_queue = deque()
current_queue.append(root)
# Process nodes level by level
while current_queue:
level_size = len(current_queue) # Number of nodes in the current level
level_nodes = [] # Collect values for this level
# Process each node in the current level
for _ in range(level_size):
temp = current_queue.popleft() # Dequeue the next node
level_nodes.append(str(temp.data)) # Add its value to this level's list
# Enqueue left child if it exists
if temp.left:
current_queue.append(temp.left)
# Enqueue right child if it exists
if temp.right:
current_queue.append(temp.right)
# Join all values in this level and append to result
result.append(", ".join(level_nodes))
# Join all levels with a separator
return " : ".join(result)
def main():
# Prepare test cases
test_cases_roots = []
# First tree
input1 = [
TreeNode(100),
TreeNode(50),
TreeNode(200),
TreeNode(25),
TreeNode(75),
TreeNode(350)
]
tree1 = BinaryTree(input1)
test_cases_roots.append(tree1.root)
# Second tree with some missing nodes (represented by None)
input2 = [
TreeNode(25),
TreeNode(50),
None,
TreeNode(100),
TreeNode(200),
TreeNode(350)
]
tree2 = BinaryTree(input2)
test_cases_roots.append(tree2.root)
# Third tree skewed toward right
input3 = [
TreeNode(350),
None,
TreeNode(100),
None,
TreeNode(50),
TreeNode(25)
]
tree3 = BinaryTree(input3)
test_cases_roots.append(tree3.root)
# Fourth tree with just one node
tree4 = BinaryTree([TreeNode(100)])
test_cases_roots.append(tree4.root)
# Fifth test case: empty tree
test_cases_roots.append(None)
# Run level-order traversal on each test case
for i in range(len(test_cases_roots)):
if i > 0:
print()
print(i + 1, ".\tBinary Tree")
display_tree(test_cases_roots[i]) # Display tree structure visually
print("\n\tLevel order traversal: ")
print("\t", level_order_traversal(test_cases_roots[i]))
print("\n" + '-' * 100)
if __name__ == '__main__':
main()
Binary Tree Level Order Traversal using Breadth-First Search

MAANG mini-case study: Amazon frequently tests candidates with graph problems involving shortest paths or network latency. Candidates familiar with BFS can quickly implement efficient, queue-based solutions to find the minimum number of hops or shortest routes between two points. This pattern recognition significantly boosts their performance and the interviewer’s perception of their expertise.

Related variations: Word Ladder, Two Sum IV - Input is a BST

When to use

Use Binary Search when working with sorted data or when a problem’s search space can be reasoned about in a monotonic way (for example, “is it possible with X?” leading to yes/no answers). In interviews, you often don’t use the vanilla version; instead, you modify the classic binary search algorithm to fit conditions like finding the first/last occurrence, a boundary value, or even searching over an answer space rather than an array.

Visual intuition

Imagine trying to find a specific word in a dictionary. Instead of flipping through pages from the start, you naturally open near the middle. Depending on whether the word you’re looking for comes before or after, you discard half of the remaining pages immediately. Binary Search applies this logic: repeatedly dividing your search interval in half until your target is found.

Canonical problem: Binary Search—Given a sorted array of integers and a target value, return the index of the target if it exists. Otherwise, return -1.

def binary_search(nums, target):
low = 0
high = len(nums) - 1
while low <= high:
# Finding the mid using floor division
mid = low + ((high - low) // 2)
# Target value is present at the middle of the array
if nums[mid] == target:
return mid
# Target value is present in the low subarray
elif target < nums[mid]:
high = mid - 1
# Target value is present in the high subarray
elif target > nums[mid]:
low = mid + 1
# Target value is not present in the array
return -1
def main():
nums_lists = [
[1],
[0, 1],
[1, 2, 3],
[-1, 0, 3, 5, 9, 12],
[-100, -67, -55, -50, -49, -40, -33, -22, -10, -5]
]
target_list = [12, 1, 3, 9, -22]
for i in range(len(nums_lists)):
nums = nums_lists[i]
target = target_list[i]
index = binary_search(nums, target)
print(i+1, ".\tArray to search: ", nums, sep="")
print("\tTarget: ", target, sep="")
if index != -1:
print("\t", target, " exists in the array at index ", index, sep="")
else:
print("\t", target, " does not exist in the array so the return value is ", index, sep="")
print('-' * 100)
if __name__ == '__main__':
main()
Binary Search

MAANG mini-case study: Netflix commonly tests candidates on problems requiring efficient searches through large, sorted data sets, such as logs or media catalogs. Candidates who recognize the potential of Binary Search significantly outperform those who rely on linear or brute-force approaches, showcasing their ability to handle large-scale data efficiently.

Related variations: Search in Rotated Sorted Array, Find K Closest Elements

7. Merge Intervals#

When to use
Use Merge Intervals when handling problems that involve overlapping ranges, such as time slots, calendar events, or number‑line segments. You want to merge these intervals so that no overlaps remain, producing a minimal set of non‑overlapping intervals.

Visual intuition
Imagine you’re managing meeting schedules. Each meeting is a block on a timeline. When two meetings overlap, you merge them into a longer meeting covering both. This pattern sorts intervals by start time and merges them greedily as you scan from left to right.

Canonical problem: Merge Intervals—Given an array of intervals, merge all overlapping intervals and return an array of non‑overlapping intervals that cover all input intervals.

def merge_intervals(intervals):
# Sort intervals by their starting value to process in order
intervals.sort(key=lambda x: x[0])
# Initialize the result list with the first interval
result = [intervals[0]]
# Iterate through the remaining intervals
for i in range(1, len(intervals)):
# If the current interval overlaps with the last interval in result
if intervals[i][0] <= result[-1][1]:
# Merge by updating the end of the last interval in result
result[-1][1] = max(result[-1][1], intervals[i][1])
else:
# If no overlap, simply add the current interval to result
result.append([intervals[i][0], intervals[i][1]])
# Return the merged list of intervals
return result
# Driver code to test merge_intervals with multiple examples
def main():
# List of interval sets to test
all_intervals = [
[[3, 7], [1, 5], [4, 6]],
[[1, 5], [6, 8], [4, 6], [11, 15]],
[[3, 7], [10, 12], [6, 8], [11, 15]],
[[1, 5]],
[[1, 9], [4, 4], [3, 8]],
[[1, 2], [8, 8], [3, 4]],
[[1, 5], [1, 3]],
[[1, 5], [6, 9]],
[[0, 0], [1, 18], [1, 3]],
]
# Loop through each test case
for i in range(len(all_intervals)):
print(i + 1, ".\tIntervals to merge: ", all_intervals[i], sep="")
# Call merge_intervals on the current set
result = merge_intervals(all_intervals[i])
# Print the merged output
print("\tMerged intervals:\t", result)
print("-" * 100)
# Run the driver code
if __name__ == '__main__':
main()
Merge Intervals

MAANG mini-case study: Google and Amazon often ask scheduling or resource allocation questions, where recognizing the Merge Intervals pattern leads to an efficient solution.

Related variations: Meeting Rooms II, Employee Free Time

8. Topological Sort#

When to use

Topological Sort is used when tasks or items have clear dependencies, and you must find an order of execution that respects these dependencies. It’s commonly applied in course scheduling, prerequisite resolution, or dependency graphs.

Visual intuition

Think of Topological Sort as planning a series of tasks, where some tasks depend on others. Imagine you need to take specific courses before graduating: “Algorithms” requires “Data Structures,” and “Data Structures” requires “Intro to Programming.” Topological sort helps you arrange these courses into a sequence that satisfies all prerequisites.

To achieve this, the algorithm repeatedly selects nodes with no incoming dependencies and places them in order. Each time you remove a node, dependencies change, making new nodes eligible. The result is a linear sequence that respects all constraints.

Canonical problem: Course Schedule—Given a number of courses and prerequisites, determine if it’s possible to complete all courses.

from collections import deque
def can_finish(num_courses, prerequisites):
counter = 0
if num_courses <= 0:
return True
# a. Initialize the graph
# count of incoming prerequisites
inDegree = {i: 0 for i in range(num_courses)}
graph = {i: [] for i in range(num_courses)} # adjacency list graph
# b. Build the graph
for edge in prerequisites:
parent, child = edge[1], edge[0]
graph[parent].append(child) # put the child into it's parent's list
inDegree[child] += 1 # increment child's inDegree
# c. Find all sources i.e., all num_courses with 0 in-degrees
sources = deque()
for key in inDegree:
if inDegree[key] == 0:
sources.append(key)
# d. For each source, increment the counter and subtract one from
# all of its children's in-degrees
# if a child's in-degree becomes zero, add it to the sources queue
while sources:
course = sources.popleft()
counter += 1
# get the node's children to decrement their in-degrees
for child in graph[course]:
inDegree[child] -= 1
if inDegree[child] == 0:
sources.append(child)
# topological sort is not possible if the counter is not equal to num_courses
return counter == num_courses
# Driver code
def main():
prereq = [
[[1, 0], [2, 1]],
[[1, 0], [0, 1]],
[[1, 0], [2, 1], [3, 2], [4, 3]],
[[1, 0], [2, 1], [3, 2], [4, 3], [0, 4]],
[[2, 0], [2, 1], [3, 2], [4, 2], [3, 1]]
]
courses = [3, 2, 10, 5, 5]
for i in range(len(courses)):
print((i + 1), ".\tNumber of courses: ", courses[i], sep="")
print("\tNumber of pre-requisites: ", prereq[i])
print("\tOutput: ", can_finish(courses[i], prereq[i]))
print('-'*100)
if __name__ == "__main__":
main()
Course Schedule using Topological Sort

MAANG mini-case study: Google regularly asks interviewees questions involving dependency resolutions, such as build orders for software compilation or task scheduling with constraints. Candidates familiar with Topological Sort can quickly implement efficient solutions and clearly articulate the dependency structure, impressing interviewers with their structured thinking and practical experience.

Related variations: Course Schedule II, Alien Dictionary

9. Backtracking#

When to use

Backtracking is perfect for problems where you need to systematically explore all possible solutions or combinations, typically for permutations, combinations, puzzles (like Sudoku), or constraint-satisfaction problems. It’s beneficial when the problem space is ample, but it can be reduced by pruning invalid options early.

Visual intuition

Think of Backtracking as navigating through a decision tree. At every step, you make a choice, proceed down that path, and see where it takes you. If you encounter a path that leads to no valid solution, you “backtrack,” reverse that choice, and explore another possibility.

Backtracking is a brute-force approach enhanced by intelligently pruning paths that clearly won’t lead to solutions. It ensures that you exhaustively explore valid possibilities without wasting time on clearly invalid scenarios.

Canonical problem: Word Search—Given a 2D grid of characters and a word, we need to determine if the word can be constructed from letters of sequentially adjacent cells.

# Function to search a specific word in the grid
def word_search(grid, word):
n = len(grid)
m = len(grid[0])
for row in range(n):
for col in range(m):
if depth_first_search(row, col, word, 0, grid):
return True
return False
# Apply backtracking on every element to search the required word
def depth_first_search(row, col, word, index, grid):
if len(word) == index:
return True
if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]) \
or grid[row][col] != word[index]:
return False
temp = grid[row][col]
grid[row][col] = '*'
for rowOffset, colOffset in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
if depth_first_search(row + rowOffset, col + colOffset, word, index + 1, grid):
return True
grid[row][col] = temp
return False
def main():
input = [
([['E', 'D', 'X', 'I', 'W'],
['P', 'U', 'F', 'M', 'Q'],
['I', 'C', 'Q', 'R', 'F'],
['M', 'A', 'L', 'C', 'A'],
['J', 'T', 'I', 'V', 'E']], "EDUCATIVE"),
([['E', 'D', 'X', 'I', 'W'],
['P', 'A', 'F', 'M', 'Q'],
['I', 'C', 'A', 'S', 'F'],
['M', 'A', 'L', 'C', 'A'],
['J', 'T', 'I', 'V', 'E']], "PACANS"),
([['h', 'e', 'c', 'm', 'l'],
['w', 'l', 'i', 'e', 'u'],
['a', 'r', 'r', 's', 'n'],
['s', 'i', 'i', 'o', 'r']], "warrior"),
([['C', 'Q', 'N', 'A'],
['P', 'S', 'E', 'I'],
['Z', 'A', 'P', 'E'],
['J', 'V', 'T', 'K']], "SAVE"),
([['O', 'Y', 'O', 'I'],
['B', 'Y', 'N', 'M'],
['K', 'D', 'A', 'R'],
['C', 'I', 'M', 'I'],
['Z', 'I', 'T', 'O']], "DYNAMIC"),
]
num = 1
for i in input:
print(num, ".\tGrid =", sep="")
for j in range(len(i[0])):
print("\t\t", i[0][j])
if i[1] == "":
print('\n\tWord = ""')
else:
print(f"\n\tWord = {i[1]}")
search_result = word_search(i[0], i[1])
if search_result:
print("\n\tSearch result = Word found")
else:
print("\n\tSearch result = Word could not be found")
num += 1
print("-"*100, "\n")
if __name__ == "__main__":
main()
Word Search using Backtracking

MAANG mini-case study: Meta frequently asks backtracking-based interview questions, such as finding combinations or subsets that meet specific constraints. Candidates skilled at backtracking demonstrate their ability to navigate complex problem spaces, make efficient decisions about paths worth exploring, and impress interviewers with systematic thinking.

Related variations: Subsets, Permutations

10. Union-Find (Disjoint Set)#

When to use

Use Union‑Find (also called Disjoint Set Union) to manage and query dynamic connectivity between elements. It’s especially useful in graph problems where you repeatedly check whether two nodes are in the same connected component or need to merge components, such as detecting cycles, counting isolated groups, or managing network connections.

Visual intuition

Think of Union-Find as a way to track friend groups at a party. Initially, everyone is in their group. When two people meet and become friends, their separate groups merge into one larger group. Union-Find efficiently manages this grouping and merging operation, letting you quickly determine whether two people are already part of the same friend group.

The algorithm typically consists of two primary operations:

  1. Find: Determine the group (or parent) to which an element belongs.

  2. Union: Combine two groups into one larger group.

Optimizations like “Path Compression” and “Union by Rank” make this process highly efficient, ensuring near constant time complexity for each operation.

Canonical problem: Number of Provinces—Given an adjacency matrix representing connections between cities, find the number of disconnected groups of cities (provinces).

main.py
union_find.py
from union_find import UnionFind
def find_connected_cities(cities):
n = len(cities)
# Initialize a UnionFind object with n nodes
uf = UnionFind(n)
# Iterate over all pairs of cities
for i in range(n):
for j in range(i+1, n):
# If cities i and j are connected, union their sets
if cities[i][j] == 1:
uf.union(i, j)
# Return the number of connected components
return uf.count
# Driver code
def main():
cities = [
[[1, 1, 0], [1, 1, 0], [0, 0, 1]],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[1, 1, 1], [1, 1, 1], [1, 1, 1]],
[[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 1], [1, 0, 1, 1]],
[[1, 0, 1], [0, 1, 0], [1, 0, 1]],
]
for i, city in enumerate(cities, 1):
print(f"{i}.\tCities: {city}")
print(f"\tNumber of provinces: {find_connected_cities(city)}")
print("-" * 100)
if __name__ == "__main__":
main()
Number of Provinces using Union Find

MAANG mini-case study: Meta frequently asks Union-Find related interview questions, especially handling social network graphs or determining user groups based on connections. Candidates who recognize and apply Union-Find quickly produce optimized solutions, demonstrating algorithmic proficiency to interviewers.

Related variations: Redundant Connection, Accounts Merge

11. Monotonic Stack#

When to use

Use a Monotonic Stack or Queue when dealing with problems finding the next greater or smaller element in an array. These problems usually require maintaining elements in strictly increasing or decreasing order.

Visual intuition

Imagine you are processing stock prices day by day. You want to know how far ahead you have to look to find a higher price each day. As you scan, you keep a stack of prices from previous days in decreasing order. When you encounter a higher price, you pop from the stack until the order is restored, instantly finding answers for multiple days.

Canonical problem: Daily Temperatures—Given an array of daily temperatures, return an array indicating how many days it takes to get a warmer temperature.

def daily_temperatures(temperatures):
n = len(temperatures) # Get the number of days
output = [0] * n # Initialize the output array with all elements set to 0
stack = [] # Initialize an empty stack to hold indices of temperatures
for i in range(n): # Loop through each day
# While the stack is not empty and the current temperature is higher than the temperature
# at the index stored at the top of the stack
while stack and temperatures[i] > temperatures[stack[-1]]:
j = stack.pop() # Pop the top index from the stack
output[j] = i - j # Calculate the number of days until a warmer temperature and store it in the output array
stack.append(i) # Push the current index onto the stack
return output # Return the output array after processing all temperatures
def main():
test_cases = [
[73, 74, 75, 71, 69, 72, 76, 73],
[30, 40, 50, 60],
[30, 60, 90],
[90, 60, 30],
[30, 30, 30, 30],
]
# Loop through the test cases and print the results
for i, temperatures in enumerate(test_cases):
print(i+1, "\ttemperature:", temperatures)
print("\n\toutput:", daily_temperatures(temperatures))
print("-"*100)
main()
Daily Temperatures using Stack

MAANG mini-case study: Google frequently tests candidates with problems involving monotonic stacks, such as stock price fluctuations or weather pattern analysis. Recognizing and applying monotonic structures immediately improves your solution’s efficiency, often making the difference between an average solution and one that demonstrates algorithmic proficiency.

Related variations: Parsing A Boolean Expression, Flatten Nested List Iterator

12. Dynamic Programming (DP) - Advanced#

When to use

Use Dynamic Programming (DP) when a problem can be broken into smaller overlapping subproblems, and you can reuse solutions to those subproblems. Typical signs include problems that ask you to count the number of ways to do something, find an optimal result (like minimum cost or maximum value), or work with sequences where results depend on previous states.

Visual intuition

Imagine solving a complex puzzle that involves solving smaller puzzles within it. Rather than repeatedly solving these smaller puzzles from scratch, you store their solutions the first time you solve them. Later, when the same smaller puzzle reappears, you reuse the stored solution instead of recalculating it.

Dynamic Programming follows this logic exactly. It uses memoization or tabulation to store solutions of overlapping subproblems, thus reducing redundant calculations. The result is faster performance compared to brute-force approaches.

Canonical problem: Longest Increasing Subsequence—Given an integer array, find the length of the longest strictly increasing subsequence.

def longest_subsequence(nums):
size = len(nums)
# initialize an array of size 'size' with all values as 1
dp = [1] * size
# loop through the input list
for i in range(1, size):
for j in range(i):
# if nums[i] > nums[j], then there is a possibility of increasing subsequence
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j]+1)
# return the maximum value in dp array which represents the length of the longest increasing subsequence
return max(dp)
# driver code
def main():
lists = [[10, 9, 2, 5, 3, 7, 101, 18], [7, 7, 7, 7, 7, 7, 7], [0, 1, 0, 3, 2, 3],
[3, 2], [6, 9, 8, 2, 3, 5, 1, 4, 7], [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15],
[9, 2, 5, 3, 6, 14, 11, 7, 9, 5, 13, 3, 15, 0, 8, 4, 1, 9, 5, 13, 3, 11, 7, 15, 0, 10, 6, 14, 9, 2, 5, 3, 2, 10, 6, 10, 6, 5, 13, 3, 11, 7, 15, 3, 11, 7, 15]]
for i, input_list in enumerate(lists):
print(i+1, ".\tInput array: ", input_list, sep="")
print("\tLength of LIS is:", longest_subsequence(input_list))
print("\tSpace-optimized approach")
print("-"*100)
if __name__ == '__main__':
main()
Longest Increasing Subsequence using Dynamic Programming

MAANG mini-case study: MAANG coding interviews frequently have Dynamic Programming questions due to their complexity and importance. Amazon often asks candidates to solve optimization problems involving profit maximization or resource allocation, and candidates who recognize and implement DP approaches consistently perform better, delivering more optimal and efficient solutions.

Related variations: Coin Change, Longest Common Subsequence

Common coding interview mistakes#

Even experienced engineers hit snags during live interviews. The challenge is not just solving problems but doing so under time pressure while thinking out loud. Many candidates get stuck because they apply the wrong pattern or overlook a more straightforward approach. Recognizing these traps early can save valuable minutes and show interviewers you know how to course‑correct.

Below is a quick reference table of common symptoms, what they usually mean, and how to adjust your approach:

Symptom

Likely Pattern Misfit

Quick Fix

O(n²) solution on sorted input

Ignoring Two Pointers

Revisit sorted data and pointers

Repeating subproblem work

Missing DP

Cache or tabulate results

Failing the shortest path

Using DFS

Switch to BFS

Using extra storage in cycle detection

Not using Fast and Slow Pointers

Apply the Fast and Slow Pointers method

Explain your pattern shifts to the interviewer. A straightforward reasoning process often matters as much as the final answer.

Next steps#

Now that you have a clear picture, it’s time to practice with intent. Don’t just solve random problems—focus on representative problems for each common coding pattern. The Blind 75 list is a great starting point: it’s a curated set of high-yield problems that reflect what you’re likely to face in real interviews.

Work through them until you can solve each one from scratch. Build intuition, not just memory.

More patterns worth exploring:

Once you’ve mastered the top core patterns, other patterns can also be valuable, especially in senior‑level interviews or domain‑specific roles. Consider adding these to your practice plan over time:

  • K‑way Merge: Useful for merging multiple sorted lists or streams, often with a heap.

  • Bitwise Manipulation: Great for problems involving parity, unique elements, or low‑level optimizations.

  • Math and Geometry: Patterns around number theory, modular arithmetic, convex hulls, and distance calculations; these show up in optimization and computational geometry problems.

These aren’t as common in day‑one MAANG rounds, but they appear in follow‑up interviews or more specialized roles if you aim high or want to future‑proof your skills. Set aside time to explore them.

Here are a few resources that can make your prep far more effective:

  • Educative PAL (Personalized Adaptive Learning) tool: PAL analyzes your progress and adapts problem sets to your weak areas. It’s like having a coach who knows exactly where you need more practice. (It is highly recommended if you want a structured path rather than figuring it out alone.)

  • AI Mock Interviews: Practice is one thing, performing under interview pressure is another. Educative’s AI‑powered mock interviews simulate real technical interviews and provide you with on‑the‑spot hints and detailed feedback to help you improve. It’s a safe way to build confidence before the actual day.

  • Grokking the Coding Interview Patterns: A completely updated flagship course, covering all these patterns with interactive examples, step‑by‑step breakdowns, and interview‑style walkthroughs.

  • Data Structures and DP Course: Build strong fundamentals that power these patterns.

Frequently Asked Questions

What are LeetCode patterns?

LeetCode patterns are LeetCode-style questions involve scenarios where you’re asked to write code to find solutions. This lets you showcase your practical understanding of certain data structures and algorithms. Solving these problems shows you can apply theoretical concepts in real-world coding challenges.

Can a beginner start with LeetCode?

LeetCode questions have different difficulty levels depending on the learner’s abilities. If you’re a beginner, you can choose the easy level and start from there.

Why focus on patterns instead of solving hundreds of random problems?

Because patterns scale. Learning 10 core patterns equips you to handle hundreds of interview questions. Random grinding often leads to burnout and poor recall under pressure. Patterns give you a mental framework so even a fresh problem feels familiar.

Do patterns really show up in real interviews or just on paper?

Absolutely. Almost every question I’ve seen in FAANG onsite rounds maps to a well‑known pattern. For example, Amazon often asks scheduling problems that require Merge Intervals, and Meta loves graph problems that fit DFS/BFS. Recognizing the pattern is usually what separates a good answer from a great one.

Are these patterns the same as standard algorithms?

Not exactly. A pattern is a higher‑level strategy, while an algorithm is a specific procedure. For example, Binary Search is an algorithm, but “Two Pointers” is a pattern that may use sorting or other algorithms, depending on the problem.

Are learning patterns enough to pass interviews?

Patterns are the core, but combine them with mock interviews, clear communication, and time management. For senior roles, add system design prep. Patterns give you the problem‑solving edge, but interview performance combines technique and delivery.


Written By:
Muhammad Bilal

Free Resources