When to use:-Sorted array pair problems
-Removing duplicates in place
-Containerwith most water
-Palindrome checking
Variants:-Oppositeends(converging)-Samedirection(fast/slow or lagging)
Example
// JavaScript - Remove duplicates from sorted arrayfunctionremoveDuplicates(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;}
# Python
def remove_duplicates(arr):if not arr:return0
write_idx =1for i inrange(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.
When to use:-Cycledetection(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)functionisHappy(n){functiondigitSquareSum(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;}
# Python
def 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'.
When to use:-Subarray/substring withconstraint(max sum, min length, distinct chars)-Fixed-size window statistics
-Longest/shortest substring matching a condition
Template:-Expand right pointer
-Whenwindow invalid, shrink left pointer
-Update answer at each valid state
Example
// JavaScript - Longest substring without repeating charactersfunctionlengthOfLongestSubstring(s){const charIdx =newMap();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;}
# Python
def length_of_longest_substring(s):
char_idx ={}
max_len = left =0for right, ch inenumerate(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
// JavaScriptfunctionmergeIntervals(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;}
# Python
def 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 sortfunctiontopKFrequent(nums, k){const freq =newMap();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 importCounter
def top_k_frequent(nums, k):
freq =Counter(nums)
# nlargest uses a heap internally
return[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 XORCore idea: prefix[i]- prefix[j]= sum of arr[j..i-1]
Example
// JavaScript - Count subarrays with sum divisible by kfunctionsubarraysDivByK(arr, k){const modCount =newMap([[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;}
# Python
def 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
UseBFS when:-Shortest path in unweighted graph
-Level-by-level processing
-Closest node / nearest distance
UseDFS when:-Exploring all paths / all possibilities
-Cycle detection
-Topological sort
-Treetraversals(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 gridfunctionminSteps(grid, start, end){const rows = grid.length, cols = grid[0].length;const queue =[[...start,0]];// [row, col, steps]const visited =newSet([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 below
while 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 and 0<= nc < cols and grid[nr][nc]==0and(nr,nc) not in 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