20 Essential DSA Patterns in Python For Interviews
20 Essential DSA Patterns in Python For Interviews
Table of Contents
1. Sliding Window Pattern
2. Two Pointer Pattern
3. Fast & Slow Pointers Pattern
4. Merge Intervals Pattern
5. Cyclic Sort Pattern
6. In-place Reversal of a Linked List
7. Breadth-First Search (BFS)
8. Depth-First Search (DFS)
9. Two Heaps Pattern
10. Subsets Pattern
11. Modified Binary Search
12. Topological Sort
13. Bitwise XOR Pattern
14. Fast Exponentiation
15. Greedy Pattern
16. Dynamic Programming
17. Matrix Traversal
18. Backtracking
19. Divide & Conquer
20. Graph Traversal
Pattern Overview
The Sliding Window Pattern is a powerful technique for solving problems involving arrays or strings
where you need to examine contiguous sequences of elements. Instead of using nested loops (O(n²)), this
pattern maintains a "window" that slides through the data, achieving O(n) time complexity.
When to Use:
Finding longest/shortest substring with specific properties
Finding maximum/minimum sum of subarray of size k
String permutation problems
Any problem dealing with contiguous elements
How it Works:
Array: [1, 3, 2, 6, -1, 4, 1, 8, 2]
Window size: 3
... and so on
return max_sum
Find the length of the smallest contiguous subarray whose sum is greater than or equal to a given value.
Find the length of the longest substring with at most k distinct characters.
freq_map = defaultdict(int)
max_length = 0
start = 0
return max_length
You have two baskets, and each basket can carry any quantity of one type of fruit. Find the maximum
number of fruits you can collect.
def fruitsIntoBaskets(fruits):
from collections import defaultdict
basket = defaultdict(int)
max_fruits = 0
start = 0
def noRepeatSubstring(s):
char_index = {}
max_length = 0
start = 0
char_index[right_char] = end
max_length = max(max_length, end - start + 1)
return max_length
Pattern Overview
The Two Pointer Pattern uses two pointers to traverse data structures, typically arrays or linked lists. These
pointers can move towards each other (from opposite ends) or in the same direction at different speeds.
This pattern is excellent for reducing time complexity from O(n²) to O(n).
When to Use:
Sorted arrays or linked lists
Finding pairs with specific sum
Comparing elements from both ends
Removing duplicates in-place
Palindrome problems
How it Works:
Sorted Array: [1, 2, 3, 4, 6, 8, 9]
Target Sum: 10
left right
↓ ↓
[1, 2, 3, 4, 6, 8, 9]
Sum = 1 + 9 = 10 ✓ Found!
Problems:
if current_sum == target:
return [left, right]
def removeDuplicates(nums):
if not nums:
return 0
next_non_duplicate = 1
return next_non_duplicate
Given a sorted array, create a new array containing squares of all numbers sorted in ascending order.
def makeSquares(arr):
n = len(arr)
squares = [0] * n
left, right = 0, n - 1
highest_square_idx = n - 1
highest_square_idx -= 1
return squares
def searchTriplets(arr):
arr.sort()
triplets = []
if current_sum == 0:
triplets.append([arr[i], arr[left], arr[right]])
left += 1
right -= 1
return triplets
def dutchFlagSort(arr):
low, high = 0, len(arr) - 1
i = 0
Pattern Overview
The Fast & Slow Pointers pattern (also known as Floyd's Tortoise and Hare algorithm) uses two pointers
that move at different speeds through a data structure. This pattern is particularly useful for detecting
cycles and finding middle elements in linked lists.
When to Use:
Detecting cycles in linked lists
Finding the middle of a linked list
Determining if a number is happy
Finding the start of a cycle
Palindrome linked list problems
How it Works:
Linked List with Cycle:
1→2→3→4
↑ ↓
7←6←5
Problems:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def hasCycle(head):
slow = head
fast = head
if slow == fast:
return True
return False
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def detectCycle(head):
slow = head
fast = head
# First phase: detect cycle
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# Second phase: find entry point
ptr1 = head
while ptr1 != slow:
ptr1 = ptr1.next
slow = slow.next
return ptr1
return None
A happy number is defined by the following process: replace the number by the sum of the squares of its
digits, repeat until it equals 1 or loops endlessly.
class HappyNumber:
def findSquareSum(self, num):
total = 0
while num > 0:
digit = num % 10
total += digit * digit
num //= 10
return total
while True:
slow = self.findSquareSum(slow)
fast = self.findSquareSum(self.findSquareSum(fast))
if slow == fast:
break
return slow == 1
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def middleNode(head):
slow = head
fast = head
return slow
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def isPalindrome(head):
if not head or not head.next:
return True
return True
def reverse(head):
prev = None
while head:
nxt = head.next
head.next = prev
prev = head
head = nxt
return prev
Pattern Overview
The Merge Intervals pattern deals with overlapping intervals. It's commonly used in problems involving
time intervals, meeting rooms, or any scenario where you need to merge or manipulate overlapping
ranges.
When to Use:
Calendar scheduling problems
Meeting room allocation
Merging overlapping ranges
Finding gaps in intervals
Interval intersection problems
How it Works:
Input intervals: [[1,3], [2,6], [8,10], [15,18]]
Problems:
def merge(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = []
current = intervals[0]
merged.append(current)
return merged
result.append(newInterval)
while i < n:
result.append(intervals[i])
i += 1
return result
return result
def canAttendMeetings(intervals):
intervals.sort(key=lambda x: x[0])
return True
def minMeetingRooms(intervals):
if not intervals:
return 0
rooms = 0
end_ptr = 0
for i in range(len(intervals)):
if start_times[i] < end_times[end_ptr]:
rooms += 1
else:
end_ptr += 1
return rooms
When to Use:
Array contains numbers from 0 to n or 1 to n
Finding missing numbers
Finding duplicate numbers
Finding the smallest missing positive number
Problems involving array indices
How it Works:
Array: [3, 1, 5, 4, 2]
[5, 1, 3, 4, 2]
[2, 1, 3, 4, 5]
Final: [1, 2, 3, 4, 5]
Problems:
def cyclicSort(nums):
i = 0
while i < len(nums):
correct_index = nums[i] - 1
if nums[i] != nums[correct_index]:
nums[i], nums[correct_index] = nums[correct_index], nums[i]
else:
i += 1
def findMissingNumber(nums):
i = 0
while i < len(nums):
if nums[i] < len(nums) and nums[i] != nums[nums[i]]:
nums[i], nums[nums[i]] = nums[nums[i]], nums[i]
else:
i += 1
for i in range(len(nums)):
if nums[i] != i:
return i
return len(nums)
def findMissingNumbers(nums):
i = 0
while i < len(nums):
correctIndex = nums[i] - 1
if nums[i] != nums[correctIndex]:
nums[i], nums[correctIndex] = nums[correctIndex], nums[i]
else:
i += 1
missingNumbers = []
for i in range(len(nums)):
if nums[i] != i + 1:
missingNumbers.append(i + 1)
return missingNumbers
def findDuplicateNumber(nums):
i = 0
while i < len(nums):
if nums[i] != i + 1:
if nums[i] == nums[nums[i] - 1]:
return nums[i]
nums[i], nums[nums[i] - 1] = nums[nums[i] - 1], nums[i]
else:
i += 1
return -1
def findAllDuplicates(nums):
i = 0
while i < len(nums):
correctIndex = nums[i] - 1
if nums[i] != nums[correctIndex]:
nums[i], nums[correctIndex] = nums[correctIndex], nums[i]
else:
i += 1
duplicates = []
for i in range(len(nums)):
if nums[i] != i + 1:
duplicates.append(nums[i])
return duplicates
Pattern Overview
This pattern reverses linked lists or portions of linked lists in-place without using extra memory. It's
fundamental for many linked list problems and often combined with other patterns.
When to Use:
Reversing entire linked lists
Reversing sub-lists
Reversing every k nodes
Rotating linked lists
Problems requiring link manipulation
How it Works:
Original: 1 → 2 → 3 → 4 → 5 → null
Step 1: null ← 1 2 → 3 → 4 → 5
Step 2: null ← 1 ← 2 3 → 4 → 5
Step 3: null ← 1 ← 2 ← 3 4 → 5
...
Final: null ← 1 ← 2 ← 3 ← 4 ← 5
Problems:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def reverseList(head):
prev = None
curr = head
while curr:
next_temp = curr.next
curr.next = prev
prev = curr
curr = next_temp
return prev
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
dummy = ListNode(0)
dummy.next = head
prev = dummy
start = prev.next
then = start.next
return dummy.next
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
if count == k:
# Step 2: Recursively process the remaining list
curr = reverseKGroup(curr, k)
return head
Problem 4: Rotate a Linked List
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
return newHead
Reverse the nodes of a linked list k at a time. Nodes that do not form a complete k-group should remain
as they are
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
if count == k:
# Recursively reverse the remaining list
curr = reverseKGroup(curr, k)
return head
Pattern Overview:
Breadth-First Search is an algorithm for traversing or searching tree and graph data structures level-by-
level, visiting all nodes at the current depth before moving to nodes at the next depth level. BFS is
typically implemented using a queue.
When to Use:
Shortest path or minimum steps problems.
Level-order traversals (trees, graphs).
Finding nodes at a certain distance.
/ \
2 3
/\ /\
4 56 7
BFS Traversal: 1 → 2 → 3 → 4 → 5 → 6 → 7
Problems:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def levelOrder(root):
result = []
if not root:
return result
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
current = queue.popleft()
level.append(current.val)
if current.left:
queue.append(current.left)
if current.right:
queue.append(current.right)
result.append(level)
return result
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def minDepth(root):
if not root:
return 0
queue = deque([root])
depth = 1
while queue:
size = len(queue)
for _ in range(size):
node = queue.popleft()
if not node.left and not node.right:
return depth
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
depth += 1
return depth
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def rightSideView(root):
result = []
if not root:
return result
queue = deque([root])
while queue:
size = len(queue)
for i in range(size):
node = queue.popleft()
if i == size - 1:
result.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return result
Problem 4: Rotten Oranges (LeetCode 994)
def orangesRotting(grid):
if not grid:
return 0
for i in range(rows):
for j in range(cols):
if grid[i][j] == 2:
queue.append((i, j))
elif grid[i][j] == 1:
fresh += 1
if fresh == 0:
return 0
while queue:
size = len(queue)
rotted = False
for _ in range(size):
x, y = queue.popleft()
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 1:
grid[nx][ny] = 2
queue.append((nx, ny))
fresh -= 1
rotted = True
if rotted:
minutes += 1
for a, b in prerequisites:
adj[b].append(a)
indegree[a] += 1
while queue:
course = queue.popleft()
count += 1
for neighbor in adj[course]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
Pattern Overview:
Depth-First Search (DFS) is an algorithm that explores nodes in depth-wise order, going as deep as
possible before backtracking. It is implemented using recursion or stacks.
When to Use:
Finding paths, connectivity, or cycles in graphs and trees.
Searching deeper before breadth (opposite of BFS).
Top-down problems where decisions affect subsequent paths (backtracking).
/\
2 3
/\ \
4 5 6
DFS Traversal: 1 → 2 → 4 → 5 → 3 → 6
Problems:
def maxDepth(root):
if root is None:
return 0
left = maxDepth(root.left)
right = maxDepth(root.right)
return max(left, right) + 1
def binaryTreePaths(root):
paths = []
dfsPaths(root, "", paths)
return paths
def numIslands(grid):
if not grid:
return 0
islands = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
dfs(grid, i, j)
islands += 1
return islands
Pattern Overview:
The Two Heaps pattern leverages two heap data structures (a min-heap and a max-heap) simultaneously,
allowing efficient retrieval of both minimum and maximum values or median calculations. This technique
is extremely efficient for streaming data problems or scenarios involving frequent min/max queries.
When to Use:
Problems involving real-time or streaming data.
Finding median from a continuous stream.
Efficient retrieval of the largest/smallest elements frequently.
Balancing two opposite extremes.
[3] [] 3
[1] [3] 2
[1,3] [5] 3
Problems:
import heapq
class MedianFinder:
def __init__(self):
self.maxHeap = [] # max heap (as negative values)
self.minHeap = [] # min heap
import heapq
from collections import defaultdict
class MedianSlidingWindow:
def medianSlidingWindow(self, nums, k):
result = []
minHeap = [] # min heap for the larger half
maxHeap = [] # max heap for the smaller half (inverted)
delayed = defaultdict(int)
def prune(heap):
while heap and delayed[heap[0][1]] > 0:
delayed[heap[0][1]] -= 1
heapq.heappop(heap)
def balance():
if len(maxHeap) > len(minHeap) + 1:
val = heapq.heappop(maxHeap)[1]
heapq.heappush(minHeap, (val, val))
elif len(maxHeap) < len(minHeap):
val = heapq.heappop(minHeap)[1]
heapq.heappush(maxHeap, (-val, val))
if i >= k:
to_remove = nums[i - k]
delayed[to_remove] += 1
if to_remove <= maxHeap[0][1]:
prune(maxHeap)
else:
prune(minHeap)
balance()
if i >= k - 1:
if k % 2 == 1:
result.append(float(maxHeap[0][1]))
else:
result.append((maxHeap[0][1] + minHeap[0][1]) / 2.0)
return result
import heapq
for _ in range(k):
while minCapital and minCapital[0][0] <= w:
_, profit = heapq.heappop(minCapital)
heapq.heappush(maxProfit, -profit) # Max-heap using negative profits
if not maxProfit:
break
w += -heapq.heappop(maxProfit)
return w
import heapq
for p in points:
dist = -(p[0]**2 + p[1]**2)
heapq.heappush(maxHeap, (dist, p))
if len(maxHeap) > K:
heapq.heappop(maxHeap)
import heapq
def findRightInterval(intervals):
n = len(intervals)
maxStartHeap = [(-intervals[i][0], i) for i in range(n)]
maxEndHeap = [(-intervals[i][1], i) for i in range(n)]
heapq.heapify(maxStartHeap)
heapq.heapify(maxEndHeap)
result = [-1] * n
while maxEndHeap:
end, endIdx = heapq.heappop(maxEndHeap)
end = -end
return result
Pattern Overview:
The Subsets Pattern deals with generating all possible subsets (or combinations, permutations) from a
given set of elements. It's heavily based on recursion and backtracking, where the algorithm explores all
possibilities step by step, then backtracks to explore alternatives.
When to Use:
Problems requiring generation of combinations or permutations.
Finding subsets satisfying certain conditions.
Backtracking scenarios with exhaustive exploration.
Subsets generated:
[]
[1]
[3]
[5]
[1,3]
[1,5]
[3,5]
[1,3,5]
Problems:
def subsets(nums):
result = []
subset = []
def backtrack(start):
result.append(list(subset))
for i in range(start, len(nums)):
subset.append(nums[i])
backtrack(i + 1)
subset.pop()
backtrack(0)
return result
def subsetsWithDup(nums):
nums.sort()
res = []
temp = []
def backtrack(start):
res.append(list(temp))
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue
temp.append(nums[i])
backtrack(i + 1)
temp.pop()
backtrack(0)
return res
def permute(nums):
res = []
temp = []
used = [False] * len(nums)
def backtrack():
if len(temp) == len(nums):
res.append(temp[:])
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
temp.append(nums[i])
backtrack()
temp.pop()
used[i] = False
backtrack()
return res
def letterCasePermutation(s):
res = []
s = list(s)
def backtrack(idx):
if idx == len(s):
res.append(''.join(s))
return
if s[idx].isalpha():
s[idx] = s[idx].lower()
backtrack(idx + 1)
s[idx] = s[idx].upper()
backtrack(idx + 1)
else:
backtrack(idx + 1)
backtrack(0)
return res
backtrack(target, 0, [])
return res
Pattern Overview:
The Modified Binary Search pattern extends traditional binary search to solve problems where a direct
sorted search isn't enough. It’s commonly used for sorted arrays that are rotated, infinite arrays, or arrays
with duplicates.
When to Use:
Sorted arrays with special conditions (rotated, infinite size, unknown boundaries).
Searching for an element with conditions beyond simple equality.
Problems requiring efficient logarithmic search with complex conditions.
Problems:
return -1
def findMin(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
class ArrayReader:
def get(self, index: int) -> int:
# This should be implemented externally
pass
return -1
Problem 4: Peak Index in a Mountain Array (LeetCode 852)
def peakIndexInMountainArray(arr):
left, right = 0, len(arr) - 1
while left < right:
mid = left + (right - left) // 2
if arr[mid] < arr[mid + 1]:
left = mid + 1
else:
right = mid
return left
Problem 5: Search for a Range (Find First and Last Position - LeetCode 34)
Pattern Overview:
The Topological Sort pattern is used to find a linear ordering of vertices in a Directed Acyclic Graph
(DAG) such that for every directed edge u → v, vertex u comes before v in the ordering.
This is widely used in scheduling, build systems, task ordering, and dependency resolution.
When to Use:
Scheduling tasks with dependencies.
Course scheduling problems.
Build order problems.
Cycle detection in directed graphs.
1→2→3
↓ ↑
4→5→6
Core Idea:
Use
BFS
DFS
Problems:
for u, v in edges:
graph[u].append(v)
inDegree[v] += 1
while sources:
vertex = sources.popleft()
sortedOrder.append(vertex)
for child in graph[vertex]:
inDegree[child] -= 1
if inDegree[child] == 0:
sources.append(child)
completed = 0
while queue:
course = queue.popleft()
completed += 1
for next_course in graph[course]:
inDegree[next_course] -= 1
if inDegree[next_course] == 0:
queue.append(next_course)
while queue:
course = queue.popleft()
order.append(course)
def alienOrder(words):
graph = defaultdict(set)
inDegree = {}
while q:
c = q.popleft()
result.append(c)
for nei in graph[c]:
inDegree[nei] -= 1
if inDegree[nei] == 0:
q.append(nei)
while q:
if len(q) > 1:
return False # Multiple choices, no unique sequence
node = q.popleft()
if idx >= len(org) or org[idx] != node:
return False
idx += 1
for nei in graph[node]:
inDegree[nei] -= 1
if inDegree[nei] == 0:
q.append(nei)
When to Use:
Finding the single number in arrays where all other numbers occur in pairs.
Finding two unique numbers in arrays.
Swapping variables without using extra space.
Detecting missing or duplicate numbers using XOR.
Parity check or bitwise tricks.
Array: [1, 2, 3, 2, 1]
Process: 1 ^ 2 ^ 3 ^ 2 ^ 1 = (1 ^ 1) ^ (2 ^ 2) ^ 3 = 0 ^ 0 ^ 3 = 3
Problems:
def singleNumber(nums):
result = 0
for num in nums:
result ^= num
return result
Key Concept:
When all numbers appear twice except one, XOR all elements → duplicates cancel out.
Find the two numbers that appear only once when all others appear twice.
return result
Key Concept:
Use the rightmost set bit to partition numbers into two groups
Key Concept:
XOR from 0 to n and XOR all array elements. The missing number remains.
Key Concept:
Using right shift and bitwise AND to count number of 1’s efficiently
Key Concept:
Simulate binary addition using XOR for sum and AND for carry.
Pattern Overview:
The Fast Exponentiation (or Binary Exponentiation) pattern is an efficient method to compute large
exponents in O(log n) time using the divide and conquer principle.
Instead of multiplying the base n times, it squares the base and divides the exponent by 2 at each step.
When to Use:
Calculating large power values modulo some number.
Modular inverse calculations.
Efficient power calculations for cryptography.
Solving exponential growth problems.
3^13 = 3^(1101)₂
Breakdown:
Steps:
Start with result = 1
If exponent is odd → multiply result by base
Square base each time
Divide exponent by 2
Problems:
N = n
if N < 0:
x = 1 / x
N = -N
result = 1
while N > 0:
if N % 2 == 1:
result *= x
x *= x
N //= 2
return result
Key Concept:
while n > 0:
if n & 1:
result = (result * base) % mod
base = (base * base) % mod
n >>= 1
return result
Key Concept:
while n > 0:
if n % 2 == 1:
result = (result * x) % mod
x = (x * x) % mod
n //= 2
return result
for digit in b:
result = (modPow(result, 10, mod) * modPow(a, digit, mod)) % mod
return result
Key Concept:
Key Concept:
half = fastPower(x, n // 2)
if n % 2 == 0:
return half * half
else:
return half * half * x if n > 0 else half * half / x
Key Concept:
Pattern Overview:
The Greedy Pattern solves optimization problems by making locally optimal choices at each step with
the hope of finding a global optimum.
Greedy algorithms work when the problem has the greedy-choice property and optimal substructure —
meaning a locally optimal solution leads to a globally optimal one.
When to Use:
Interval scheduling problems.
Activity selection problems.
Finding minimum coins or jumps.
Optimization where you can make a greedy decision without revisiting.
Problems requiring minimal/maximal total cost, length, or time.
Problems:
return jumps
Key Concept:
result = []
start, end = 0, 0
for i, ch in enumerate(s):
end = max(end, lastIndex[ord(ch) - ord('a')])
if i == end:
result.append(end - start + 1)
start = i + 1
return result
Key Concept:
Greedily choose the largest partition without overlapping characters.
end = intervals[0][1]
count = 1
for i in range(1, len(intervals)):
if intervals[i][0] >= end:
count += 1
end = intervals[i][1]
return len(intervals) - count
Key Concept:
Greedily keep intervals with the earliest end times to maximize non-overlapping intervals.
def findMinArrowShots(points):
if not points:
return 0
points.sort(key=lambda x: x[1])
arrows = 1
end = points[0][1]
return arrows
Key Concept:
Greedily shoot the arrow at the end of the current balloon's range.
child = cookie = 0
while child < len(g) and cookie < len(s):
if s[cookie] >= g[child]:
child += 1
cookie += 1
return child
Key Concept:
Always assign the smallest available cookie that satisfies the current child.
Pattern Overview:
The Dynamic Programming Pattern solves complex problems by breaking them down into overlapping
subproblems and solving each subproblem only once, storing its result.
When to Use:
Problems that can be divided into smaller, similar subproblems.
Optimization problems (minimum, maximum, longest, shortest).
When recursive brute-force solutions cause repeated calculations.
When memoization or bottom-up tabulation is possible.
Fib(0) = 0, Fib(1) = 1
Problems:
def climbStairs(n):
if n <= 2:
return n
first, second = 1, 2
for _ in range(3, n + 1):
ways = first + second
first = second
second = ways
return second
Key Concept:
Number of ways to reach step n is the sum of ways to reach n-1 and n-2.
def rob(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], nums[i] + dp[i - 2])
return dp[-1]
Key Concept:
At each house, rob it or skip it, whichever yields the maximum profit.
return dp[0][n - 1]
Key Concept:
Key Concept:
Bottom-up calculation to find the minimum number of coins for each amount.
return dp[m][n]
Key Concept:
Classic DP for string transformation problems using insertion, deletion, and substitution.
Pattern Overview:
The Matrix Traversal Pattern is commonly used to solve problems involving 2D grids or matrices. It often
requires exploring cells in a particular direction (up, down, left, right, or diagonally) and may involve
backtracking, BFS, DFS, or simple iteration.
When to Use:
Pathfinding in a matrix.
Counting islands or connected components.
Searching for patterns in grids.
Flood-fill algorithms.
Simulating spread (e.g. infection, fire).
Grid:
11000
01001
10011
00000
10101
Problems:
count = 0
rows, cols = len(grid), len(grid[0])
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
dfs(i, j)
count += 1
return count
Key Concept:
temp = board[i][j]
board[i][j] = '#' # Mark as visited
for i in range(len(board)):
for j in range(len(board[0])):
if dfs(i, j, 0):
return True
return False
Key Concept:
Key Concept:
def floodFill(image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int
oldColor = image[sr][sc]
if oldColor == newColor:
return image
dfs(sr, sc)
return image
Key Concept:
q = deque()
q.append((0, 0))
grid[0][0] = 1 # Mark as visited
# 8 possible directions
dirs = [(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1)]
path_length = 1
while q:
size = len(q)
for _ in range(size):
x, y = q.popleft()
return -1
Key Concept:
Use BFS to find the shortest path in a grid, visiting all eight directions.
Pattern Overview:
The Backtracking Pattern is a systematic way of trying out all possible configurations to solve a problem.
It incrementally builds candidates for the solution and abandons a candidate (“backtracks”) as soon as it
determines that the candidate cannot lead to a valid solution.
Start with empty set → try adding each number → recursively explore → backtrack
Tree structure:
[]
/ | \
1 2 3
/ \ / \ /\
2 33 - -
At each node:
Include the current element.
Exclude the current element.
Backtrack to previous state.
Problems:
backtrack(0, [])
return result
Key Concept:
def backtrack(current):
if len(current) == len(nums):
result.append(current.copy())
return
for num in nums:
if num in current:
continue
current.append(num)
backtrack(current)
current.pop()
backtrack([])
return result
Key Concept:
Key Concept:
def backtrack(row):
if row == n:
# Generate the board configuration
board = []
for i in range(n):
row_str = ['.'] * n
row_str[positions[i]] = 'Q'
board.append(''.join(row_str))
result.append(board)
return
backtrack(0)
return result
Key Concept:
def isValid(board: List[List[str]], row: int, col: int, num: str) -> bool:
# Check row and column
for i in range(9):
if board[row][i] == num or board[i][col] == num:
return False
return True
Key Concept:
Pattern Overview:
The Divide & Conquer Pattern breaks a large problem into smaller subproblems of the same type, solves
them recursively, and combines their solutions to solve the original problem.
When to Use:
Problems that can be split into independent subproblems.
Sorting and searching problems.
Tree problems (naturally divide into subtrees).
Problems where you can break input into smaller chunks.
Problems:
def quick_sort(nums: List[int], low: int = 0, high: int = None) -> None:
if high is None:
high = len(nums) - 1
if low < high:
pivot_index = partition(nums, low, high)
quick_sort(nums, low, pivot_index - 1)
quick_sort(nums, pivot_index + 1, high)
Key Concept:
def quick_sort(nums: list, low: int = 0, high: int = None) -> None:
"""Sorts the list in-place using quicksort algorithm"""
if high is None:
high = len(nums) - 1
if low < high:
pivot_index = partition(nums, low, high)
quick_sort(nums, low, pivot_index - 1)
quick_sort(nums, pivot_index + 1, high)
def partition(nums: list, low: int, high: int) -> int:
"""Lomuto partition scheme"""
pivot = nums[high] # Choose last element as pivot
i = low - 1 # Index for smaller elements
# Example usage
if __name__ == "__main__":
data = [10, 7, 8, 9, 1, 5]
print("Original array:", data)
quick_sort(data)
print("Sorted array:", data)
Key Concept:
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
Key Concept:
if nums[mid] == target:
return mid
return -1
Key Concept:
import random
def quick_select(nums: List[int], left: int, right: int, k_smallest: int) -> int:
"""Recursive quickselect implementation"""
if left == right:
return nums[left]
pivot_index = partition(nums, left, right)
if pivot_index == k_smallest:
return nums[pivot_index]
elif pivot_index < k_smallest:
return quick_select(nums, pivot_index + 1, right, k_smallest)
else:
return quick_select(nums, left, pivot_index - 1, k_smallest)
Key Concept:
Pattern Overview:
The Graph Traversal Pattern is used to explore nodes and edges in a graph using systematic methods
like Breadth-First Search (BFS) and Depth-First Search (DFS).
When to Use:
Problems involving graphs, grids, or networks.
When you need to visit all nodes, find paths, detect cycles, or explore components.
Problems that can be represented as adjacency lists or matrices.
Graph:
A-B-C
| |
D-E
Queue: [A]
Visit: A → B → D → C → E
BFS:
Explore level by level.
Uses a queue.
DFS:
Explore as deep as possible.
Uses
recursion or stack.
Problems:
class Node:
def __init__(self, val = 0, neighbors = None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
while queue:
current = queue.popleft()
for neighbor in current.neighbors:
if neighbor not in node_map:
# Clone the neighbor if not already cloned
node_map[neighbor] = Node(neighbor.val)
queue.append(neighbor)
# Connect the cloned node to its cloned neighbors
node_map[current].neighbors.append(node_map[neighbor])
return node_map[node]
Key Concept:
Key Concept:
def dfs(node):
if visited[node]:
return
visited[node] = True
for neighbor in graph[node]:
dfs(neighbor)
return count
Key Concept:
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if not visited[neighbor]:
visited[neighbor] = True
parent[neighbor] = node
queue.append(neighbor)
elif neighbor != parent[node]:
# Found a cycle
return False
Key Concept:
queue = deque([beginWord])
level = 1
while queue:
level_size = len(queue)
for _ in range(level_size):
current_word = queue.popleft()
for i in range(len(current_word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = current_word[:i] + c + current_word[i+1:]
if next_word == endWord:
return level + 1
if next_word in word_set:
queue.append(next_word)
word_set.remove(next_word)
level += 1
return 0
Key Concept: