50 DSA Interview Questions and Answers (2026)
50 DSA interview questions covering arrays, linked lists, trees, graphs, dynamic programming, and the 8 coding patterns that cover most interview problems. Updated for 2026
On this page
DSA is the single biggest filter in technical interviews, regardless of company size or which language the role uses. The same core data structures and algorithmic patterns show up everywhere: at startups, mid-size product companies, and FAANG alike.
The candidates who do well aren't the ones who've memorized 500 individual LeetCode problems. They're the ones who recognize patterns: "this is a sliding window problem," "this needs two pointers," "this is a graph problem in disguise." Roughly 80 to 90 percent of interview problems map onto a handful of recurring patterns once you learn to see them.
These 50 questions cover both layers: the conceptual fundamentals every candidate should know cold, and the pattern-driven coding problems that make up the bulk of real interviews, each with working code and stated time/space complexity. If you're also preparing for object-oriented design rounds, our OOP interview questions guide covers the four pillars, SOLID principles, and design patterns.
Category 1: Fundamentals and Complexity (Q1-Q8)
Fundamental questions test whether you understand trade-offs before writing code. Interviewers use these to assess whether you think about WHY you reach for a particular data structure, not just whether you can use one.
Q1. What is a data structure and why does choosing the right one matter?
A data structure is a way of organizing and storing data so it can be accessed and modified efficiently. The choice of data structure directly determines how fast your operations run and how much memory your program uses.
The classic interview framing: the same logical problem (storing a collection of items) can be solved with an array, a linked list, a hash map, or a tree, but each choice has dramatically different performance characteristics for insertion, deletion, and lookup. Picking the wrong structure for the access pattern your problem actually needs is one of the most common reasons candidates get a working solution that's too slow.
Why interviewers ask this first: it tests whether you think about trade-offs before writing code, rather than defaulting to the same structure (usually an array or a hash map) for every problem regardless of fit.
Q2. What is the difference between linear and non-linear data structures?
Linear data structures arrange elements sequentially, where each element connects to its previous and next neighbor in a single line. Arrays, linked lists, stacks, and queues are all linear. You can traverse the entire structure in one pass, start to end.
Non-linear data structures organize elements hierarchically or as a network, where an element can connect to multiple other elements in non-sequential ways. Trees and graphs are non-linear. There is no single "start to end" traversal order; you choose a traversal strategy (BFS, DFS, in-order, etc.) based on what you need.
Linear: [1] -> [2] -> [3] -> [4]
Non-linear: [1]
/ \
[2] [3]
/
[4]This distinction matters because the algorithms you reach for differ completely. Linear structures favor simple iteration. Non-linear structures require explicit traversal algorithms (recursion, BFS queues, DFS stacks) since there's no single natural order to visit every element.
Q3. What is Big-O notation and why do interviewers care about it?
Big-O notation describes how an algorithm's running time or space requirement grows as the input size grows, in the worst case. It abstracts away machine-specific constants and focuses on the growth rate, which is what actually matters as input scales from 10 elements to 10 million.
def find_max(arr): # O(n), must check every element once
max_val = arr[0]
for num in arr:
if num > max_val:
max_val = num
return max_val
def binary_search(arr, target): # O(log n), halves the search space each step
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1Common complexity classes, from best to worst: O(1) constant, O(log n) logarithmic, O(n) linear, O(n log n) linearithmic, O(n²) quadratic, O(2^n) exponential, O(n!) factorial.
Interviewers care because a "correct" solution that's O(n²) when an O(n log n) or O(n) solution exists is usually considered an incomplete answer at any company that takes the technical round seriously. They are testing whether you can reason about scalability, not just produce output that matches expected test cases.
Q4. What is the difference between time complexity and space complexity?
Time complexity measures how the running time of an algorithm grows relative to input size. Space complexity measures how the memory usage grows relative to input size, including any extra data structures the algorithm allocates (not counting the input itself, which is usually excluded from the count).
def sum_array(arr): # Time: O(n), Space: O(1)
total = 0
for num in arr: # one pass through n elements
total += num
return total # no extra space that scales with n
def reverse_array(arr): # Time: O(n), Space: O(n)
return [arr[i] for i in range(len(arr) - 1, -1, -1)]
# creates a NEW list of size n, this is the extra spaceA very common interview follow-up: "can you do this in O(1) space?" For the reverse example, yes, by reversing in place with two pointers swapping elements:
def reverse_in_place(arr): # Time: O(n), Space: O(1)
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1This time/space trade-off question comes up constantly: many problems can trade extra memory (a hash map, a visited set) for faster time, or vice versa, and naming that trade-off explicitly is a strong signal in interviews.
Q5. What is an abstract data type (ADT) and how does it differ from a data structure?
An abstract data type defines the behavior and operations a structure supports, without specifying how those operations are implemented. A data structure is the concrete implementation that fulfills that contract.
The classic example: a Stack is an ADT. It defines push, pop, and peek operations with LIFO (last-in-first-out) behavior. You can implement that Stack ADT using an array or a linked list, two completely different concrete data structures fulfilling the same abstract contract.
# The Stack ADT implemented with a Python list (dynamic array)
class ArrayStack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[-1]
# The same Stack ADT implemented with a linked list
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class LinkedListStack:
def __init__(self):
self.top = None
def push(self, item):
self.top = Node(item, self.top)
def pop(self):
value = self.top.value
self.top = self.top.next
return value
def peek(self):
return self.top.valueBoth classes satisfy the same Stack ADT contract with different underlying implementations and different trade-offs (array-backed stacks have better cache locality; linked-list-backed stacks avoid resizing costs).
Q6. What are the trade-offs between arrays and linked lists?
| Array | Linked List | |
|---|---|---|
| Access by index | O(1) | O(n), must traverse from head |
| Insert/delete at start | O(n), shift all elements | O(1) |
| Insert/delete at end | O(1) amortized (dynamic array) | O(1) if tail pointer kept |
| Insert/delete in middle | O(n), shift elements | O(n) to find, O(1) to insert |
| Memory layout | Contiguous | Scattered, extra pointer overhead |
| Cache performance | Good (contiguous memory) | Poor (pointer chasing) |
Arrays win when you need fast random access by index and the size is roughly known upfront. Linked lists win when you need frequent insertions/deletions at the beginning or in the middle, and don't need fast random access.
A common follow-up: "why are arrays generally faster in practice for most real-world code, even when Big-O analysis suggests linked lists should be competitive?" The answer is cache locality. Array elements sit in contiguous memory, so the CPU can prefetch them efficiently. Linked list nodes are scattered across memory (pointer chasing), causing cache misses that Big-O notation doesn't capture at all.
Q7. What is amortized time complexity?
Amortized complexity describes the average performance of an operation over a sequence of operations, even when some individual operations are much more expensive than others. It's the answer to "but doesn't resizing make array append slow sometimes?"
The classic example is dynamic array append (Python's list.append(), Java's ArrayList.add()). Most appends are O(1): there's room, you just place the new element. But occasionally, the array is full and must be resized, which means allocating a new larger array and copying every existing element, an O(n) operation.
# Dynamic array doubling strategy
# Capacity: 1 -> 2 -> 4 -> 8 -> 16 ...
# Most appends: O(1). Occasional resize: O(n).
# Averaged ("amortized") over many appends: O(1) per append.Because the array typically doubles in size when it resizes, expensive resize operations become exponentially rarer as the array grows. When you average the total cost of n appends across all n operations, it works out to O(1) per operation on average, even though some individual operations are O(n). This is what "amortized O(1)" means, and it's worth being able to explain the doubling-strategy reasoning, not just cite the term.
Q8. When would you choose a hash map over an array, and what's the catch?
Hash maps provide average O(1) lookup, insertion, and deletion by key, compared to O(n) for searching an unsorted array or O(log n) for a sorted array with binary search. Use a hash map whenever you need fast lookups by some key value rather than by position/index.
# O(n) lookup in array
def contains_array(arr, target):
return target in arr # must scan, worst case O(n)
# O(1) average lookup in hash set
def contains_hashset(items_set, target):
return target in items_set # average O(1) via hashingThe catch interviewers want you to name: hash map performance assumes a good hash function with minimal collisions. In the worst case (many keys hashing to the same bucket), operations degrade to O(n). Hash maps also use more memory per element than arrays (storing the hash table structure itself), and they don't preserve insertion order by default in many languages (though Python's dict and JavaScript's Map do preserve insertion order as of recent language versions, which is itself a common trivia question).
Category 2: Arrays and Strings (Q9-Q16)
Array and string problems are the bread and butter of coding interviews. These eight questions cover the most commonly asked problems and the patterns (hash map lookups, prefix/suffix, sliding window, two pointers) that solve the majority of array-based interview problems.
Q9. Two Sum: given an array and a target, find two numbers that add up to the target.
This is the single most commonly asked coding interview question across every company tier. The pattern: hash map for O(1) complement lookups.
def two_sum(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
# Example: two_sum([2, 7, 11, 15], 9) -> [0, 1] (2 + 7 = 9)Time: O(n). Space: O(n).
The brute-force O(n²) approach checks every pair with nested loops. The hash map approach is the expected "optimized" answer: as you iterate, you check whether the complement of the current number has already been seen, which turns the nested-loop search into a single pass with O(1) lookups.
Q10. Maximum Subarray (Kadane's Algorithm): find the contiguous subarray with the largest sum.
def max_subarray(nums):
max_sum = current_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
# Example: max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) -> 6 ([4, -1, 2, 1])Time: O(n). Space: O(1).
The key insight (Kadane's algorithm): at each position, decide whether extending the current subarray (current_sum + num) is better than starting fresh from the current element alone (num). If current_sum ever goes negative, starting fresh is always at least as good, so the running sum effectively "resets" itself through the max() comparison without needing an explicit reset condition.
Q11. Product of Array Except Self: return an array where each element is the product of all other elements, without using division.
def product_except_self(nums):
n = len(nums)
result = [1] * n
prefix = 1
for i in range(n):
result[i] = prefix
prefix *= nums[i]
suffix = 1
for i in range(n - 1, -1, -1):
result[i] *= suffix
suffix *= nums[i]
return result
# Example: product_except_self([1, 2, 3, 4]) -> [24, 12, 8, 6]Time: O(n). Space: O(1) extra (excluding the output array itself).
The pattern: prefix and suffix products. The first pass computes, for each index, the product of all elements to its LEFT. The second pass multiplies in the product of all elements to its RIGHT. Combined, each position ends up with the product of everything except itself, without ever dividing.
Q12. Merge Intervals: given a collection of intervals, merge all overlapping ones.
def merge_intervals(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0]) # sort by start time
merged = [intervals[0]]
for current in intervals[1:]:
last = merged[-1]
if current[0] <= last[1]: # overlaps with the last merged interval
last[1] = max(last[1], current[1])
else:
merged.append(current)
return merged
# Example: merge_intervals([[1,3],[2,6],[8,10],[15,18]]) -> [[1,6],[8,10],[15,18]]Time: O(n log n), dominated by the sort. Space: O(n) for the result.
The pattern: sort first, then a single linear pass. Sorting by start time guarantees that any interval that could possibly overlap with the current merged interval will be checked immediately next, since intervals further along in the sorted order start even later.
Q13. Longest Substring Without Repeating Characters.
def longest_unique_substring(s):
seen = set()
left = 0
max_length = 0
for right in range(len(s)):
while s[right] in seen:
seen.remove(s[left])
left += 1
seen.add(s[right])
max_length = max(max_length, right - left + 1)
return max_length
# Example: longest_unique_substring("abcabcbb") -> 3 ("abc")Time: O(n). Space: O(min(n, charset size)).
The pattern: sliding window. Expand the window by moving right forward. When a repeat is found, shrink the window from the left until the repeat is removed, then continue expanding. This avoids re-checking substrings from scratch, which is what makes the brute force O(n³) approach collapse down to O(n).
Q14. Valid Anagram: determine if two strings are anagrams of each other.
def is_anagram(s, t):
if len(s) != len(t):
return False
char_count = {}
for char in s:
char_count[char] = char_count.get(char, 0) + 1
for char in t:
if char not in char_count:
return False
char_count[char] -= 1
if char_count[char] == 0:
del char_count[char]
return len(char_count) == 0
# Example: is_anagram("anagram", "nagaram") -> TrueTime: O(n). Space: O(1) for fixed alphabet size (or O(k) for k distinct characters).
The pattern: frequency counting with a hash map. An alternative O(n log n) solution sorts both strings and compares them directly, which is simpler to write but asymptotically slower, worth mentioning to show you know the optimized approach exists.
Q15. Move Zeroes: move all zeroes to the end while maintaining relative order of non-zero elements, in place.
def move_zeroes(nums):
insert_pos = 0
for num in nums:
if num != 0:
nums[insert_pos] = num
insert_pos += 1
for i in range(insert_pos, len(nums)):
nums[i] = 0
# Example: nums = [0, 1, 0, 3, 12]; move_zeroes(nums) -> [1, 3, 12, 0, 0]Time: O(n). Space: O(1).
The pattern: two pointers, one tracking where the next non-zero element should be placed (insert_pos), one scanning through the array. This is a common variant of the broader "partition in place" pattern, the same underlying idea used in the Dutch national flag problem and certain quicksort partition steps.
Q16. Rotate Array: rotate an array to the right by k steps, in place.
def rotate(nums, k):
n = len(nums)
k = k % n # handle k larger than array length
def reverse(start, end):
while start < end:
nums[start], nums[end] = nums[end], nums[start]
start += 1
end -= 1
reverse(0, n - 1) # reverse the whole array
reverse(0, k - 1) # reverse the first k elements
reverse(k, n - 1) # reverse the remaining elements
# Example: nums = [1,2,3,4,5,6,7], k=3 -> [5,6,7,1,2,3,4]Time: O(n). Space: O(1).
The pattern: reverse-based rotation. Reversing the entire array, then reversing each of the two resulting segments separately, produces the rotated result without any extra array allocation. It's a favorite "do you know a clever trick beyond the obvious approach" follow-up after a candidate first proposes the simpler O(n) extra-space solution.
Category 3: Linked Lists, Stacks, and Queues (Q17-Q23)
Linked list problems test pointer manipulation skills, and stack/queue problems test whether you understand LIFO and FIFO behavior. The fast/slow pointer pattern introduced here shows up across multiple seemingly unrelated problems.
Q17. Reverse a Linked List.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_linked_list(head):
prev = None
current = head
while current:
next_node = current.next # save the next node before overwriting
current.next = prev # reverse the pointer
prev = current
current = next_node
return prev # prev is now the new headTime: O(n). Space: O(1).
This is one of the most fundamental linked list questions and a building block for many more complex linked list problems (reverse in groups of k, reverse a sublist, palindrome checks). The key insight: you need to save next before overwriting current.next, otherwise you lose your way forward through the list.
Q18. Detect a Cycle in a Linked List (Floyd's Cycle Detection).
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalseTime: O(n). Space: O(1).
The pattern: fast and slow pointers (also called the tortoise and hare algorithm). The slow pointer moves one step at a time, the fast pointer moves two steps. If there is a cycle, the fast pointer will eventually lap the slow pointer and they will meet inside the cycle. If there's no cycle, the fast pointer reaches the end (None) first.
This is a top-tier interview pattern specifically because it solves the problem in O(1) space, compared to the more obvious O(n) space solution using a hash set to track visited nodes. Naming both approaches and explaining why the pointer trick wins on space is a strong answer.
Q19. Merge Two Sorted Linked Lists.
def merge_two_lists(l1, l2):
dummy = ListNode()
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
current.next = l1 if l1 else l2 # attach whichever list has leftovers
return dummy.nextTime: O(n + m). Space: O(1) extra (reusing existing nodes).
The dummy node pattern shown here (a placeholder head node that gets discarded at the end via dummy.next) is a common technique that simplifies linked list code by avoiding special-case handling for the very first node.
Q20. Valid Parentheses: determine if a string of brackets is properly balanced.
def is_valid(s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
return len(stack) == 0
# Example: is_valid("({[]})") -> True
# Example: is_valid("(]") -> FalseTime: O(n). Space: O(n) for the stack in the worst case.
The pattern: stack-based matching. Every opening bracket gets pushed. Every closing bracket must match the most recently pushed opening bracket (LIFO order), which is exactly the behavior a stack provides natively. This pattern generalizes to any "matching pairs in nested order" problem.
Q21. Design an LRU (Least Recently Used) Cache.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
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.capacity:
self.cache.popitem(last=False) # evict the least recently usedTime: O(1) for both get and put. Space: O(capacity).
This is a frequently asked "design" question testing whether you know to combine a hash map (for O(1) key lookup) with a doubly linked list (for O(1) reordering to track recency), or know that your language's standard library already provides this combination (Python's OrderedDict). Many interviewers expect you to implement the doubly linked list + hash map combination manually, so be ready to explain (or implement) that version if asked to go deeper.
Q22. Implement a Queue using two Stacks.
class QueueUsingStacks:
def __init__(self):
self.stack1 = [] # for enqueue
self.stack2 = [] # for dequeue
def enqueue(self, x):
self.stack1.append(x)
def dequeue(self):
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop()) # reverse order
return self.stack2.pop()Time: O(1) amortized for both operations. Space: O(n).
This question tests understanding of how LIFO and FIFO behaviors relate. Moving every element from stack1 to stack2 reverses their order once, which converts the LIFO behavior of a single stack into the FIFO behavior a queue needs. The amortized analysis (Q7) applies directly here: most dequeues are O(1) because stack2 already has elements ready, with the occasional O(n) transfer when stack2 empties out.
Q23. Find the Middle of a Linked List in one pass.
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is now at the middleTime: O(n). Space: O(1).
This reuses the same fast/slow pointer pattern from cycle detection (Q18). The fast pointer moves twice as fast as the slow pointer, so by the time the fast pointer reaches the end, the slow pointer has covered exactly half the distance. This is a strong example of how learning one pattern (fast/slow pointers) unlocks multiple seemingly unrelated problems.
Category 4: Trees and Graphs (Q24-Q32)
Tree and graph problems test recursive thinking and traversal strategy selection. Recognizing that a 2D grid problem is secretly a graph connectivity problem (Q28) or that a dependency problem is secretly cycle detection (Q29) is the kind of pattern recognition that separates strong candidates.
Q24. What are the three types of binary tree traversal and how do they differ?
In-order visits left subtree, then the node, then right subtree. For a BST, in-order visits nodes in sorted ascending order.
Pre-order visits the node first, then left subtree, then right subtree. Useful for creating a copy of a tree.
Post-order visits left subtree, then right subtree, then the node. Useful for deleting a tree safely (children before parent).
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
def preorder(root):
if not root:
return []
return [root.val] + preorder(root.left) + preorder(root.right)
def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]All three run in O(n) time and O(h) space for the recursion stack, where h is the tree height (O(log n) for a balanced tree, O(n) for a completely skewed tree, which is itself a common follow-up about worst-case space).
Q25. What is the difference between BFS and DFS, and when do you use each?
Breadth-First Search (BFS) explores all neighbors of a node before moving to the next level, using a queue. It explores level by level, outward from the starting point.
Depth-First Search (DFS) explores as far as possible down one branch before backtracking, using a stack (or recursion, which uses the call stack implicitly).
from collections import deque
def bfs(root):
if not root:
return []
result, queue = [], deque([root])
while queue:
node = queue.popleft()
result.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
return result
def dfs(root):
if not root:
return []
result, stack = [], [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
return resultUse BFS when you need the shortest path in an unweighted graph, or level-by-level processing (binary tree level order traversal, finding the minimum number of steps to reach a target).
Use DFS when you need to explore all possible paths, detect cycles, solve connectivity problems, or when the problem is naturally recursive (tree traversals, backtracking problems).
Q26. Validate a Binary Search Tree.
def is_valid_bst(root, lower=float('-inf'), upper=float('inf')):
if not root:
return True
if not (lower < root.val < upper):
return False
return (is_valid_bst(root.left, lower, root.val) and
is_valid_bst(root.right, root.val, upper))Time: O(n). Space: O(h) for the recursion stack.
The common mistake candidates make: checking only that <code>root.left.val < root.val < root.right.val</code> at each node locally, without tracking the valid RANGE inherited from ancestors. A node deep in the left subtree must be less than EVERY ancestor it's a left descendant of, not just its immediate parent. The recursive range-passing approach shown here handles this correctly.
Q27. Find the Lowest Common Ancestor of two nodes in a Binary Tree.
def lowest_common_ancestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root # p and q found in different subtrees, root is the LCA
return left if left else rightTime: O(n). Space: O(h).
The pattern: post-order recursion that "bubbles up" findings. Each recursive call returns either None (neither target found), one of the target nodes (found exactly one), or the LCA itself (both targets found in different branches below). The genuinely clever part: if a node's left and right recursive calls both return non-None, that node IS the LCA.
Q28. Number of Islands: count the number of connected groups of land cells in a grid.
def num_islands(grid):
if not grid:
return 0
def dfs(i, j):
if (i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0])
or grid[i][j] == '0'):
return
grid[i][j] = '0' # mark as visited by sinking the island cell
dfs(i + 1, j)
dfs(i - 1, j)
dfs(i, j + 1)
dfs(i, j - 1)
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
count += 1
dfs(i, j) # sink the entire connected island
return countTime: O(rows cols). Space: O(rows cols) worst case for the recursion stack.
This is the canonical "grid as implicit graph" problem. Each cell is a node, and adjacency is implicit (up/down/left/right neighbors) rather than an explicit adjacency list. Recognizing that a 2D grid traversal problem is secretly a graph connectivity problem is a key pattern-recognition skill this question specifically tests.
Q29. Course Schedule: given course prerequisites, determine if it's possible to finish all courses.
def can_finish(num_courses, prerequisites):
graph = {i: [] for i in range(num_courses)}
for course, prereq in prerequisites:
graph[course].append(prereq)
visiting, visited = set(), set()
def has_cycle(course):
if course in visiting:
return True # found a cycle
if course in visited:
return False # already fully processed, no cycle
visiting.add(course)
for prereq in graph[course]:
if has_cycle(prereq):
return True
visiting.remove(course)
visited.add(course)
return False
return not any(has_cycle(course) for course in range(num_courses))Time: O(V + E). Space: O(V + E) for the graph plus O(V) for the recursion stack.
This is the classic real-world application of topological sort and cycle detection in a directed graph: if course A requires course B, and course B (eventually) requires course A, the schedule is impossible because of the circular dependency. The visiting set (currently in the recursion path) versus visited set (fully processed, confirmed cycle-free) distinction is the key technique for detecting cycles in directed graphs specifically.
Q30. Maximum Depth of a Binary Tree.
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))Time: O(n). Space: O(h).
This looks trivial, and it largely is, but it's frequently asked as a warm-up question to establish baseline recursive thinking. The recursive structure here (base case: empty tree has depth 0; recursive case: 1 plus the deeper of the two child subtrees) is the template that nearly every tree depth/height/balance problem builds on.
Q31. Determine if two Binary Trees are the same (identical structure and values).
def is_same_tree(p, q):
if not p and not q:
return True
if not p or not q:
return False
if p.val != q.val:
return False
return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right)Time: O(n). Space: O(h).
Three base cases to get right: both null (match), exactly one null (no match), and value mismatch (no match). Many candidates forget the "exactly one is null" case and write code that crashes with a null pointer exception, which is itself a useful thing to watch for during an interview.
Q32. What is the difference between an adjacency list and an adjacency matrix for representing a graph?
# Adjacency matrix for a 4-node graph
graph_matrix = [
[0, 1, 1, 0],
[1, 0, 0, 1],
[1, 0, 0, 1],
[0, 1, 1, 0],
]
# Adjacency list for the same graph
graph_list = {
0: [1, 2],
1: [0, 3],
2: [0, 3],
3: [1, 2],
}| Adjacency Matrix | Adjacency List | |
|---|---|---|
| Space | O(V²) | O(V + E) |
| Check if edge exists | O(1) | O(degree of node) |
| Iterate neighbors | O(V) | O(degree of node) |
| Best for | Dense graphs | Sparse graphs (most real-world graphs) |
Adjacency lists are the default choice for most real-world graphs, which tend to be sparse (relatively few edges compared to V² possible edges). Adjacency matrices make sense when the graph is dense or when O(1) edge-existence checks matter more than memory efficiency.
Category 5: Coding Patterns Reference (Q33-Q40)
This is the highest-leverage section of the entire guide. Learning these 8 patterns covers the vast majority of coding interview problems. Each entry describes the pattern, names the signals that indicate when to use it, and points back to the concrete problem earlier in this guide that demonstrates it.
Q33. What is the Two Pointers pattern and what signals indicate you should use it?
Use two indices that move through a structure based on specific conditions, typically reducing an O(n²) brute-force approach to O(n).
Signals: sorted arrays, finding pairs that satisfy a condition, palindrome checks, or removing duplicates in place.
def two_sum_sorted(arr, target): # arr is SORTED
left, right = 0, len(arr) - 1
while left < right:
current = arr[left] + arr[right]
if current == target:
return [left, right]
elif current < target:
left += 1 # need a bigger sum, move left pointer up
else:
right -= 1 # need a smaller sum, move right pointer down
return []This is distinct from the hash-map Two Sum (Q9), which works on unsorted arrays. When the array is already sorted, two pointers achieves the same O(n) time with O(1) space instead of O(n) space, which is a common "can you do it without extra space" follow-up.
Q34. What is the Sliding Window pattern and what signals indicate you should use it?
Maintain a "window" (a contiguous range defined by two pointers) over an array or string, expanding or shrinking it based on problem conditions, rather than recomputing from scratch for every possible subarray.
Signals: contiguous subarrays/substrings, problems mentioning "consecutive elements," finding the longest/shortest substring or subarray satisfying a condition.
Already demonstrated in Q13 (Longest Substring Without Repeating Characters). The pattern avoids the brute-force approach of checking every possible window from scratch (typically O(n²) or O(n³)), instead reusing work as the window slides, getting down to O(n).
Q35. What is the Fast and Slow Pointers pattern and what signals indicate you should use it?
Use two pointers moving at different speeds through a structure (usually a linked list), commonly one step versus two steps per iteration.
Signals: cycle detection, finding the middle element, detecting palindromic structure in a linked list.
Already demonstrated in Q18 (cycle detection) and Q23 (finding the middle). The key insight that unifies both use cases: the speed difference between the two pointers creates a predictable mathematical relationship (the fast pointer covers exactly twice the distance) that you can exploit without needing extra memory to track positions explicitly.
Q36. What is the Backtracking pattern and what signals indicate you should use it?
Explore all possible solutions by making a choice, recursing into the consequences of that choice, and undoing the choice (backtracking) if it doesn't lead anywhere, to try the next option.
Signals: "generate all combinations/permutations," "find all possible ways to," constraint satisfaction problems (N-Queens, Sudoku), or exhaustive search where you need every valid solution.
def subsets(nums):
result = []
def backtrack(start, current):
result.append(current[:]) # record a copy of the current combination
for i in range(start, len(nums)):
current.append(nums[i]) # make a choice
backtrack(i + 1, current) # recurse
current.pop() # undo the choice (backtrack)
backtrack(0, [])
return result
# Example: subsets([1, 2, 3]) -> [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]Time: O(2^n) for subsets specifically. Space: O(n) for the recursion depth.
The append-recurse-pop sequence is the universal backtracking template. The pop() after the recursive call is what makes it backtracking rather than just plain recursion: it explicitly undoes the choice before trying the next option in the loop.
Q37. What are common variants of Binary Search beyond the basic version?
Beyond standard binary search (Q3), several common variants come up specifically because they require adapting the basic template rather than applying it directly. Search in a rotated sorted array:
def search_rotated(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]: # left half is sorted
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else: # right half is sorted
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1Time: O(log n). The key insight: at each midpoint, one half of the array is guaranteed to be properly sorted (even though the whole array isn't). Determine which half is sorted, then decide whether the target could lie in that sorted half or must be in the other half.
The unifying signal across all binary search variants: any time you can eliminate half the remaining search space based on a comparison, binary search (or a variant of it) is probably the right tool, even when the input isn't obviously "search for X in a sorted array."
Q38. What is Topological Sort and what signals indicate you should use it?
Produce a linear ordering of nodes in a directed acyclic graph (DAG) such that for every directed edge from A to B, A comes before B in the ordering.
Signals: dependency ordering, task scheduling, build systems, course prerequisites (already shown in Q29), or any problem describing a DAG ordering constraint.
from collections import deque
def topological_sort(num_nodes, edges):
graph = {i: [] for i in range(num_nodes)}
in_degree = {i: 0 for i in range(num_nodes)}
for src, dst in edges:
graph[src].append(dst)
in_degree[dst] += 1
queue = deque([node for node in range(num_nodes) if in_degree[node] == 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 if len(result) == num_nodes else [] # empty if a cycle existsTime: O(V + E). Space: O(V + E).
This uses Kahn's algorithm (BFS-based): start with nodes that have no incoming edges (in-degree 0), process them, decrement the in-degree of their neighbors, and add any neighbor that reaches in-degree 0 to the queue. If the final result doesn't include every node, a cycle exists.
Q39. What is a Monotonic Stack and what signals indicate you should use it?
A monotonic stack maintains elements in either strictly increasing or strictly decreasing order, popping elements that violate the monotonic property before pushing a new element.
Signals: "next greater element," "daily temperatures," problems asking for the nearest element satisfying a comparison in a specific direction.
def next_greater_element(nums):
result = [-1] * len(nums)
stack = [] # stores indices, maintained in decreasing value order
for i, num in enumerate(nums):
while stack and nums[stack[-1]] < num:
result[stack.pop()] = num # found the "next greater" for this index
stack.append(i)
return result
# Example: next_greater_element([2, 1, 2, 4, 3]) -> [4, 2, 4, -1, -1]Time: O(n). Space: O(n).
The naive brute force is O(n²): for each element, scan forward until you find something bigger. The monotonic stack gets this down to O(n) because each element is pushed and popped at most once.
Q40. What is Union-Find (Disjoint Set Union) and what signals indicate you should use it?
Union-Find efficiently tracks which elements belong to which connected group (set) and supports two operations: union (merge two groups) and find (determine which group an element belongs to), both close to O(1) with path compression.
Signals: counting connected components, detecting cycles in an undirected graph, problems about grouping/merging entities based on relationships (friend circles, network connectivity).
class UnionFind:
def __init__(self, n):
self.parent = list(range(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):
root_x, root_y = self.find(x), self.find(y)
if root_x != root_y:
self.parent[root_x] = root_y
return True # a union happened (different groups)
return False # already in the same groupTime: nearly O(1) amortized per operation with path compression. Space: O(n).
Union-Find is often the cleanest solution for "are these two things connected" problems, frequently outperforming repeated BFS/DFS when you need to answer many connectivity queries interspersed with updates to the graph structure over time.
Category 6: Dynamic Programming and Greedy (Q41-Q47)
Dynamic programming is where mid-level and senior candidates get separated. The key is recognizing when a problem has optimal substructure and overlapping subproblems, then deciding between top-down (memoization) and bottom-up (tabulation) approaches.
Q41. What is Dynamic Programming and when should you reach for it?
Dynamic Programming (DP) solves complex problems by breaking them into smaller overlapping subproblems, solving each subproblem once, and storing (memoizing) the result to avoid redundant recomputation.
Two conditions signal a DP problem: optimal substructure (the optimal solution can be built from optimal solutions to its subproblems) and overlapping subproblems (the same subproblems get solved repeatedly in a naive recursive approach).
# Naive recursive Fibonacci: O(2^n), exponential
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
# DP with memoization: O(n), each value computed exactly once
def fib_memo(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]Signals in a problem statement: "find the maximum/minimum," "count the number of ways," or "is it possible to," combined with a sense that smaller versions of the same problem feed into the answer for larger versions.
Q42. Climbing Stairs: count the number of distinct ways to climb n stairs, taking 1 or 2 steps at a time.
def climb_stairs(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
# Example: climb_stairs(5) -> 8Time: O(n). Space: O(1).
This is structurally identical to Fibonacci (Q41): the number of ways to reach step n equals the ways to reach step n-1 plus the ways to reach step n-2. Recognizing "this is secretly Fibonacci" is itself a useful pattern-matching skill.
Q43. Coin Change: find the minimum number of coins needed to make a given amount.
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # base case: 0 coins needed to make amount 0
for current_amount in range(1, amount + 1):
for coin in coins:
if coin <= current_amount:
dp[current_amount] = min(dp[current_amount],
dp[current_amount - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# Example: coin_change([1, 2, 5], 11) -> 3 (5 + 5 + 1)Time: O(amount * len(coins)). Space: O(amount).
The DP array dp[i] stores the minimum coins needed to make amount i. For each amount, try every coin denomination that fits, and take the best result from "1 coin plus however many coins were needed for the remainder." This bottom-up (tabulation) style fills the array from smaller amounts up to the target.
Q44. Longest Common Subsequence (LCS) between two strings.
def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
# Example: longest_common_subsequence("abcde", "ace") -> 3 ("ace")Time: O(m n). Space: O(m n).
This is the canonical 2D DP problem. dp[i][j] represents the LCS length considering the first i characters of text1 and the first j characters of text2. If the characters match, extend by 1. If they don't, take the best achievable by dropping one character from either string. This same 2D DP table structure underlies many string-comparison problems (edit distance, longest common substring).
Q45. 0/1 Knapsack: maximize value within a weight capacity, where each item can be used at most once.
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(
dp[i - 1][w], # don't take item i-1
dp[i - 1][w - weights[i - 1]] + values[i - 1] # take it
)
else:
dp[i][w] = dp[i - 1][w] # can't fit, skip it
return dp[n][capacity]
# Example: knapsack([1,3,4,5], [1,4,5,7], 7) -> 9Time: O(n capacity). Space: O(n capacity).
For each item, you choose to either include it (and gain its value while using up its weight) or exclude it (keeping the previous best for the same capacity). This is the foundational pattern behind a huge family of "subset selection with a constraint" problems.
Q46. House Robber: maximize the sum of non-adjacent elements in an array.
def rob(nums):
if not nums:
return 0
prev2, prev1 = 0, 0
for num in nums:
current = max(prev1, prev2 + num) # skip this house, or rob it
prev2, prev1 = prev1, current
return prev1
# Example: rob([2, 7, 9, 3, 1]) -> 12 (2 + 9 + 1)Time: O(n). Space: O(1).
At each house, you decide: skip it (carry forward the best result so far, prev1) or rob it (take its value plus the best result from two houses back, prev2 + num, since you can't rob adjacent houses). This is a simpler relative of Knapsack (Q45) with an implicit "adjacency" constraint instead of a weight constraint.
Q47. When does a Greedy approach fail where Dynamic Programming succeeds?
A greedy algorithm makes the locally optimal choice at each step, hoping it leads to a globally optimal solution. This works for some problems (like coin change with standard US denominations: 1, 5, 10, 25) but fails for others where local optimality doesn't guarantee global optimality.
The classic counter-example: Coin Change (Q43) with coins [1, 3, 4] and target 6. A greedy approach takes the largest coin first: 4, leaving 2, then two 1-coins, for a total of 3 coins (4+1+1). But the optimal answer is 2 coins (3+3).
# Greedy approach: WRONG for non-standard coin sets
def greedy_coin_change(coins, amount):
coins.sort(reverse=True)
count = 0
for coin in coins:
count += amount // coin
amount %= coin
return count if amount == 0 else -1
# greedy_coin_change([1, 3, 4], 6) -> 3 (WRONG, optimal is 2)
# DP approach from Q43 correctly returns 2Greedy is faster (usually O(n) or O(n log n)) and simpler when it works, but it only works when the problem has the "greedy choice property," where a locally optimal choice never prevents reaching the globally optimal solution. DP is the safer, more broadly correct tool when you're not certain that property holds, at the cost of higher time/space complexity. Being able to identify specifically WHY greedy fails on a counter-example is what separates a strong answer here.
Category 7: Sorting, Searching, and Wrap-Up (Q48-Q50)
This final category covers sorting algorithm trade-offs, advanced binary search variants, and the meta-question about what mistakes to avoid during the interview itself.
Q48. Compare common sorting algorithms and when you'd choose each.
| Algorithm | Time (average) | Time (worst) | Space | Stable? | Notes |
|---|---|---|---|---|---|
| Bubble Sort | O(n²) | O(n²) | O(1) | Yes | Rarely used in practice, mainly educational |
| Insertion Sort | O(n²) | O(n²) | O(1) | Yes | Fast for small or nearly-sorted arrays |
| Merge Sort | O(n log n) | O(n log n) | O(n) | Yes | Predictable, good for linked lists |
| Quick Sort | O(n log n) | O(n²) | O(log n) | No | Fast in practice, worst case rare with good pivot |
| Heap Sort | O(n log n) | O(n log n) | O(1) | No | In-place, but worse cache performance |
Most languages' built-in sort functions use a hybrid approach (Python's Timsort, a hybrid of merge sort and insertion sort). "Stable" matters when you need to preserve the relative order of equal elements, which comes up in multi-key sorting.
A common follow-up: "why does quicksort have a worst case of O(n²) despite being fast in practice?" Answer: a poor pivot choice (always picking the first or last element on an already-sorted array) degenerates into O(n²). Randomized pivot selection or median-of-three pivot selection mitigates this in practice.
Q49. What are the variants of Binary Search you should know beyond finding an exact match?
Building on Q37, two other binary search variants come up consistently. Finding the first/last occurrence in a sorted array with duplicates:
def find_first_occurrence(arr, target):
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
result = mid
right = mid - 1 # keep searching LEFT for an earlier occurrence
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return resultBinary search on the answer space (rather than searching an actual array): used for problems like "find the minimum capacity needed" or "find the square root," where you binary search over a RANGE OF POSSIBLE ANSWERS, checking at each midpoint whether that candidate answer satisfies the problem's constraints.
Recognizing when a problem is "secretly" a binary search on the answer space, rather than a search through an explicit array, is a more advanced pattern-recognition skill that separates strong mid-level candidates.
Q50. What are the most common mistakes candidates make in DSA interviews?
- Jumping straight to code without clarifying the problem. Strong candidates ask about edge cases (empty input, duplicates, negative numbers), confirm input constraints, and restate the problem in their own words before writing a single line.
- Defaulting to brute force and stopping there. A correct O(n²) solution when an O(n) or O(n log n) solution exists is usually treated as an incomplete answer. Always verbally flag the brute-force complexity, then work toward an optimized approach.
- Not stating time/space complexity explicitly. Interviewers expect you to state it unprompted as part of explaining your solution. Silence on this point reads as a gap in fundamentals.
- Not testing the solution against edge cases out loud. Walking through your own code with an empty input, a single-element input, or all-duplicates, and catching your own bugs before the interviewer does, is one of the strongest positive signals you can send.
- Memorizing solutions instead of understanding patterns. A candidate who can't adapt the Two Sum solution when the interviewer changes the problem slightly (three numbers instead of two, or a sorted array) reveals that the underlying pattern was never actually understood.
Quick Reference: All 50 Questions at a Glance
| # | Question | Core Concept |
|---|---|---|
| Q1 | What is a data structure | Organization for efficient access/modification |
| Q2 | Linear vs non-linear data structures | Sequential vs hierarchical/network |
| Q3 | Big-O notation | Growth rate, worst-case, O(1) through O(n!) |
| Q4 | Time vs space complexity | Running time growth vs memory growth |
| Q5 | ADT vs data structure | Contract (Stack) vs implementation (array/linked list) |
| Q6 | Arrays vs linked lists | O(1) access vs O(1) insert/delete, cache locality |
| Q7 | Amortized time complexity | Dynamic array doubling, average over many ops |
| Q8 | Hash map vs array | O(1) average lookup vs collision worst case |
| Q9 | Two Sum | Hash map complement lookup, O(n) |
| Q10 | Maximum Subarray (Kadane's) | Running sum reset via max(), O(n) |
| Q11 | Product of Array Except Self | Prefix and suffix products, O(n) |
| Q12 | Merge Intervals | Sort by start, single pass merge, O(n log n) |
| Q13 | Longest Substring Without Repeating | Sliding window with seen set, O(n) |
| Q14 | Valid Anagram | Frequency counting hash map, O(n) |
| Q15 | Move Zeroes | Two pointers in-place partition, O(n) |
| Q16 | Rotate Array | Reverse-based rotation trick, O(n) O(1) space |
| Q17 | Reverse a Linked List | Save next before overwrite, O(n) |
| Q18 | Detect a Cycle (Floyd's) | Fast/slow pointers, O(n) O(1) space |
| Q19 | Merge Two Sorted Lists | Dummy node technique, O(n+m) |
| Q20 | Valid Parentheses | Stack-based bracket matching, O(n) |
| Q21 | LRU Cache design | Hash map + doubly linked list, O(1) ops |
| Q22 | Queue using two Stacks | Reverse order via stack transfer, amortized O(1) |
| Q23 | Find Middle of Linked List | Fast/slow pointers, O(n) O(1) space |
| Q24 | Binary tree traversals | In-order, pre-order, post-order |
| Q25 | BFS vs DFS | Queue level-by-level vs stack/recursion depth-first |
| Q26 | Validate a BST | Range-bound recursion, not just local comparison |
| Q27 | Lowest Common Ancestor | Post-order bubble-up recursion |
| Q28 | Number of Islands | Grid as implicit graph, DFS sinking |
| Q29 | Course Schedule | Directed graph cycle detection, visiting/visited sets |
| Q30 | Maximum Depth of Binary Tree | Base recursive template for tree height |
| Q31 | Same Tree | Three base cases: both null, one null, value mismatch |
| Q32 | Adjacency list vs matrix | Sparse vs dense graph representation |
| Q33 | Two Pointers pattern | Sorted arrays, pairs, O(n²) to O(n) |
| Q34 | Sliding Window pattern | Contiguous subarrays/substrings |
| Q35 | Fast and Slow Pointers | Cycle detection, finding middle |
| Q36 | Backtracking pattern | Append-recurse-pop template, exhaustive search |
| Q37 | Binary Search variants | Rotated array, sorted-half detection |
| Q38 | Topological Sort | Kahn's algorithm, dependency ordering |
| Q39 | Monotonic Stack | Next greater/smaller element, O(n²) to O(n) |
| Q40 | Union-Find | Connected components, near-O(1) path compression |
| Q41 | What is DP | Optimal substructure, overlapping subproblems |
| Q42 | Climbing Stairs | Fibonacci-structured intro DP |
| Q43 | Coin Change | Bottom-up tabulation, minimum coins |
| Q44 | Longest Common Subsequence | 2D DP table, string comparison foundation |
| Q45 | 0/1 Knapsack | Include/exclude decision per item |
| Q46 | House Robber | Adjacency-constrained DP, Knapsack relative |
| Q47 | When greedy fails vs DP | Coin [1,3,4] counter-example |
| Q48 | Sorting algorithm comparison | Time/space/stability trade-offs |
| Q49 | Binary search beyond exact match | First/last occurrence, answer space search |
| Q50 | Common DSA interview mistakes | Clarify first, state complexity, test edges |
Frequently Asked Questions
How many LeetCode problems do I need to solve to be interview-ready?
You don't need hundreds. Solving 50 to 75 well-chosen problems that cover the 8 core patterns (two pointers, sliding window, fast/slow pointers, backtracking, binary search variants, topological sort, monotonic stack, union-find) is more effective than grinding 300 random problems. The goal is pattern recognition, not memorization. If you can identify which pattern a new problem maps to within the first few minutes, you're interview-ready.
Which programming language should I use for DSA interviews?
Use whichever language you're most fluent in, unless the job posting specifies one. Python is the most popular choice for DSA interviews because its syntax is concise and its built-in data structures (lists, dicts, sets, deque, heapq) map directly to the abstractions you need. Java is common in enterprise environments and has strong typing that prevents certain classes of bugs. JavaScript works fine but has fewer built-in data structures (no built-in heap, for instance).
The language matters less than fluency. Struggling with syntax while explaining an algorithm is a worse signal than using a "less optimal" language confidently.
How do DSA interviews differ from system design interviews?
DSA interviews test algorithmic problem-solving: given a specific input, produce a specific output as efficiently as possible. The constraint is correctness and time/space complexity. System design interviews test architectural judgment: design a system that handles millions of users, explain the trade-offs between different databases, or describe how you'd scale a specific feature. The constraint is scalability, reliability, and maintainability.
Most companies at mid-level and above run both types. This guide covers DSA. For complementary preparation, see our SQL interview questions for database fundamentals and our OOP interview questions for object-oriented design, which bridges the gap between DSA and system design.
Which coding patterns are most important to learn first?
Start with these three, which cover the highest percentage of interview problems:
- Hash map lookups (Q9, Q14): the single most useful technique across all problem types. If your first instinct is nested loops, ask yourself if a hash map can eliminate the inner loop.
- Two pointers (Q15, Q33): essential for sorted array problems and in-place modifications.
- BFS/DFS (Q25, Q28, Q29): covers tree traversals, graph problems, and grid-based problems in one pattern family.
After those, add sliding window (Q34), backtracking (Q36), and basic dynamic programming (Q41-Q43). These six patterns together cover the vast majority of problems you'll encounter in interviews.
How should I approach a DSA problem I've never seen before?
Follow this sequence: (1) Clarify the problem by restating it in your own words and asking about edge cases and constraints. (2) Work through 1-2 small examples by hand to build intuition. (3) Identify which pattern the problem maps to by looking at the signals described in Q33-Q40. (4) Start with a brute-force approach, state its complexity, then optimize. (5) Code the solution, talking through your decisions as you go. (6) Test with edge cases (empty input, single element, all duplicates) before declaring you're done.
The biggest mistake is skipping steps 1-3 and jumping straight to code. The few minutes spent clarifying and identifying the pattern almost always save time overall.
Dynamic programming feels impossible. How do I get better at it?
Start with the three simplest DP problems in order: Climbing Stairs (Q42, which is just Fibonacci), House Robber (Q46, which adds one constraint to Fibonacci), and Coin Change (Q43, which generalizes to multiple choices per step). These three problems share the same recursive structure, and seeing that structure repeat across different problem statements is the key insight that makes DP click.
For each problem, write the naive recursive solution first, then identify which subproblems get recomputed, then add memoization, then convert to bottom-up tabulation. Going through all four stages (naive, identify overlap, memoize, tabulate) builds the intuition that jumping straight to the DP table never does.
Related Articles
45 OOP Interview Questions and Answers (2026)
45 OOP interview questions covering the four pillars, SOLID principles, and design patterns with real code in Java, Python, JavaScript, and C#. Updated for 2026
40 SQL Interview Questions and Answers (2026)
40 SQL and relational database interview questions covering joins, indexes, ACID, window functions, CTEs, MySQL vs PostgreSQL, and query challenges.
30 Node.js Interview Questions and Answers (2026)
30 Node.js interview questions with full answers: event loop, streams, clustering, worker threads, memory leaks, and security. Updated for 2026.