LeetCode Pattern Mastery
LeetCode Pattern Mastery
Use the Quick Reference table (next page) to identify patterns at a glance during an interview.
Quick Reference — Pattern Selection Guide
Linked list cycle / find middle Fast & Slow Pointers O(N) O(1) space
Tree path sums, BST validation Tree Traversal O(N) with correct ordering
Range sum queries, static array Prefix Sum O(N) per query → O(1)
Primes, GCD, factorial, digits Math / Number Theory O(√N) to O(N log log N)
Tree Q queries, path between nodes LCA / Binary Lifting O(N×Q) → O((N+Q) log N)
Connected components, dynamic
DSU / Union-Find O(α(N)) per operation
edges
Point updates + range queries Segment / Fenwick Tree O(N) query → O(log N)
Minimize max / maximize min value Binary Search on Answer O(V×N) → O(N log V)
Section 1 — Arrays & Strings
Two Pointers
Category: Arrays & Strings | Difficulty: Easy → Medium
What it solves
Use two index pointers (usually left and right, or slow and fast) to scan through an array or string in a
single pass. Eliminates the need for a nested loop.
Naive vs Optimised
❌ Brute Force O(N²) ✅ Two Pointers O(N)
Check every pair with nested loops — TLE on Move pointers toward each other based on
large inputs conditions — single pass
How to recognise it
sorted array two sum remove duplicates
Step-by-step approach
1 Sort the array if not already sorted (required for most two-pointer problems).
3 While left < right: check condition. If too small → move left right. If too large → move right
left. If match → record answer.
4 Handle duplicates by skipping equal elements after a match to avoid duplicate results.
Complexity
Metric Value
Space O(1)
Exam Tip: Two pointers almost always requires a SORTED array (or you sort first). If the array is
unsorted and sorting isn't allowed, use a HashMap instead.
Python implementation
# Two Sum II — sorted array
def two_sum(numbers, target):
left, right = 0, len(numbers) - 1
while left < right:
s = numbers[left] + numbers[right]
if s == target:
return [left + 1, right + 1]
elif s < target:
left += 1
else:
right -= 1
return []
What it solves
Maintain a window of elements satisfying some condition. Expand by moving right pointer, shrink by
moving left pointer. Every element enters and exits the window at most once.
Naive vs Optimised
❌ Brute Force O(N²) ✅ Sliding Window O(N)
How to recognise it
longest substring minimum window contiguous subarray
Step-by-step approach
1 Initialise left = 0, right = 0, and a state variable (count, sum, hashmap, etc.).
3 Shrink: while window is invalid, remove nums[left] from state, advance left pointer.
4 Update answer with current valid window. Every element enters/exits at most once → O(N).
Complexity
Metric Value
Time O(N)
Exam Tip: Sliding window FAILS with negative numbers (shrinking doesn't always help). For
negatives, use Kadane's algorithm for max subarray or Prefix Sum + HashMap for target sum.
Python implementation
# Minimum window substring
def min_window(s, t):
need = {}
for c in t: need[c] = [Link](c, 0) + 1
have, required = 0, len(need)
window = {}
left = 0
result = ""
min_len = float('inf')
return result
Binary Search (Classic + Variants)
Category: Arrays & Searching | Difficulty: Easy → Hard
What it solves
Eliminate half the search space each iteration by comparing with the midpoint. Works on any SORTED
array or any problem with a monotonic condition (true/false boundary).
Naive vs Optimised
❌ Linear Scan O(N) ✅ Binary Search O(log N)
Scan every element to find the target — Halve the search space each step — finds target
unnecessary when data is sorted in log N steps
How to recognise it
sorted array find target first/last position
minimize maximum
Step-by-step approach
1 Set lo = 0, hi = len(array) - 1 (or the answer range for parametric search).
4 For 'find leftmost': when found, save mid and continue with hi = mid - 1. For 'find rightmost':
save mid and continue with lo = mid + 1.
Complexity
Metric Value
Space O(1)
Python implementation
# Classic binary search
def binary_search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target: return mid
elif nums[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
What it solves
Trade space for time — store elements in a hash structure for O(1) lookup. Eliminates nested loops for
'find complement', 'count occurrences', and 'check existence' problems.
Naive vs Optimised
❌ Nested Loops O(N²) ✅ HashMap O(N)
For each element, scan the rest of the array — Store elements in a map — O(1) lookup replaces
TLE the inner loop
How to recognise it
two sum anagram duplicate
Step-by-step approach
1 Initialise an empty HashMap (or HashSet for existence-only checks).
2 For each element: check if the complement/answer exists in the map first.
4 For frequency problems: store {element: count} and iterate the map for the answer.
Complexity
Metric Value
Space O(N)
Python implementation
# Two Sum — unsorted array
def two_sum(nums, target):
seen = {} # value → index
for i, n in enumerate(nums):
complement = target - n
if complement in seen:
return [seen[complement], i]
seen[n] = i
return []
# Group anagrams
def group_anagrams(strs):
groups = {}
for s in strs:
key = tuple(sorted(s)) # sorted string as key
[Link](key, []).append(s)
return list([Link]())
Stack — Matching & Parsing
Category: Arrays & Strings | Difficulty: Easy → Medium
What it solves
Use a stack for problems requiring matching paired elements (brackets, tags) or parsing expressions.
The stack naturally handles LIFO order — the most recent unmatched element is always at the top.
Naive vs Optimised
❌ Counter only ✅ Stack O(N)
Counting opens/closes works for single bracket Push opens, pop and match closes — handles
types but fails for mixed types like ()[]{} any combination of paired elements
How to recognise it
valid parentheses decode string evaluate expression
basic calculator
Step-by-step approach
1 Initialise an empty stack.
3 For closing brackets: check if stack is non-empty and top matches. If yes pop, if no →
invalid.
4 After processing all characters: stack should be empty for a valid string.
Complexity
Metric Value
Time O(N)
Space O(N)
Exam Tip: Any time you need to 'remember' the most recent unresolved element and check it
against a future element, that's a stack problem. Brackets, nested structures, and expression
parsing are classic tells.
Python implementation
# Valid parentheses
def is_valid(s):
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for c in s:
if c in mapping:
top = [Link]() if stack else '#'
if mapping[c] != top: return False
else:
[Link](c)
return not stack
What it solves
Precompute cumulative sums so any range sum [l, r] can be answered in O(1). The range sum equals
prefix[r+1] - prefix[l]. Extended with a HashMap for 'subarray sum equals K' problems.
Naive vs Optimised
❌ Recompute each range O(N) per query ✅ Prefix Sum O(1) per query
Sum from scratch for every query — O(N×Q) total O(N) precompute, then O(1) per range query
How to recognise it
range sum query subarray sum equals K pivot index
Step-by-step approach
1 Build prefix array: prefix[0] = 0, prefix[i] = prefix[i-1] + nums[i-1].
3 For 'count subarrays with sum K': maintain a running prefix sum and a HashMap {sum:
count}.
Complexity
Metric Value
Precompute O(N)
Python implementation
# Static range sum queries
class NumArray:
def __init__(self, nums):
[Link] = [0] * (len(nums) + 1)
for i, n in enumerate(nums):
[Link][i+1] = [Link][i] + n
What it solves
Two pointers moving at different speeds through a linked list or sequence. The fast pointer moves 2
steps, slow moves 1. They meet if a cycle exists.
Naive vs Optimised
❌ HashSet O(N) space ✅ Fast & Slow O(1) space
Track visited nodes in a set — works but uses Two pointers — cycle detection with no extra
O(N) extra space space
How to recognise it
linked list cycle find middle of list detect cycle
Step-by-step approach
1 Initialise slow = head, fast = head.
4 To find cycle start: reset slow to head. Move both one step until they meet — that's the
cycle entry point.
Complexity
Metric Value
Space O(1)
Exam Tip: If fast and slow pointers meet → cycle exists. To find the START of the cycle: reset
slow to head, keep fast at meeting point, move both one step at a time — they meet at the cycle
start.
Python implementation
# Detect cycle in linked list
def has_cycle(head):
slow = fast = head
while fast and [Link]:
slow = [Link]
fast = [Link]
if slow == fast:
return True
return False
What it solves
Explore nodes level by level using a queue. Guarantees the SHORTEST path in an unweighted graph.
Visit all neighbours before going deeper.
Naive vs Optimised
❌ DFS for shortest path ✅ BFS O(V+E)
DFS finds A path, not necessarily the SHORTEST Queue-based level-order traversal — shortest
path in unweighted graphs path guaranteed
How to recognise it
shortest path minimum steps level order traversal
number of islands
Step-by-step approach
1 Initialise queue with the start node(s). Mark start as visited immediately.
3 For each unvisited neighbour: mark visited, enqueue with updated distance.
4 Return when target is found (guaranteed shortest) or when queue is empty (no path).
Complexity
Metric Value
Time O(V + E)
Python implementation
from collections import deque
What it solves
Explore as far as possible along each branch before backtracking. Ideal for connected component
counting, flood fill, cycle detection, and path existence problems.
Naive vs Optimised
❌ BFS for connected components ✅ DFS O(V+E)
BFS works but DFS is simpler and uses less code Recursive or iterative deep exploration — marks
for connectivity problems visited cells/nodes
How to recognise it
number of islands connected components flood fill
count provinces
Step-by-step approach
1 For each unvisited node/cell, call DFS and increment component count.
2 In DFS: mark current node as visited immediately (before recursing to avoid revisits).
4 Count = number of DFS calls from the outer loop = number of connected components.
Complexity
Metric Value
Python implementation
# Number of islands — grid DFS
def num_islands(grid):
if not grid: return 0
rows, cols = len(grid), len(grid[0])
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
What it solves
Navigate a binary tree in different orders. Inorder gives sorted output for BSTs. Preorder is used for
serialization. Postorder processes children before parents. Level-order processes row by row.
Naive vs Optimised
❌ Linear scan O(N²) ✅ Recursive/Iterative O(N)
For BST operations without using the BST Single pass through all nodes with correct order
property — misses the O(log N) advantage guarantees
How to recognise it
inorder traversal level order max depth
Step-by-step approach
1 Choose traversal order: inorder (left→root→right), preorder (root→left→right), postorder
(left→right→root), or level-order (BFS).
2 For recursive: define base case (node is None → return), then recursive case with the
pattern.
4 For LCA: if both nodes are in different subtrees → current node is LCA.
Complexity
Metric Value
Exam Tip: For BST problems: always use the BST property (left < root < right). Inorder traversal
of BST gives a sorted array. Validate BST by passing min/max bounds down the recursion.
Python implementation
# All traversals in one place
def inorder(root): # sorted output for BST
return inorder([Link]) + [[Link]] + inorder([Link]) if root else
[]
# Validate BST
def is_valid_bst(root, lo=float('-inf'), hi=float('inf')):
if not root: return True
if [Link] <= lo or [Link] >= hi: return False
return (is_valid_bst([Link], lo, [Link]) and
is_valid_bst([Link], [Link], hi))
Dijkstra's Algorithm (Weighted Shortest Path)
Category: Trees & Graphs | Difficulty: Medium → Hard
What it solves
Find the shortest path in a weighted graph with NON-NEGATIVE edge weights. Uses a min-heap
(priority queue) to always process the closest unvisited node first.
Naive vs Optimised
❌ BFS for weighted graphs ✅ Dijkstra O((V+E) log V)
BFS assumes all edges have equal weight — Min-heap always processes nearest node —
wrong answer on weighted graphs correct shortest path on weighted graphs
How to recognise it
weighted shortest path network delay time cheapest flights
path with minimum cost non-negative weights single source shortest path
Step-by-step approach
1 Initialise dist[] = infinity for all nodes. dist[source] = 0. Push (0, source) to min-heap.
3 Mark node as visited. For each neighbour: if dist[node] + edge_weight < dist[neighbour],
update dist and push to heap.
Complexity
Metric Value
Space O(V + E)
Exam Tip: Dijkstra FAILS on negative edge weights (use Bellman-Ford instead). Key insight:
once a node is popped from the min-heap, its shortest distance is FINAL — skip it if visited.
Python implementation
import heapq
while heap:
cost, u = [Link](heap)
if u in visited: continue # already found shortest path
[Link](u)
for weight, v in graph[u]:
new_cost = cost + weight
if new_cost < dist[v]:
dist[v] = new_cost
[Link](heap, (new_cost, v))
What it solves
A tree where each node represents a character. Paths from root spell out strings. Enables O(L) prefix
search and autocomplete — far faster than searching through all strings.
Naive vs Optimised
❌ Linear scan O(N×L) ✅ Trie O(L)
Check every string for a prefix match — slow for Traverse the trie character by character — O(L)
large dictionaries per operation where L = word length
How to recognise it
word search II autocomplete prefix matching
replace words
Step-by-step approach
1 Create TrieNode class with children dict (or array[26]) and is_end = False flag.
2 Insert: for each character, create a new node if it doesn't exist, then mark is_end = True at
the last character.
3 Search: traverse the trie character by character. If any character is missing → word not
found.
4 StartsWith (prefix): same as search but don't check is_end — just confirm all characters
exist.
Complexity
Metric Value
Python implementation
class TrieNode:
def __init__(self):
[Link] = {}
self.is_end = False
class Trie:
def __init__(self):
[Link] = TrieNode()
What it solves
Break the problem into overlapping subproblems. Store solutions to subproblems to avoid
recomputation. dp[i] represents the optimal answer for the first i elements.
Naive vs Optimised
❌ Recursion without memo O(2^N) ✅ Bottom-up DP O(N)
Recompute overlapping subproblems Fill dp[] left to right, each cell builds on previous
exponentially results
How to recognise it
maximum subarray climbing stairs coin change
fibonacci
Step-by-step approach
1 Define: what does dp[i] represent? (e.g. 'max profit using first i items').
3 Transition: how does dp[i] depend on dp[i-1], dp[i-2], etc.? Write the recurrence.
Complexity
Metric Value
Python implementation
# Climbing Stairs — dp[i] = ways to reach step i
def climb_stairs(n):
if n <= 2: return n
dp = [0] * (n + 1)
dp[1], dp[2] = 1, 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
What it solves
Use a 2D table where dp[i][j] represents the answer for a subproblem involving the first i elements of
one input and first j elements of another (or a grid position).
Naive vs Optimised
❌ Recursion O(2^(M+N)) ✅ 2D DP O(M×N)
How to recognise it
longest common subsequence edit distance unique paths
knapsack
Step-by-step approach
1 Define: what does dp[i][j] represent? Be precise.
2 Base cases: fill dp[0][j] and dp[i][0] (empty string / start of grid).
3 Transition: when characters match vs don't match (string DP), or when moving from
adjacent cells (grid DP).
Complexity
Metric Value
Time O(M × N)
Python implementation
# Longest Common Subsequence
def lcs(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1 # characters match
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # skip one
return dp[m][n]
What it solves
Build solutions incrementally and abandon (backtrack) as soon as a partial solution cannot lead to a
valid complete solution. Explores the solution space like a pruned decision tree.
Naive vs Optimised
❌ Generate All O(N!) ✅ Backtracking O(pruned tree)
How to recognise it
all permutations all subsets all combinations
generate parentheses
Step-by-step approach
1 Define the recursive function with: current path, starting index (for combinations), and result
list.
2 Base case: if path satisfies the condition, add a copy of path to result and return.
3 Loop through valid choices. Add choice to path, recurse, then remove choice (backtrack).
4 Add pruning conditions to skip invalid choices early (e.g. skip duplicates, stop if sum
exceeds target).
Complexity
Metric Value
Exam Tip: Backtracking template is always the same: (1) base case → add to result, (2) loop
through choices, (3) make choice, (4) recurse, (5) undo choice. The pruning condition is where
the problem-specific logic lives.
Python implementation
# All subsets
def subsets(nums):
result = []
def backtrack(start, path):
[Link](path[:]) # add copy at every node
for i in range(start, len(nums)):
[Link](nums[i])
backtrack(i + 1, path)
[Link]() # undo choice
backtrack(0, [])
return result
# All permutations
def permutations(nums):
result = []
def backtrack(path, used):
if len(path) == len(nums):
[Link](path[:]); return
for i in range(len(nums)):
if used[i]: continue
used[i] = True
[Link](nums[i])
backtrack(path, used)
[Link]()
used[i] = False
backtrack([], [False] * len(nums))
return result
Section 6 — Sorting & Ordering
Heap / Priority Queue
Category: Sorting & Ordering | Difficulty: Medium
What it solves
Maintain a dynamic sorted structure where you can always extract the min or max in O(log N).
Essential for K-largest/smallest problems and merge-sorted-lists problems.
Naive vs Optimised
❌ Sort then slice O(N log N) ✅ Heap O(N log K)
Sort entire array to find K-th element — overkill Maintain a K-size heap — only keep track of the
when K << N best K elements
How to recognise it
K largest K smallest K-th largest
task scheduler
Step-by-step approach
1 For K-largest: initialise a min-heap of size K with the first K elements.
2 For each remaining element: if it's larger than heap[0] (the minimum), pop and push the
new element.
4 For K-th largest specifically: return heap[0] (the smallest of the K largest = K-th largest
overall).
Complexity
Metric Value
Exam Tip: Python's heapq is a MIN-heap. For max-heap: negate all values. For K largest
elements: use a MIN-heap of size K — if new element > heap top, pop and push. The heap
always holds the K largest seen so far.
Python implementation
import heapq
# K largest elements
def k_largest(nums, k):
min_heap = nums[:k]
[Link](min_heap) # O(K)
for n in nums[k:]:
if n > min_heap[0]:
[Link](min_heap, n) # pop min, push n
return min_heap # contains K largest
# K-th largest
def kth_largest(nums, k):
return [Link](k, nums)[-1]
# or maintain heap of size k as above and return heap[0]
What it solves
Problems involving ranges [start, end]. The key technique is sorting by start time, then using greedy
merging or a min-heap to handle overlaps.
Naive vs Optimised
❌ Nested loop O(N²) ✅ Sort + sweep O(N log N)
How to recognise it
meeting rooms merge intervals insert interval
Step-by-step approach
1 Sort intervals by start time (or end time depending on the problem).
2 For merging: iterate and check if current interval overlaps with the last merged one. If yes,
extend. If no, append.
3 For minimum rooms: use a min-heap. Push end times. For each new interval, if it starts
after heap top, pop (room freed). Always push current end time.
Complexity
Metric Value
Python implementation
# Minimum meeting rooms needed
import heapq
def min_meeting_rooms(intervals):
[Link](key=lambda x: x[0])
heap = [] # min-heap of end times
for start, end in intervals:
if heap and heap[0] <= start:
[Link](heap, end) # reuse room
else:
[Link](heap, end) # need new room
return len(heap)
What it solves
Make the locally optimal choice at each step with the hope that it leads to the globally optimal solution.
Greedy works when the problem has the 'greedy choice property' — local optimum leads to global
optimum.
Naive vs Optimised
❌ DP for all optimization ✅ Greedy O(N) or O(N log N)
DP is correct but often O(N²) — greedy gives One pass making locally optimal choices — no
O(N) when the greedy property holds backtracking needed
How to recognise it
jump game gas station meeting rooms
task scheduler
Step-by-step approach
1 Sort if the order of processing matters (e.g. sort intervals by start or end time).
3 Iterate and make the greedy choice at each step — always pick the locally best option.
Complexity
Metric Value
Space O(1)
Exam Tip: Greedy works when: (1) making the best local choice never hurts you globally, AND
(2) the problem has optimal substructure. If in doubt between greedy and DP — check with a
small example. If greedy gives wrong answer → use DP.
Python implementation
# Jump Game II — minimum jumps to reach end
def jump(nums):
jumps = 0
current_end = 0 # farthest we can reach with current jumps
farthest = 0 # farthest we can reach at all
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end: # must take a jump
jumps += 1
current_end = farthest
return jumps
# Merge intervals
def merge_intervals(intervals):
[Link](key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]: # overlap
merged[-1][1] = max(merged[-1][1], end)
else:
[Link]([start, end])
return merged
What it solves
Use bitwise operations (&, |, ^, ~, <<, >>) to solve problems in O(1) or O(N) with constant space. XOR
is especially powerful for finding single/missing numbers.
Naive vs Optimised
❌ Sort or HashMap ✅ Bit ops O(N) O(1) space
Using extra space or sorting when bitwise ops XOR, AND, shifts — no extra data structures
can solve it in O(1) space needed
How to recognise it
single number missing number count bits
Step-by-step approach
1 Identify the bit operation: XOR for finding single/missing, AND for masking, shifts for powers
of 2.
2 For 'single number': XOR all elements — duplicates cancel out (x^x=0), leaving the unique
element.
3 For 'count set bits': use Brian Kernighan's trick: while n > 0: count += 1; n = n & (n-1).
4 For 'power of 2': n > 0 and (n & (n-1)) == 0 — a power of 2 has exactly one set bit.
Complexity
Metric Value
Exam Tip: XOR tricks: x ^ x = 0, x ^ 0 = x. XOR all elements to find the one that appears odd
times. n & (n-1) removes the lowest set bit — use it to count set bits.
Python implementation
# Single number (appears once, all others twice)
def single_number(nums):
result = 0
for n in nums: result ^= n # x^x=0, x^0=x
return result
What it solves
Problems solvable with mathematical insights: modular arithmetic, GCD, prime sieve, digit
manipulation. Recognizing the math avoids unnecessary data structures.
Naive vs Optimised
❌ Brute force simulation ✅ Math formula O(1) or O(√N)
How to recognise it
power of N factorial trailing zeros palindrome number
Step-by-step approach
1 Identify if a mathematical property directly solves the problem (trailing zeros = count 5s,
GCD = Euclidean).
3 For prime sieve: start with all True, mark multiples of each prime as False from p² upward.
Complexity
Metric Value
Python implementation
# GCD — Euclidean algorithm
def gcd(a, b):
while b: a, b = b, a % b
return a
N ≤ 10^5 O(N log N) Sort + scan, heap, binary search, segment tree
Window expands/shrinks in
"Contiguous subarray" Sliding Window
O(N)
"Minimum/maximum of
DP or Greedy Try greedy first — simpler
something"
"Minimize the maximum" Binary Search on Answer Parametric search on the value
Local optimum always leads to global optimum Local optimum may NOT lead to global optimum
Example: always pick the meeting that ends Example: coin change — greedy fails for some
earliest denominations