Question Banks
DSA Coding Problems
25 must-know problems with complete solutions, complexities, and follow-ups
P1: Two Sum Hash Map O(n)
Problem: Given array of integers and target, return indices of two numbers that add to target.
Approach: Single-pass hash map. For each number, check if complement exists.
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return [] Complexity: O(n) time, O(n) space.
Follow-up: What if the array is sorted? → Two pointers O(n) time O(1) space.
P2: Merge Intervals Sort + Merge
Problem: Given array of intervals, merge all overlapping intervals.
Approach: Sort by start. Iterate and merge if current overlaps with last merged.
def merge(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 Complexity: O(n log n) time, O(n) space.
P3: LRU Cache DLL + Hash Map
Problem: Implement LRU cache with O(1) get and put.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.cap = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.cap:
self.cache.popitem(last=False) P4: Merge K Sorted Lists Min-Heap
Problem: Given k sorted linked lists, merge into one sorted list.
Approach: Min-heap of size k tracking smallest across all lists. Pop min, push its next.
Complexity: O(N log k) time, O(k) space.
Follow-up: What if lists are on different machines? (distributed merge)
P5: Longest Increasing Subsequence O(n log n)
Problem: Find length of longest strictly increasing subsequence.
Approach: Patience sorting with binary search. Maintain tails array.
import bisect
def length_of_lis(nums):
tails = []
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails) Complexity: O(n log n) time, O(n) space.
P6: Binary Search in Rotated Sorted Array
Problem: Search target in a sorted array that has been rotated at some pivot. Return index or -1.
Approach: Modified binary search. One half is always sorted determine which half target falls in.
def search_rotated(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
# Left half is sorted
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
# Right half is sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1 Complexity: O(log n) time, O(1) space.
Follow-up: What if duplicates exist? → Worst case O(n) when all elements equal except one.
P7: Sliding Window Maximum (Deque)
Problem: Given array and window size k, return max of each sliding window.
Approach: Monotonic decreasing deque storing indices. Front always has current window max.
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # stores indices, front = max
result = []
for i, num in enumerate(nums):
# Remove elements outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove smaller elements from back
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return result Complexity: O(n) time, O(k) space.
Follow-up: How to handle this in a stream where you can't index? → Use a max-heap with lazy deletion.
P8: Word Break (Dynamic Programming)
Problem: Given a string and dictionary of words, determine if string can be segmented into dictionary words.
Approach: DP where dp[i] = True if s[0:i] can be segmented.
def word_break(s, word_dict):
word_set = set(word_dict)
dp = [False] * (len(s) + 1)
dp[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set:
dp[i] = True
break
return dp[len(s)] Complexity: O(n² · m) time where m is avg word length for hashing, O(n) space.
Follow-up: Return all possible segmentations? → Backtracking with memoization.
P9: Clone Graph (BFS)
Problem: Given a node in a connected undirected graph, return a deep copy.
Approach: BFS with a hash map mapping original → clone.
from collections import deque
def clone_graph(node):
if not node:
return None
cloned = {node: Node(node.val)}
queue = deque([node])
while queue:
curr = queue.popleft()
for neighbor in curr.neighbors:
if neighbor not in cloned:
cloned[neighbor] = Node(neighbor.val)
queue.append(neighbor)
cloned[curr].neighbors.append(cloned[neighbor])
return cloned[node] Complexity: O(V + E) time, O(V) space.
Follow-up: What about a directed graph with cycles? → Same approach works since we track visited via cloned map.
P10: Course Schedule (Topological Sort)
Problem: Given n courses and prerequisites, determine if all courses can be finished (detect cycle in DAG).
Approach: Kahn's algorithm BFS with in-degree tracking.
from collections import deque, defaultdict
def can_finish(num_courses, prerequisites):
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
completed = 0
while queue:
node = queue.popleft()
completed += 1
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return completed == num_courses Complexity: O(V + E) time, O(V + E) space.
Follow-up: Return the actual ordering? → Collect nodes as you pop from queue.
P11: Find Median from Data Stream (Two Heaps)
Problem: Design a data structure that supports addNum and findMedian in O(log n) and O(1).
Approach: Max-heap for lower half, min-heap for upper half. Keep balanced.
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negate values)
self.hi = [] # min-heap
def add_num(self, num):
heapq.heappush(self.lo, -num)
# Ensure max of lo <= min of hi
heapq.heappush(self.hi, -heapq.heappop(self.lo))
# Balance sizes: lo can have at most 1 more
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def find_median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2.0 Complexity: addNum O(log n), findMedian O(1).
Follow-up: What if 99% of numbers are between 0-100? → Bucket counting for O(1) operations.
P12: Serialize/Deserialize Binary Tree
Problem: Design an algorithm to serialize and deserialize a binary tree.
Approach: Preorder traversal with null markers.
class Codec:
def serialize(self, root):
result = []
def dfs(node):
if not node:
result.append("N")
return
result.append(str(node.val))
dfs(node.left)
dfs(node.right)
dfs(root)
return ",".join(result)
def deserialize(self, data):
values = iter(data.split(","))
def dfs():
val = next(values)
if val == "N":
return None
node = TreeNode(int(val))
node.left = dfs()
node.right = dfs()
return node
return dfs() Complexity: O(n) time and space for both operations.
Follow-up: How to serialize a BST more efficiently? → Only preorder needed (no null markers), rebuild using value bounds.
P13: Minimum Window Substring (Sliding Window)
Problem: Given strings s and t, find the minimum window in s containing all characters of t.
Approach: Expand window right, contract left when all chars satisfied.
from collections import Counter
def min_window(s, t):
need = Counter(t)
missing = len(t)
left = 0
start, end = 0, float('inf')
for right, char in enumerate(s):
if need[char] > 0:
missing -= 1
need[char] -= 1
while missing == 0:
if right - left < end - start:
start, end = left, right
need[s[left]] += 1
if need[s[left]] > 0:
missing += 1
left += 1
return "" if end == float('inf') else s[start:end + 1] Complexity: O(|s| + |t|) time, O(|t|) space.
Follow-up: What if you need ALL minimum windows (not just one)? → Collect all windows with length == best.
P14: Trapping Rain Water (Two Pointers)
Problem: Given elevation map, compute how much water it can trap.
Approach: Two pointers tracking left_max and right_max.
def trap(height):
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return water Complexity: O(n) time, O(1) space.
Follow-up: 2D version (trapping rain water on a matrix)? → BFS with min-heap from borders inward.
P15: Valid Parentheses Generator (Backtracking)
Problem: Generate all valid combinations of n pairs of parentheses.
Approach: Backtracking with open/close counts.
def generate_parenthesis(n):
result = []
def backtrack(curr, open_count, close_count):
if len(curr) == 2 * n:
result.append(curr)
return
if open_count < n:
backtrack(curr + "(", open_count + 1, close_count)
if close_count < open_count:
backtrack(curr + ")", open_count, close_count + 1)
backtrack("", 0, 0)
return result Complexity: O(4^n / √n) time (Catalan number), O(n) recursion depth.
P16: Number of Islands (DFS/BFS)
Problem: Given a 2D grid of '1' (land) and '0' (water), count number of islands.
Approach: DFS from each unvisited '1', mark all connected cells as visited.
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] != '1':
return
grid[r][c] = '0' # mark visited
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count Complexity: O(m × n) time, O(m × n) space worst case (recursion stack).
Follow-up: What if grid is too large for recursion? → Iterative BFS with explicit queue or Union-Find.
P17: Kth Largest Element (Quickselect)
Problem: Find kth largest element in unsorted array without sorting.
Approach: Quickselect partition around pivot, recurse on relevant half.
import random
def find_kth_largest(nums, k):
target = len(nums) - k # kth largest = (n-k)th smallest
def quickselect(left, right):
pivot_idx = random.randint(left, right)
nums[pivot_idx], nums[right] = nums[right], nums[pivot_idx]
pivot = nums[right]
store = left
for i in range(left, right):
if nums[i] < pivot:
nums[i], nums[store] = nums[store], nums[i]
store += 1
nums[store], nums[right] = nums[right], nums[store]
if store == target:
return nums[store]
elif store < target:
return quickselect(store + 1, right)
else:
return quickselect(left, store - 1)
return quickselect(0, len(nums) - 1) Complexity: O(n) average, O(n²) worst case. O(1) extra space.
Follow-up: Guaranteed O(n)? → Median-of-medians pivot selection (impractical but theoretical).
P18: Dijkstra's Shortest Path
Problem: Find shortest path from source to all vertices in weighted graph (non-negative weights).
Approach: Priority queue (min-heap) processing nearest unvisited vertex first.
import heapq
from collections import defaultdict
def dijkstra(graph, source, n):
dist = [float('inf')] * n
dist[source] = 0
heap = [(0, source)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]:
continue # stale entry
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
heapq.heappush(heap, (dist[v], v))
return dist Complexity: O((V + E) log V) with binary heap, O(V) space.
Follow-up: Negative weights? → Bellman-Ford O(VE). Negative cycles? → Bellman-Ford detects them.
P19: Union-Find with Path Compression
Problem: Implement efficient disjoint set with union by rank and path compression.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # path compression
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
# Union by rank
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True Complexity: Nearly O(1) per operation O(α(n)) amortized (inverse Ackermann).
Use cases: Kruskal's MST, connected components, cycle detection in undirected graphs.
P20: Maximum Subarray (Kadane's Algorithm)
Problem: Find contiguous subarray with maximum sum.
Approach: Track current max ending at each position. Reset if negative.
def max_subarray(nums):
max_sum = curr_sum = nums[0]
for num in nums[1:]:
curr_sum = max(num, curr_sum + num)
max_sum = max(max_sum, curr_sum)
return max_sum Complexity: O(n) time, O(1) space.
Follow-up: Return the actual subarray? → Track start/end indices when max_sum updates. Circular array? → max(kadane, total_sum - min_subarray).
P21: Word Search II (Trie + DFS)
Problem: Given a board of characters and a list of words, find all words that exist on the board.
Approach: Build Trie from word list, then DFS from each cell matching Trie paths.
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
def find_words(board, words):
root = TrieNode()
for word in words:
node = root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.word = word
rows, cols = len(board), len(board[0])
result = []
def dfs(r, c, node):
ch = board[r][c]
if ch not in node.children:
return
child = node.children[ch]
if child.word:
result.append(child.word)
child.word = None # avoid duplicates
board[r][c] = '#' # mark visited
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
dfs(nr, nc, child)
board[r][c] = ch # restore
# Prune: remove leaf nodes
if not child.children:
del node.children[ch]
for r in range(rows):
for c in range(cols):
dfs(r, c, root)
return result Complexity: O(m × n × 4^L) where L = max word length. Trie pruning makes it practical.
P22: Alien Dictionary (Topological Sort)
Problem: Given sorted list of words in alien language, derive character ordering.
Approach: Build graph from adjacent word comparisons, then topological sort.
from collections import defaultdict, deque
def alien_order(words):
# Build adjacency and in-degree
graph = defaultdict(set)
in_degree = {c: 0 for word in words for c in word}
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
min_len = min(len(w1), len(w2))
# Check invalid case: prefix ordering
if len(w1) > len(w2) and w1[:min_len] == w2[:min_len]:
return ""
for j in range(min_len):
if w1[j] != w2[j]:
if w2[j] not in graph[w1[j]]:
graph[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
break
# BFS topological sort
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
c = queue.popleft()
result.append(c)
for neighbor in graph[c]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
if len(result) != len(in_degree):
return "" # cycle detected
return "".join(result) Complexity: O(C) where C = total characters across all words.
P23: Best Time to Buy and Sell Stock (State Machine DP)
Problem: With at most k transactions, find max profit. Generalized stock problem.
Approach: State machine with states: holding, not holding, for each transaction.
def max_profit(k, prices):
if not prices:
return 0
n = len(prices)
if k >= n // 2:
# Unlimited transactions
return sum(max(0, prices[i+1] - prices[i]) for i in range(n-1))
# dp[t][0] = max profit with t transactions, not holding
# dp[t][1] = max profit with t transactions, holding
dp = [[0, float('-inf')] for _ in range(k + 1)]
for price in prices:
for t in range(k, 0, -1):
dp[t][0] = max(dp[t][0], dp[t][1] + price) # sell
dp[t][1] = max(dp[t][1], dp[t-1][0] - price) # buy
return dp[k][0] Complexity: O(nk) time, O(k) space.
Follow-up: With cooldown? → Add a "cooldown" state: can't buy on day after selling.
P24: Edit Distance (2D DP)
Problem: Find minimum operations (insert, delete, replace) to convert word1 to word2.
Approach: Classic 2D DP. dp[i][j] = edit distance of word1[0:i] and word2[0:j].
def min_distance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # delete
dp[i][j-1], # insert
dp[i-1][j-1] # replace
)
return dp[m][n] Complexity: O(mn) time, O(mn) space. Can optimize to O(min(m,n)) space with rolling array.
Follow-up: What if operations have different costs? → Modify the min() weights accordingly.
P25: Design Twitter (OOP + Heap)
Problem: Design a simplified Twitter with postTweet, getNewsFeed, follow, unfollow.
Approach: User → set of followees. Each user has tweet list. News feed = merge k sorted lists (heaps).
import heapq
from collections import defaultdict
class Twitter:
def __init__(self):
self.time = 0
self.tweets = defaultdict(list) # userId -> [(time, tweetId)]
self.following = defaultdict(set) # userId -> set of followees
def post_tweet(self, user_id, tweet_id):
self.tweets[user_id].append((self.time, tweet_id))
self.time += 1
def get_news_feed(self, user_id):
# Merge latest 10 from user + followees
users = self.following[user_id] | {user_id}
heap = []
for uid in users:
if self.tweets[uid]:
idx = len(self.tweets[uid]) - 1
t, tid = self.tweets[uid][idx]
heap.append((-t, tid, uid, idx))
heapq.heapify(heap)
feed = []
while heap and len(feed) < 10:
neg_t, tid, uid, idx = heapq.heappop(heap)
feed.append(tid)
if idx > 0:
t2, tid2 = self.tweets[uid][idx - 1]
heapq.heappush(heap, (-t2, tid2, uid, idx - 1))
return feed
def follow(self, follower_id, followee_id):
self.following[follower_id].add(followee_id)
def unfollow(self, follower_id, followee_id):
self.following[follower_id].discard(followee_id) Complexity: postTweet O(1), getNewsFeed O(F·log F) where F = followees, follow/unfollow O(1).
Follow-up: How would you scale this? → Fan-out-on-write for normal users, fan-out-on-read for celebrities (hybrid approach).