Data structures & algorithms
Still the gate at large companies.
Data structures are the standard ways of arranging data in memory, such as a list, a lookup table or a tree, and each makes some operations cheap and others expensive. Algorithms are the standard recipes for working through them. Big-O notation is the shorthand for how the cost grows as the data grows: a method that is fine on a hundred records can be unusable on a million, and this notation tells you which one you have before you find out in production.
The everyday payoff is modest but real. Knowing that a lookup table finds an item immediately where scanning a list does not is the difference between a job that finishes and a job that runs all night. The larger reason is the hiring process: large companies and infrastructure teams still gate on a timed algorithm round, and no amount of shipped work gets you past a failed one.
It builds on Python and sits apart from the rest of this atlas; it is not what the daily work consists of. Its relevance is a question about your target companies rather than about the field, so decide where you are applying before deciding how many hours to spend here.
The whole practical surface is about a dozen recurring patterns, and whether you need them depends on where you apply.
In more detailHash maps, two pointers, sliding windows, binary search, heaps, intervals, tree and graph traversal, and one-dimensional dynamic programming. That list is the whole practical surface. Evidence from 2026 loops suggests algorithm rounds survive at large companies and infrastructure roles and disappear at product-AI companies where agents are the product.
Contested
Whether DSA is still required for AI roles is genuinely company-dependent. It survives at big tech and infra roles; it is often skipped at AI-native product companies.
Learn first
Working out whether you need this
By the end: You know whether to spend 40 hours here, based on where you are applying.
- Where algorithm rounds survive and where they do not
- The pattern list that covers almost everything
- What is not worth learning
- How to decide for your own target list
Before committing forty hours, work out whether you need them. Algorithm rounds have not disappeared, but they have not stayed everywhere either, and which you face depends on the specific companies you are applying to. This section lays out that split, names the short pattern list that covers almost everything still asked, and says plainly which classic topics you can skip. Read it and decide; skipping this track deliberately is a legitimate outcome.
It depends on the company, not the year#
Whether you face an algorithm round is not a function of how modern the role is. Reported 2026 loops suggest a fairly clear split: algorithm rounds survive at large companies and at platform or infrastructure roles, and largely disappear at product-focused AI companies where agents and retrieval are the product.
Still asking algorithm questions:
large technology companies
AI infrastructure and platform roles
established enterprises hiring AI engineers
Often not:
AI-native product companies
forward deployed and applied roles
small teams hiring for shipping speedDecide your target list first
This is 40 hours. If every company on your list runs a practical build round instead, that is 40 hours better spent on a portfolio project. If two of them run classic algorithm screens, it is 40 hours that decides whether you get past the first stage. Work out which you are in before starting.
The whole practical surface#
Interview algorithm questions draw from a much smaller set than the field contains. Learn these eleven patterns properly and you can approach almost anything you will be asked.
- 1.Hash maps and arrays
- 2.Two pointers
- 3.Sliding window
- 4.Binary search, including on the answer space
- 5.Stacks, including monotonic stacks
- 6.Heaps and top-k
- 7.Intervals
- 8.Trees: breadth-first and depth-first
- 9.Graphs: traversal and topological sort
- 10.Recursion and backtracking
- 11.One-dimensional dynamic programming
What to skip#
- Two-dimensional and bitmask dynamic programming. Rare, and hard enough to consume disproportionate time.
- Segment trees, Fenwick trees, union-find beyond the basic idea.
- Maximum flow, matching, advanced graph algorithms.
- Competitive programming techniques generally. Different game, different rules.
The temptation is to keep learning further patterns because each feels like progress. Depth on the eleven beats breadth beyond them, because the eleven actually recur.
Where people go wrong
Grinding hundreds of problems without checking whether the companies you want even ask. It is the most commonly over-invested area in this entire field.
Build this: Take your target list and find out, for each company, what the coding round actually is. Ask the recruiter; they will usually tell you. Then decide how much of this topic you need.
Open this section on its own pageThe high-frequency patterns
By the end: You can recognise and solve the patterns that appear most often.
- Hash maps: the answer to a surprising number of questions
- Two pointers and sliding window
- Binary search, including on the answer
- Stacks and monotonic stacks
Interview problems are drawn from a surprisingly short list of shapes, and most of the difficulty is recognising which shape you have been handed. This section covers the four that appear most often, and for each one gives the phrase in the question that gives it away, a template short enough to write from memory, the cost, and the mistake that gets made under pressure. Learn the signals and a large share of problems stop being puzzles and become recall.
Phrase in the question Reach for
"how many", "count", "duplicate", "anagram" hash map
"pair that sums", "sorted array", "palindrome" two pointers
"substring", "subarray", "window", "longest" sliding window
"sorted", "minimum value that works", "k-th" binary search
"next greater", "previous smaller", "nesting" stack
"top k", "k largest", "stream" heapHash maps#
A hash map turns a scan into a lookup. Anything you would otherwise find by looking through the data again — has this appeared before, how many times, what else shares this property — costs one operation instead of a pass, which takes a quadratic solution down to linear.
The signal is any question about finding, counting, grouping or duplicates. The move that generalises is check before you insert: look for what you need, then add yourself, so you never match against yourself.
# Two sum: for each number, has its complement already been seen?
def two_sum(nums, target):
seen = {} # value -> index
for i, n in enumerate(nums):
if target - n in seen: # check first
return [seen[target - n], i]
seen[n] = i # then insert
return []
# O(n) time, O(n) space. The brute force is two nested loops, O(n^2).Two standard library types cover most of the rest. Counter tallies in one line and answers most-common questions directly. defaultdict lets you group without checking whether the key exists yet, which removes the line most people forget.
from collections import Counter, defaultdict
Counter(words).most_common(3) # [('the', 12), ('a', 9), ('of', 7)]
# Group anagrams: the sorted letters are the identity of the group.
groups = defaultdict(list)
for word in words:
groups["".join(sorted(word))].append(word)
list(groups.values()) # [['eat','tea','ate'], ['tan','nat']]The dictionary key has to be hashable
A list cannot be a key and a tuple can, which is why grouping code converts one to the other. If you write groups[sorted(word)] it raises TypeError: unhashable type. Under time pressure this is the error people stare at longest, so recognise it on sight.
Two pointers#
Two indices walking through a sequence, usually inward from both ends. It works when the data is sorted or symmetric, because then the comparison at the current pair tells you which end to move: too small means raise the left, too large means lower the right. That decision is what removes the second loop.
The signal is a sorted array, a pair or triple that must satisfy something, or a question about reading from both directions.
def two_sum_sorted(nums, target):
"""The sorted version. No extra memory, unlike the hash map above."""
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return [left, right]
if total < target:
left += 1 # only the left end can raise the sum
else:
right -= 1 # only the right end can lower it
return []
# O(n) time, O(1) space. This is the template; most variants change the
# comparison, not the shape.The fast-and-slow variant uses the same two indices at different speeds instead of from opposite ends. It detects a cycle in a linked list, finds the middle in one pass, and removes the n-th node from the end without knowing the length.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: # identity, not equality
return True
return FalseSliding window#
A contiguous range that grows at the right and shrinks at the left. Every element enters once and leaves once, so the whole scan is linear even though the window is re-examined constantly. The skill is knowing what to maintain as it moves, and shrinking exactly while the window is invalid rather than once.
The signal is the words subarray, substring, window, or longest and shortest applied to a contiguous run.
def longest_unique(s):
"""Longest substring with no repeated character."""
last_seen = {}
start = best = 0
for i, ch in enumerate(s):
# If we have seen it inside the current window, jump past it.
if ch in last_seen and last_seen[ch] >= start:
start = last_seen[ch] + 1
last_seen[ch] = i
best = max(best, i - start + 1)
return best
longest_unique("abcabcbb") # 3, "abc"def min_window_sum(nums, target):
"""Shortest subarray summing to at least target. The while, not an if."""
start = total = 0
best = float("inf")
for end, n in enumerate(nums):
total += n
while total >= target: # shrink while valid, not once
best = min(best, end - start + 1)
total -= nums[start]
start += 1
return 0 if best == float("inf") else bestAn if where a while belongs
Shrinking with an if handles one element per step and silently returns a window that is too long. It passes the small examples and fails on the case where several elements must leave at once, which is usually the hidden test. If your window answer is slightly too big, this is why.
Binary search#
Halve the search space each step, which is why a million items take twenty comparisons. Writing it correctly under pressure is the hard part, so use one template and never improvise: keep the invariant that the answer is always inside the current range, and let the loop end when the range is empty.
def lower_bound(nums, target):
"""First index whose value is >= target. Returns len(nums) if none is."""
lo, hi = 0, len(nums) # hi is exclusive
while lo < hi: # ends when the range is empty
mid = (lo + hi) // 2
if nums[mid] < target:
lo = mid + 1 # mid is ruled out
else:
hi = mid # mid might be the answer, so keep it
return loThe version that separates candidates is binary search on the answer, where you search a range of possible answers rather than an array. It applies whenever you can cheaply ask is this candidate good enough, and where good enough is monotonic — if a value works, every larger one works too.
def min_capacity(weights, days):
"""Smallest ship capacity that moves all packages within days trips."""
def fits(capacity):
used, load = 1, 0
for w in weights:
if load + w > capacity:
used, load = used + 1, 0
load += w
return used <= days
lo, hi = max(weights), sum(weights) # must fit the largest; one trip
while lo < hi:
mid = (lo + hi) // 2
if fits(mid):
hi = mid # this works, try smaller
else:
lo = mid + 1
return loWhere the infinite loop comes from
Mixing an inclusive hi with lo = mid, or writing while lo <= hi with hi = mid, leaves a range that stops shrinking and the loop never ends. Pick the exclusive-hi template above, and if you ever write lo = mid without the plus one, check it against a two-element input by hand.
Stacks#
A stack is the right structure whenever the most recent unfinished thing is what matters: matching brackets, nested structures, undo. The variant worth knowing by name is the monotonic stack, which keeps its contents sorted by discarding anything the new element makes irrelevant. That is what answers next greater element in one pass instead of two nested loops.
def next_greater(nums):
"""For each element, the next element to its right that is larger."""
answer = [-1] * len(nums)
stack = [] # indices, values decreasing
for i, n in enumerate(nums):
# Anything smaller than n has just found its answer.
while stack and nums[stack[-1]] < n:
answer[stack.pop()] = n
stack.append(i)
return answer
next_greater([2, 1, 2, 4, 3]) # [4, 2, 4, -1, -1]If you can explain why that inner while does not make it quadratic — each index enters and leaves exactly once — you have the amortised argument interviewers are listening for on this pattern.
Drilling this properly#
Doing many problems badly is how most people waste this. After each one, write a single line naming the pattern and the phrase in the question that signalled it, then close the solution. Before an interview reread only those lines. Recognition is the thing that transfers; the code is a template you already have.
Pattern Time Space Worth drilling
hash map O(n) O(n) two sum, group anagrams, top-k frequent
two pointers O(n) O(1) sorted pair, palindrome, remove duplicates
fast and slow O(n) O(1) cycle detection, middle node, n-th from end
sliding window O(n) O(k) longest unique, min window, max sum of size k
binary search O(log n) O(1) lower bound, rotated array, on the answer
monotonic stack O(n) O(n) next greater, daily temperatures, valid parensWhere people go wrong
Memorising solutions rather than patterns. A memorised solution fails the moment the question is phrased differently, which it always is. Learn the shape and why it works.
Build this: Do 12 problems per pattern, and after each write one sentence naming the pattern and the signal that indicated it. Reread those sentences before an interview rather than the solutions.
Open this section on its own pageTrees, graphs and the rest
By the end: You can handle tree and graph questions and the remaining common patterns.
- Tree traversal, both orders
- Graph traversal and topological sort
- Heaps and top-k
- Intervals, backtracking and one-dimensional dynamic programming
Tree and graph questions look harder than they are, because nearly all of them are the same two traversals with something extra attached. This section covers those and then clears the remaining ground: heaps for top-k, interval problems, backtracking, and the limited amount of dynamic programming that is actually asked. Together with the previous section that is the complete list worth drilling.
Trees#
Almost every tree question is a traversal with something extra. There are two, and knowing which you need is most of the work. Depth-first goes down one branch before trying the next, uses a stack or recursion, and is what you want for anything about paths, depth or structure. Breadth-first visits a whole level before descending, uses a queue, and is what you want for anything about levels or the shortest number of steps.
def depth(node):
"""Depth-first. The recursion is the stack."""
if node is None:
return 0
return 1 + max(depth(node.left), depth(node.right))
from collections import deque
def levels(root):
"""Breadth-first, one list per level. The len(queue) snapshot is the trick:
it fixes how many nodes belong to this level before any are added."""
if root is None:
return []
out, queue = [], deque([root])
while queue:
level = []
for _ in range(len(queue)): # exactly this level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
out.append(level)
return outA binary search tree adds one property: everything left of a node is smaller, everything right is larger. That makes lookup logarithmic, and it makes an in-order traversal produce the values in sorted order, which is the basis of most BST questions.
def inorder(node, out=None):
"""Left, self, right. On a BST this emits the values in sorted order."""
out = [] if out is None else out
if node:
inorder(node.left, out)
out.append(node.val)
inorder(node.right, out)
return out
def is_bst(node, low=float("-inf"), high=float("inf")):
"""Every node must fit a range, not merely beat its parent."""
if node is None:
return True
if not low < node.val < high:
return False
return (is_bst(node.left, low, node.val)
and is_bst(node.right, node.val, high))Comparing a node with its parent is not enough
A tree with root 10, left child 5, and that child's right child 12 satisfies every parent comparison and is not a search tree, because 12 sits in the left subtree of 10. The range has to be carried down. This is the most common wrong answer to the most common tree question.
Graphs#
A graph is the same two traversals with one addition: you must remember where you have been, or a cycle makes the search run forever. Almost every grid question is a graph question where the neighbours are the four adjacent cells. Breadth-first gives the shortest path when every edge costs the same, which is why it is the answer to most maze and spreading questions.
from collections import deque
def shortest_path(grid, start, goal):
"""Fewest steps through a grid of open cells. BFS, because every step
costs the same. Dijkstra is for when they do not."""
rows, cols = len(grid), len(grid[0])
queue = deque([(start, 0)])
seen = {start} # mark on push, not on pop
while queue:
(r, c), dist = queue.popleft()
if (r, c) == goal:
return dist
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols
and grid[nr][nc] != "#" and (nr, nc) not in seen):
seen.add((nr, nc))
queue.append(((nr, nc), dist + 1))
return -1Topological sort is the other graph algorithm worth having ready. It orders tasks so every dependency comes first, and it answers the build-order and course-schedule family. The version to remember counts incoming edges and repeatedly takes whatever has none left; if the output is shorter than the input, there was a cycle.
def topo_sort(nodes, edges):
"""edges are (before, after) pairs. Returns [] if there is a cycle."""
incoming = {n: 0 for n in nodes}
out = {n: [] for n in nodes}
for before, after in edges:
out[before].append(after)
incoming[after] += 1
ready = deque(n for n in nodes if incoming[n] == 0)
order = []
while ready:
n = ready.popleft()
order.append(n)
for nxt in out[n]:
incoming[nxt] -= 1
if incoming[nxt] == 0:
ready.append(nxt)
return order if len(order) == len(nodes) else []
topo_sort("abcd", [("a", "b"), ("a", "c"), ("b", "d"), ("c", "d")])
# ['a', 'b', 'c', 'd']Heaps and top-k#
A heap keeps the smallest item reachable in constant time and costs logarithmic time to push or pop. That is exactly what top-k needs: hold a heap of size k, and whenever something bigger arrives, drop the smallest. Sorting the whole input is n log n; this is n log k, which matters when n is large and k is ten.
import heapq
def top_k(nums, k):
"""Python's heapq is a min-heap, which is what you want here: the
smallest of the current best k sits at the top, ready to be evicted."""
heap = []
for n in nums:
if len(heap) < k:
heapq.heappush(heap, n)
elif n > heap[0]:
heapq.heapreplace(heap, n) # push and pop in one operation
return sorted(heap, reverse=True)
top_k([5, 1, 9, 3, 7, 8], 3) # [9, 8, 7]
# heapq.nlargest(k, nums) does the same thing and is the right answer in
# production. Interviewers usually want the loop.The variant worth rehearsing is the streaming one, because it is asked in near-identical form at several of the largest employers: find the top k items in a stream you cannot store. The answer is the same heap, kept at size k, with the stream passing through it — you hold k items rather than n, and each item costs a comparison against the smallest of them.
from collections import Counter
def top_k_stream(stream, k):
"""Exact top-k by frequency, holding k items rather than the stream."""
counts = Counter()
heap = [] # min-heap of (count, item), size <= k
in_heap = set()
for item in stream:
counts[item] += 1
if item in in_heap:
# Rebuild lazily: its count moved, so the heap order is stale.
heap = [(counts[i], i) for _, i in heap]
heapq.heapify(heap)
elif len(heap) < k:
heapq.heappush(heap, (counts[item], item))
in_heap.add(item)
elif counts[item] > heap[0][0]:
_, evicted = heapq.heapreplace(heap, (counts[item], item))
in_heap.discard(evicted)
in_heap.add(item)
return sorted(heap, reverse=True)The follow-up is always the same
Two of them. First: the counter still grows with the number of distinct items, so what do you do when that will not fit? The answer is that you give up exactness — Count-Min Sketch or Space-Saving give you approximate counts in fixed memory, and you say out loud which guarantee you are trading. Second: what breaks when it is sharded? Top-k is not decomposable. The global tenth-most-frequent item can be eleventh on every shard and appear in no shard's local top ten, so merging local top-k lists is wrong. You either merge full counts for candidate items or accept an approximation and say so.
Intervals#
Interval questions are almost all the same problem: sort by start, then walk through deciding whether the current interval touches the one you are building. Meeting rooms, merging ranges, and insert-and-merge are the same six lines with a different comparison. The sort is what makes it work, and forgetting it is the usual failure.
def merge(intervals):
"""Overlapping ranges collapsed into one. O(n log n), the sort dominates."""
if not intervals:
return []
intervals = sorted(intervals) # by start
merged = [list(intervals[0])]
for start, end in intervals[1:]:
if start <= merged[-1][1]: # touches the one being built
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
merge([(1, 3), (2, 6), (8, 10), (15, 18)]) # [[1, 6], [8, 10], [15, 18]]The other shape counts overlaps rather than merging them — how many meeting rooms are needed at once. Treat every start as plus one and every end as minus one, sort the events, and the answer is the running maximum.
def max_rooms(meetings):
events = []
for start, end in meetings:
events.append((start, 1))
events.append((end, -1))
# An end at time t is processed before a start at time t: a room frees up
# exactly as the next meeting begins. -1 sorts before 1, which does it.
events.sort()
current = best = 0
for _, delta in events:
current += delta
best = max(best, current)
return best
max_rooms([(0, 30), (5, 10), (15, 20)]) # 2Backtracking#
Backtracking is exhaustive search with an undo. Choose, recurse, then put things back as they were so the next branch starts clean. Permutations, subsets, combinations, N-queens and sudoku are all the same skeleton, and writing that skeleton from memory is worth more than solving any one of them.
def permutations(items):
result, current, used = [], [], [False] * len(items)
def backtrack():
if len(current) == len(items):
result.append(current[:]) # copy, or every entry aliases
return
for i, item in enumerate(items):
if used[i]:
continue
used[i] = True # choose
current.append(item)
backtrack() # explore
current.pop() # undo
used[i] = False # undo
return
backtrack()
return result
permutations([1, 2, 3]) # [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]Appending the list instead of a copy
result.append(current) without the slice appends a reference to the same list, which is then mutated by the rest of the search. You end up with the right number of results and every one of them empty or identical. It is the single most common backtracking bug and it looks like a logic error rather than an aliasing one.
One-dimensional dynamic programming#
Far less of this is asked than the reputation suggests, and what is asked is almost always one-dimensional: an array where each entry is built from a few earlier ones. Work out what one entry means in words before writing any code, because that sentence is the whole solution and everything else follows from it.
def climb_stairs(n):
"""dp[i] = the number of distinct ways to reach step i.
You arrive from one step below or two, so the ways add."""
if n <= 2:
return n
a, b = 1, 2 # ways to reach steps 1 and 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
def coin_change(coins, amount):
"""dp[i] = fewest coins summing to exactly i, or infinity if impossible."""
dp = [0] + [float("inf")] * amount
for i in range(1, amount + 1):
for c in coins:
if c <= i:
dp[i] = min(dp[i], dp[i - c] + 1)
return -1 if dp[amount] == float("inf") else dp[amount]
coin_change([1, 5, 6, 9], 11) # 2, using 5 + 6The recognition signal is a question asking for the number of ways, or the best value, where a choice at each step affects what remains. If you can state what one array entry means and how it is built from earlier ones, you have finished the hard part.
What to drill, and what to skip#
Worth drilling Rarely worth it
tree traversal, both kinds red-black and AVL rotations
BFS on a grid Dijkstra, unless the job is routing
topological sort minimum spanning trees
heap for top-k Fibonacci heaps
interval merge and room counting segment trees, Fenwick trees
backtracking skeleton bitmask dynamic programming
one-dimensional dynamic programming two-dimensional, except edit distanceWhere people go wrong
Appending a mutable list in backtracking without copying it. Every entry in the result then points at the same list, which ends up empty. It looks like the algorithm failed when the algorithm was fine.
Build this: Do 15 tree problems, 12 graph problems and 8 each of the rest. Time yourself: 25 minutes is the realistic interview budget for a medium question including explanation.
Open this section on its own pageSolving one under interview conditions
By the end: You can work through a problem out loud, at speed, without freezing.
- The sequence that works
- Complexity, stated without being asked
- Testing your own solution
- What to do when you are stuck
Knowing the patterns and producing a solution in front of a stranger in thirty-five minutes are separate skills, and only the second one is examined. This is the part no amount of solved problems teaches you, because solving alone trains the wrong half: alone you may stare silently for ten minutes and still get the tick, and in the room that same ten minutes is a fail. What follows is a sequence to run every time, then a full worked transcript of it, then the specific ways strong candidates lose.
A repeatable sequence#
- 1.Restate the problem in your own words and confirm you have it right.
- 2.Ask about input size, edge cases and invalid input.
- 3.Give the brute force out loud with its complexity. This buys you a working answer and time to think.
- 4.Say what is wasteful about it and what structure would remove that waste.
- 5.State the approach and its complexity before writing any code.
- 6.Write it, narrating as you go.
- 7.Walk through a small example by hand.
- 8.Name the edge cases and check the code handles them.
Never start by coding
Candidates who begin typing immediately lose marks even when they arrive at the right answer, because the interviewer cannot see any reasoning. Steps three to five take two minutes and are what is being assessed.
The sequence in a real thirty-five minutes#
Read this once as a script and once as a clock. The times on the left are what a good pace looks like; the lines in brackets are what the interviewer is writing down. The problem is deliberately ordinary — most of them are.
00:00 INTERVIEWER Given a list of integers and a target, return the
indices of the two numbers that add to the target.
00:20 YOU So: one list, one target, and I return two positions —
not the values themselves. Is there always exactly one
answer, or do I need to handle none and handle several?
[asks before assuming — first thing on the rubric]
00:35 INT Assume exactly one.
00:40 YOU And how big does the list get? Are the numbers bounded,
can they be negative, can the same element be used twice?
00:55 INT Up to a million. Negatives are fine. Each element once.
01:05 YOU Then a million rules out anything quadratic — that's 10^12
operations. Let me still say the brute force so we agree
on correctness: two nested loops, check every pair, O(n²)
time and O(1) space. It's right, it's just too slow here.
[brute force stated as a baseline, not offered as the answer]
01:30 INT Agreed. What's better?
01:35 YOU The waste is that for each element I rescan the whole list
looking for target minus that element. That's a lookup, so
it should be a hash map. One pass: for each number, check
whether its complement has already been seen; if so I have
my pair, and if not I record where this number is. O(n)
time, O(n) space — I'm trading memory for the rescan.
02:10 INT Go ahead.
02:15 YOU [writes, narrating] I'll keep a dict from value to index.
Checking the complement before inserting the current value
is what stops it pairing an element with itself...def two_sum(nums, target):
seen = {} # value -> index
for i, n in enumerate(nums):
if target - n in seen: # check BEFORE inserting
return [seen[target - n], i]
seen[n] = i
return []04:30 YOU Let me run it. nums = [2, 7, 11, 15], target = 9.
i=0, n=2: is 7 in seen? No, seen is empty. Store 2 -> 0.
i=1, n=7: is 2 in seen? Yes, at 0. Return [0, 1]. Correct.
Edge cases: [3, 3] with target 6 — i=0 stores 3 -> 0,
i=1 finds 3 in seen and returns [0, 1], so duplicates work
because I check before I insert. Empty list returns [].
Negatives are fine, the arithmetic doesn't care.
[tests unprompted — the candidate most interviewers remember]
05:10 INT Good. Now suppose the list doesn't fit in memory.
05:15 YOU Then I can't hold the map...The clock is not the score
Reaching a working solution at minute five and handling two follow-ups well beats reaching it at minute thirty in silence, and it also beats reaching it at minute five and then sitting quietly. The signal being measured is how you think when the ground moves, which is why almost every loop ends with a change to the requirements. Expect it and stay engaged rather than treating the first working answer as the finish line.
The questions worth asking#
Clarifying questions are scored, but only the ones that could change your solution. Asking four of these costs forty seconds and repeatedly saves candidates from building the wrong thing.
Question What it changes
How large is the input? whether O(n log n) is acceptable at all
Sorted already? unlocks two pointers or binary search
Duplicates allowed? set vs counter; changes the answer's shape
Negatives? zero? empty? breaks prefix-sum and sliding-window tricks
One answer or all of them? return-early vs collect
Can I mutate the input? in-place saves the O(n) space
Streaming or in memory? heap and sketches instead of a full scan
What should invalid input do? raise, return empty, or clampComplexity, unprompted#
Give time and space complexity for every approach you mention, without being asked. It is a small thing that consistently reads as competence.
"Brute force is O(n²) time, O(1) space. The repeated work is
scanning for the complement, so a hash map removes it: O(n)
time, O(n) space. I'll do that unless memory is constrained."Three things are routinely got wrong here, and each is a cheap fix. Recursion costs stack space, so a depth-first traversal is O(h) space even with no data structure allocated. Slicing a list in Python copies it, so a recursion that passes nums[1:] is quietly quadratic. And the complexity you state must be the complexity of what you wrote — say the sort dominates if you sorted, even when the clever part after it is linear.
Test it yourself#
Do not wait to be asked whether it works. Walk a small input through your code aloud, then name the edge cases: empty input, one element, all identical, negative numbers, the maximum size. Walking the example means reading your own variables line by line, out loud, and saying the values — not re-describing the algorithm you intended to write. The gap between the two is where the bug is, and the candidates who find their own bug at minute twenty score better than the ones whose bug the interviewer finds at minute thirty.
Stuck#
Say so, and say what you have ruled out. An interviewer will often give a hint, and taking a hint well is scored positively. Sitting silently is the only genuinely bad option.
"I'm stuck. Brute force is O(n²) and I need better. Sorting
doesn't help because I need the original indices. I think I want
something that remembers what I've seen, so let me try a hash map
keyed on the value."That is three sentences: where I am, what I eliminated and why, what I will try next. It converts silence into something the interviewer can grade and can help with. If nothing comes, go back to the brute force and make it work — a correct slow solution with an honest account of why it is slow is a passing answer at most companies, and an empty editor is not.
How strong candidates lose#
What happens What it reads as Instead
Silent thinking for 3+ minutes cannot communicate narrate the dead ends
Coding before agreeing an approach does not plan two minutes of talking
Recognising and reciting a memo memorised, untested derive it out loud
Arguing when given a hint hard to work with take it, say thanks
Waving at complexity ("fast") cannot analyse give both, every time
Saying "done" without testing ships untested code walk an example first
Freezing after a requirement change rigid restate what changed
Hiding a bug you have noticed not trustworthy name it, then fix itPractise the sequence out loud, against a timer, talking to an empty room — it feels absurd and it is the single highest-return hour in this route. Record one session and watch it back. Nearly everyone discovers they go silent for far longer than they believed, and that alone, once fixed, moves the outcome.
Where people go wrong
Optimising before you have anything working. Get the brute force stated, then improve it. Silence while you search for the clever solution is the worst use of interview time.
Build this: Solve 20 problems out loud against a 25-minute timer, following the sequence every time. Record two of them and watch for silences and for skipped steps.
Open this section on its own pageNow actually do it
Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.
1Work the eleven patterns, twelve problems each
26hHash maps, two pointers, sliding window, binary search including on the answer, stacks and monotonic stacks, heaps and top-k, intervals, tree traversal, graph traversal and topological sort, backtracking, one-dimensional DP. After each problem write one sentence naming the pattern and the signal that indicated it.
Done when: 132 problems solved, and a page of one-line pattern signals you reread before an interview rather than rereading solutions.
2Timed solving, out loud
10h20 unseen medium problems against a 25 minute timer, narrating throughout: restate, clarify, brute force with complexity, the wasteful part, the improvement with complexity, then code. Record two sessions.
Done when: You finish within time on most, and on the recordings there is no silence longer than ten seconds.
3Recover from being stuck
4hDeliberately attempt 5 problems above your level. Practise saying where you are, what you have eliminated, and what you will try next. Take hints and continue cleanly.
Done when: You can be stuck for three minutes while still giving an interviewer something to assess.
40 hours of practice
Interview questions
13 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.
Working out whether you need this
Do AI engineering roles still ask algorithm questions?
It depends on the company rather than the role title. Large companies and infrastructure-flavoured teams generally still do. Product-focused AI companies where agents and retrieval are the product frequently do not, running practical build rounds instead.
How do you decide how much algorithm preparation to do?
From the target list. Check what the specific companies run, through candidate reports or by asking the recruiter directly. Forty hours is either essential or wasted depending entirely on that answer.
The high-frequency patterns
When do you reach for a hash map?
Whenever the question involves lookup, counting, grouping or duplicate detection. It turns a scan into a constant-time check, which is usually what converts a quadratic solution to a linear one.
How do you recognise a sliding window problem?
It asks about a contiguous subarray or substring with some property, usually the longest or shortest satisfying a condition. The shape is always expand with the loop, contract when the property breaks.
What is binary search on the answer?
When the answer is a value in a range and checking whether a candidate works is cheap, you binary search the range of possible answers rather than the input. The input does not need to be sorted; the answer space is.
What is a monotonic stack for?
Next-greater and next-smaller element questions in linear time. You keep the stack ordered, and when a new element breaks the order you have found the answer for everything it displaces.
Trees, graphs and the rest
When would you use BFS rather than DFS?
When the question involves levels, or the shortest path in an unweighted graph, since BFS reaches nodes in order of distance. DFS suits paths, subtree properties and anything naturally recursive.
How do you find the top k elements efficiently?
Keep a heap of size k rather than sorting everything. That is n log k rather than n log n, and it works on a stream where you cannot hold all the data at once.
What does topological sort solve?
Ordering with dependencies: course prerequisites, build order, task scheduling. It also detects cycles, since a cycle means some nodes never reach indegree zero and the output is shorter than the input.
What is the difference between recursion and backtracking?
Backtracking is recursion that undoes its choice before trying the next one. The undo step, usually a pop, is what makes it backtracking, and forgetting it is the most common bug in these problems.
Solving one under interview conditions
How do you approach a problem you have not seen before?
Restate it, clarify input size and edge cases, give the brute force with its complexity, identify the wasteful part, propose the improvement and its complexity, then write it while narrating. The brute force is not a concession, it is a working baseline and thinking time.
What do you do if you cannot solve it?
Say where I am and what I have eliminated, and propose the next thing to try. A working brute force with clear reasoning scores better than silence, and taking a hint well is a positive signal rather than a failure.
How important is finishing?
Less than candidates assume. A clear approach with correct complexity analysis, communicated well, usually scores better than a silently produced solution the interviewer could not follow.
Resources
Free to watch and read, picked for this topic.
Videos
For working out whether you need this
For the high-frequency patterns
For trees, graphs and the rest
For solving one under interview conditions
Full courses: the whole topic, start to finish
Stop 44 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.