When to use:
- Sorted array pair problems
- Removing duplicates in place
- Container with most water
- Palindrome checking
Variants:
- Opposite ends (converging)
- Same direction (fast/slow or lagging)
example
// JavaScript — Remove duplicates from sorted arrayfunction removeDuplicates(arr) {
if (arr.length === 0) return0;
let writeIdx = 1;
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) {
arr[writeIdx] = arr[i];
writeIdx++;
}
}
return writeIdx;
}
# Pythondef remove_duplicates(arr):
ifnot arr:
return0
write_idx = 1for i in range(1, len(arr)):
if arr[i] != arr[i - 1]:
arr[write_idx] = arr[i]
write_idx += 1return write_idx
Note O(n) time, O(1) space. The 'read pointer / write pointer' variant is essential for in-place array problems. Always ask: is the input sorted? If yes, two pointers likely applies. If unsorted, consider hash set instead.
two pointers patternremove duplicatesconverging pointersin-place modification
Pattern: Fast & Slow Pointer
syntax
When to use:
- Cycle detection (linked list, array)
- Finding middle of linked list
- Finding the start of a cycle
- Happy number problem
Slow moves 1 step, fast moves 2 steps.
example
// JavaScript — Happy number (sum of squared digits eventually reaches 1)function isHappy(n) {
function digitSquareSum(num) {
let sum = 0;
while (num > 0) {
const digit = num % 10;
sum += digit * digit;
num = Math.floor(num / 10);
}
return sum;
}
let slow = n, fast = n;
do {
slow = digitSquareSum(slow);
fast = digitSquareSum(digitSquareSum(fast));
} while (slow !== fast);
return slow === 1;
}
# Pythondef is_happy(n):
def digit_square_sum(num):
total = 0while num > 0:
digit = num % 10
total += digit * digit
num //= 10return total
slow = fast = n
whileTrue:
slow = digit_square_sum(slow)
fast = digit_square_sum(digit_square_sum(fast))
if slow == fast:
breakreturn slow == 1
Note The fast/slow pointer pattern detects cycles in any sequence where each value maps to the next. It uses O(1) space compared to a hash set. For linked lists, this is Floyd's algorithm. For number sequences (like happy number), the function that generates the next value plays the role of 'next pointer'.
fast slow pointerFloyd's cyclehappy numbertortoise harecycle detection pattern
Pattern: Sliding Window (Summary)
syntax
When to use:
- Subarray/substring with constraint (max sum, min length, distinct chars)
- Fixed-size window statistics
- Longest/shortest substring matching a condition
Template:
- Expand right pointer
- When window invalid, shrink left pointer
- Update answer at each valid state
example
// JavaScript — Longest substring without repeating charactersfunction lengthOfLongestSubstring(s) {
const charIdx = new Map();
let maxLen = 0, left = 0;
for (let right = 0; right < s.length; right++) {
if (charIdx.has(s[right]) && charIdx.get(s[right]) >= left) {
left = charIdx.get(s[right]) + 1;
}
charIdx.set(s[right], right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
# Pythondef length_of_longest_substring(s):
char_idx = {}
max_len = left = 0for right, ch in enumerate(s):
if ch in char_idx and char_idx[ch] >= left:
left = char_idx[ch] + 1
char_idx[ch] = right
max_len = max(max_len, right - left + 1)
return max_len
Note O(n) time, O(k) space where k = alphabet/window contents. The trick: store the last index of each character so you can jump the left pointer forward without shrinking one step at a time. This is one of the most frequently asked medium problems.
When to use:
- Overlapping intervals
- Meeting room scheduling
- Insert interval into sorted list
1. Sort intervals by start time
2. Iterate: if current overlaps with previous, merge them
3. Otherwise, add current asnew interval
example
// JavaScriptfunction mergeIntervals(intervals) {
intervals.sort((a, b) => a[0] - b[0]);
const merged = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const last = merged[merged.length - 1];
if (intervals[i][0] <= last[1]) {
last[1] = Math.max(last[1], intervals[i][1]);
} else {
merged.push(intervals[i]);
}
}
return merged;
}
# Pythondef merge_intervals(intervals):
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
Note Time O(n log n) for sorting. Space O(n) for result. The overlap condition: current.start <= prev.end. For 'meeting rooms needed' (minimum rooms), use a min-heap on end times or a sweep line approach. Always sort by start time first.
When to use:
- K most frequent elements
- K closest points
- K largest/smallest values
Approaches:
1. Sort → O(n log n)
2. Min-heap of size k → O(n log k)
3. Quickselect → O(n) average
example
// JavaScript — Top K frequent elements using bucket sortfunction topKFrequent(nums, k) {
const freq = new Map();
for (const n of nums) freq.set(n, (freq.get(n) || 0) + 1);
// Bucket: index = frequency, value = list of elementsconst buckets = Array.from({ length: nums.length + 1 }, () => []);
for (const [num, count] of freq) buckets[count].push(num);
const result = [];
for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) {
result.push(...buckets[i]);
}
return result.slice(0, k);
}
# Pythonimport heapq
from collections import Counter
def top_k_frequent(nums, k):
freq = Counter(nums)
# nlargest uses a heap internallyreturn [item for item, count in freq.most_common(k)]
output
top_k_frequent([1,1,1,2,2,3], 2) → [1, 2]
Note Bucket sort approach: O(n) time and space. Heap approach: O(n log k). For 'top K', a min-heap of size k is optimal — you evict the smallest when the heap exceeds k, so only the K largest remain. Python's heapq.nlargest and Counter.most_common are powerful shortcuts.
top K elementsK most frequentheap top Kbucket sort frequencyquickselect
Pattern: Prefix Sum (Applications)
syntax
When to use:
- Range sum queries
- Subarray sum equals K
- Subarray sum divisible by K
- Binary subarrays with sum
- Count subarrays with given XOR
Core idea: prefix[i] - prefix[j] = sum of arr[j..i-1]
example
// JavaScript — Count subarrays with sum divisible by kfunction subarraysDivByK(arr, k) {
const modCount = new Map([[0, 1]]);
let prefix = 0, count = 0;
for (const num of arr) {
prefix = ((prefix + num) % k + k) % k; // handle negatives
count += modCount.get(prefix) || 0;
modCount.set(prefix, (modCount.get(prefix) || 0) + 1);
}
return count;
}
# Pythondef subarrays_div_by_k(arr, k):
mod_count = {0: 1}
prefix = 0
count = 0for num in arr:
prefix = (prefix + num) % k
count += mod_count.get(prefix, 0)
mod_count[prefix] = mod_count.get(prefix, 0) + 1return count
output
subarrays_div_by_k([4,5,0,-2,-3,1], 5) → 7
Note Time O(n), Space O(k). The key insight: if two prefix sums have the same remainder mod k, the subarray between them is divisible by k. Handle negative modulo carefully: (x % k + k) % k ensures a non-negative remainder. This is a very common interview pattern.
prefix sum patternsubarray divisible by kmodular prefixrange sum pattern
Pattern: Union-Find (Disjoint Set)
syntax
Track connected components efficiently.
Two operations:
- find(x): return the root/representative of x's set
- union(x, y): merge the sets containing x and y
Optimizations: path compression + union by rank
Note With both optimizations, operations are nearly O(1) amortized (technically O(alpha(n)) inverse Ackermann). Perfect for: connected components, redundant connections, accounts merge, minimum spanning tree (Kruskal's). Union returns false when already connected — useful for cycle detection.
union finddisjoint setconnected componentspath compressionunion by rank
Pattern: When to Use BFS vs DFS
syntax
Use BFS when:
- Shortest path in unweighted graph
- Level-by-level processing
- Closest node / nearest distance
Use DFS when:
- Exploring all paths / all possibilities
- Cycle detection
- Topological sort
- Tree traversals (in/pre/post order)
- Backtracking problems
example
// Decision guide:// 'Shortest' or 'minimum steps' → BFS// 'All paths' or 'does a path exist' → DFS// 'Level order' → BFS// 'Permutations/combinations' → DFS (backtracking)// JavaScript — Minimum steps to reach target in gridfunction minSteps(grid, start, end) {
const rows = grid.length, cols = grid[0].length;
const queue = [[...start, 0]]; // [row, col, steps]const visited = new Set([start.toString()]);
const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
while (queue.length) {
const [r, c, steps] = queue.shift();
if (r === end[0] && c === end[1]) return steps;
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
const key = `${nr},${nc}`;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& grid[nr][nc] === 0 && !visited.has(key)) {
visited.add(key);
queue.push([nr, nc, steps + 1]);
}
}
}
return -1;
}
# Pythonfrom collections import deque
def min_steps(grid, start, end):
rows, cols = len(grid), len(grid[0])
queue = deque([(start[0], start[1], 0)])
visited = {(start[0], start[1])}
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
pass# directions defined in loop belowwhile queue:
r, c, steps = queue.popleft()
if (r, c) == tuple(end):
return steps
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
nr, nc = r + dr, c + dc
if0 <= nr < rows and0 <= nc < cols and grid[nr][nc] == 0and (nr,nc) notin visited:
visited.add((nr, nc))
queue.append((nr, nc, steps + 1))
return -1
output
BFS guarantees shortest path in unweighted graphs.
DFS is better for exhaustive search and backtracking.
Note BFS time/space: O(V + E). DFS time: O(V + E), space: O(V) worst case. In grid problems, treat each cell as a node with 4 edges. For BFS on grids, always mark visited when enqueuing (not when dequeuing) to avoid processing a cell multiple times.
BFS vs DFSwhen to use BFSwhen to use DFSshortest path gridgraph traversal choice