DSA Problem Solving Patterns
1. Sliding Window
Concept:
Use a "window" of elements and slide it over the array to reduce repeated calculations.
When to use:
When you are asked to find something in a subarray, like max sum, longest substring,
etc.
How it works:
1. Start with a window of size k.
2. Slide it one step at a time (move left and right pointers).
3. Keep track of what you need (sum, length, etc.).
Example:
Find the maximum sum of a subarray of size 3 in the array [2, 1, 5, 1, 3, 2].
Code:
def max_sum_subarray(arr, k):
window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
print(max_sum_subarray([2, 1, 5, 1, 3, 2], 3)) # Output: 9
2. Two Pointers
Concept:
Use two pointers to scan from both ends or move one ahead of another.
When to use:
• Sorted arrays
• Finding pairs
• Removing duplicates
How it works:
1. Set left = 0 and right = n-1.
2. Move them based on a condition (e.g., if sum is too big, move right back).
Example:
Find if any two numbers in a sorted array add up to a target.
Code:
def has_pair_with_sum(arr, target):
left, right = 0, len(arr) - 1
while left < right:
curr_sum = arr[left] + arr[right]
if curr_sum == target:
return True
elif curr_sum < target:
left += 1
else:
right -= 1
return False
print(has_pair_with_sum([1, 2, 3, 4, 6], 6)) # Output: True
3. Fast and Slow Pointers (a.k.a. Floyd's Cycle Detection)
Concept:
Move one pointer fast (2 steps) and one slow (1 step). If there's a cycle, they'll meet.
When to use:
• Detecting loops in linked lists
• Finding the middle of a list
How it works:
1. Start both pointers at the head.
2. Move slow by 1 step, fast by 2 steps.
3. If fast ever equals slow, there's a cycle.
Example:
Detect a cycle in a linked list.
Code:
class Node:
def __init__(self, value):
self.value = value
self.next = None
def has_cycle(head):
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
4. Merge Intervals
Concept:
Sort the intervals and merge them if they overlap.
When to use:
• Merging calendar events
• Finding total occupied time slots
How it works:
1. Sort by start time.
2. Check if the current interval overlaps with the previous.
3. If yes, merge them.
Example:
Merge [[1,3], [2,6], [8,10], [15,18]] into non-overlapping intervals.
Code:
def merge_intervals(intervals):
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for i in range(1, len(intervals)):
prev = merged[-1]
curr = intervals[i]
if curr[0] <= prev[1]: # overlap
prev[1] = max(prev[1], curr[1])
else:
merged.append(curr)
return merged
print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# Output: [[1,6],[8,10],[15,18]]
5. Cyclic Sort
Concept:
Used to sort elements from 1 to n in-place in linear time.
When to use:
• Arrays with numbers from 1 to n
• Finding missing or duplicate numbers
How it works:
1. At index i, check if nums[i] == i+1.
2. If not, swap it with the correct position.
Example:
Find the missing number from [4, 3, 2, 7, 8, 2, 3, 1].
Code:
def find_missing_number(nums):
i=0
while i < len(nums):
correct = nums[i] - 1
if nums[i] > 0 and nums[i] <= len(nums) and nums[i] != nums[correct]:
nums[i], nums[correct] = nums[correct], nums[i]
else:
i += 1
# After sorting
for i in range(len(nums)):
if nums[i] != i + 1:
return i + 1
return -1
6. In-place Reversal of Linked List
Concept:
Reverse part or all of a linked list by adjusting the next pointers — without using extra
space.
When to use:
• Reverse entire linked list
• Reverse a portion (between two nodes)
How it works:
1. Use three pointers: prev, curr, next.
2. Reverse the links one-by-one.
Example:
Reverse the linked list: 1 -> 2 -> 3 -> 4 -> 5 → 5 -> 4 -> 3 -> 2 -> 1
Code:
def reverse_linked_list(head):
prev = None
curr = head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
return prev # New head
7. Breadth-First Search (BFS - Tree/Graph)
Concept:
Explore level by level using a queue.
When to use:
• Shortest path in unweighted graphs
• Tree level order traversal
How it works:
1. Start at root or source.
2. Push neighbors into queue and mark visited.
Example:
Print a binary tree level-by-level.
Code:
from collections import deque
def bfs(root):
if not root:
return []
queue = deque([root])
result = []
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
8. Depth-First Search (DFS - Tree/Graph)
Concept:
Explore one branch as deep as possible before backtracking.
When to use:
• Path-finding problems
• Tree recursion
How it works:
1. Go to the deepest child node recursively.
2. Then backtrack.
Example:
Find all root-to-leaf paths in a binary tree.
Code:
def dfs(root, path, result):
if not root:
return
path.append(root.val)
if not root.left and not root.right:
result.append(list(path))
else:
dfs(root.left, path, result)
dfs(root.right, path, result)
path.pop() # backtrack
9. Two Heaps
Concept:
Use a min-heap and max-heap to balance numbers for median in a stream.
When to use:
• Finding median of data stream
How it works:
1. One heap keeps smaller half (max heap)
2. Other heap keeps larger half (min heap)
3. Balance them after every insert
Example:
Add numbers and get median after each one.
Code:
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # max heap (invert min heap)
self.large = [] # min heap
def add_num(self, num):
heapq.heappush(self.small, -num)
heapq.heappush(self.large, -heapq.heappop(self.small))
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def find_median(self):
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2
10. Subsets
Concept:
Use recursion or backtracking to explore all combinations.
When to use:
• Generate all subsets
• Power set problems
How it works:
1. Include or exclude each element.
2. Use recursion to explore paths.
Example:
Find all subsets of [1, 2] → [[], [1], [2], [1,2]]
Code:
def subsets(nums):
result = []
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i+1, path)
path.pop()
backtrack(0, [])
return result
11. Modified Binary Search
Concept:
Binary search but adapted for rotated arrays, peaks, etc.
When to use:
• Rotated arrays
• Bitonic arrays
• Search in unknown patterns
How it works:
1. Use binary search logic
2. Check mid and decide which half to explore
Example:
Search in rotated sorted array [4,5,6,7,0,1,2] for 0
Code:
def search_rotated(arr, target):
left, right = 0, len(arr)-1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
if arr[left] <= arr[mid]: # Left sorted
if arr[left] <= target < arr[mid]:
right = mid - 1
else:
left = mid + 1
else: # Right sorted
if arr[mid] < target <= arr[right]:
left = mid + 1
else:
right = mid - 1
return -1
12. Bitwise XOR
Concept:
XOR cancels out equal numbers (a ^ a = 0).
When to use:
• Find single non-repeating number
How it works:
1. XOR all elements.
2. Duplicates cancel out.
Example:
[1, 2, 3, 2, 1] → Output: 3
Code:
def single_number(arr):
result = 0
for num in arr:
result ^= num
return result
13. Top ‘K’ Elements
Concept:
Use a heap (min or max) to keep track of k best elements.
When to use:
• K most frequent, largest, smallest items
Example:
Find 2 most frequent elements in [1,1,1,2,2,3]
Code:
from collections import Counter
import heapq
def top_k_frequent(nums, k):
count = Counter(nums)
return [num for num, _ in heapq.nlargest(k, count.items(), key=lambda x: x[1])]
14. K-way Merge
Concept:
Merge k sorted lists using a min-heap.
When to use:
• Merge sorted files
• Merge k linked lists
Example:
Merge [[1,4,5],[1,3,4],[2,6]] → [1,1,2,3,4,4,5,6]
Code:
import heapq
def merge_k_lists(lists):
min_heap = []
for i, l in enumerate(lists):
if l:
heapq.heappush(min_heap, (l[0], i, 0))
result = []
while min_heap:
val, list_idx, element_idx = heapq.heappop(min_heap)
result.append(val)
if element_idx + 1 < len(lists[list_idx]):
next_val = lists[list_idx][element_idx + 1]
heapq.heappush(min_heap, (next_val, list_idx, element_idx + 1))
return result
15. 0/1 Knapsack
Concept:
Dynamic Programming technique to maximize value under capacity.
When to use:
• Pick/not-pick decisions
• Weight constraints
Example:
Max value in a knapsack of capacity W with given weights and values.
Code:
def knapsack(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(capacity+1):
if weights[i-1] <= w:
dp[i][w] = max(values[i-1] + dp[i-1][w - weights[i-1]], dp[i-1][w])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
16. Topological Sort (Graph)
Concept:
Sort graph nodes where A depends on B (A → B).
When to use:
• Task scheduling
• Course prerequisites
How it works:
1. Use DFS or BFS (Kahn's algo).
2. Track in-degrees and visit nodes with in-degree 0.
Code:
from collections import deque, defaultdict
def topological_sort(vertices, edges):
graph = defaultdict(list)
in_degree = {i: 0 for i in range(vertices)}
for u, v in edges:
graph[u].append(v)
in_degree[v] += 1
queue = deque([v for v in in_degree if in_degree[v] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result