0% found this document useful (0 votes)
4 views

DSA Problem Solving Patterns

The document outlines various data structure and algorithm (DSA) problem-solving patterns, including techniques such as Sliding Window, Two Pointers, Fast and Slow Pointers, and more. Each pattern is described with its concept, when to use it, how it works, and includes example code. The patterns cover a range of applications from searching and sorting to graph traversal and dynamic programming.

Uploaded by

s.shreeyaas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

DSA Problem Solving Patterns

The document outlines various data structure and algorithm (DSA) problem-solving patterns, including techniques such as Sliding Window, Two Pointers, Fast and Slow Pointers, and more. Each pattern is described with its concept, when to use it, how it works, and includes example code. The patterns cover a range of applications from searching and sorting to graph traversal and dynamic programming.

Uploaded by

s.shreeyaas
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 16

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

You might also like