Phantom Code AIPhantom Code AI
FeaturesEarn with UsMy WorkspacePricing
FeaturesEarn with UsMy WorkspacePricing
Home/Interview Questions/Coding
Round type · Coding interview

Coding Interview Questions — Real Patterns, Complexity, and What Interviewers Listen For

Coding rounds aren't memorization. They're pattern recognition plus clean implementation plus complexity analysis articulated out loud. The same algorithm gets two candidates the same final answer — only one of them passes. The difference is what they say before they write, what edge cases they catch unprompted, and whether they name the tradeoffs the interviewer was already grading them on.

This page is organized by pattern, not by company or difficulty. Two-pointer, binary search, BFS/DFS, dynamic programming, heap, tree. For each pattern: representative problems, worked answers, and the senior-level signal interviewers are listening for. Use it as a structured review before a loop, or as a calibration tool for mock interviews.

What coding rounds actually test

The rubric is more than 'did the code run.' Five axes, scored independently, weighted by company:

  • Pattern recognition speed

    Did you name the pattern in the first 60 seconds? Saying 'this looks like a sliding window over a frequency map' before writing a line is the difference between a hire and a no-hire.

  • Code cleanliness

    Variable names that read like English, helper functions where they help, no dead branches, no unnecessary state. Interviewers read the code as documentation of how you'd write a PR.

  • Complexity articulation

    Time and space, big-O for both, with a sentence justifying each. Then: 'is this optimal, or is there a tighter bound?' If you don't ask yourself that, the interviewer asks for you and docks the signal.

  • Edge-case awareness

    Empty input, single element, duplicates, integer overflow, negative values, off-by-one at boundaries. Listing them preemptively and writing test cases mentally before the code compiles — that's the senior pattern.

  • Behavioral signal during the technical round

    The clarifying questions you ask, the tradeoffs you walk through aloud, how you respond when the interviewer pushes back. A correct solution delivered in silence scores worse than a slightly slower one delivered with a running narrative of 'here's what I'm considering, here's why I'm picking this.' Coding rounds are also behavioral rounds — most candidates forget that.

Two-pointer / sliding window Q&A

When the input is sorted, near-sorted, or you're hunting a contiguous subarray, two-pointer and sliding window collapse an O(n²) brute force down to O(n). The signal interviewers want: you spotted the pattern in 30 seconds, not 3 minutes.

Q: Two Sum — return indices of two numbers that add to target

Brute force is the nested loop, O(n²). The interviewer is waiting for you to say 'I'll trade space for time' and reach for a hash map. Single pass: for each element x, check if target - x is already in the map; if yes return the stored index and the current one, otherwise insert x with its index. O(n) time, O(n) space. The edge cases that earn points: what if the same number appears twice and is the answer (e.g., target=6, input=[3,3]) — your map check happens before the insert, so the duplicate works naturally. What if no pair exists — return an empty array or null per spec; ask. What about negative numbers, zeros, integer overflow — hash map doesn't care about sign, but mentioning overflow on int32 is a senior-level signal. Interviewers are listening for whether you state the complexity unprompted and whether you handle the duplicate-value case without prompting. Saying 'I'll keep a map of value to index, single pass, return the moment I find the complement' before you write a line of code is the move. Avoid sorting first — sorting destroys the original indices, so you'd need to track them separately, which is more code for the same complexity.

Q: Longest substring without repeating characters

Classic sliding window. Maintain a window [left, right] and a set of characters in the window. Expand right; when s[right] is already in the set, shrink left until the duplicate is removed, then continue. Track max length as you go. O(n) time, O(min(n, charset)) space. The optimization that separates mid from senior: instead of a set, use a hash map of char → last seen index, so when you hit a duplicate you can jump left to max(left, lastSeen + 1) in one step instead of shrinking one at a time. Edge cases to call out aloud: empty string returns 0, single character returns 1, all identical characters returns 1, all unique returns the length. Mention the alphabet — if it's ASCII you can use a fixed-size array of 128 ints instead of a hash map, which is faster in practice and a nice constant-factor win to mention. Interviewers listen for the word 'window' early and for you to articulate the invariant: 'the window always contains only unique characters.' That invariant is the proof your code is correct.

Q: Minimum window substring — smallest window in s containing all chars of t

The hard one in this family. Two-pointer sliding window with a frequency map of t and a counter of how many distinct chars are still needed. Expand right, decrementing the need counter only when a character's count in the window matches its required count. When need hits zero, you have a valid window — now contract left, updating the answer, until the window becomes invalid again. Then resume expanding. O(n + m) time, O(charset) space. The traps: counting must respect duplicates (if t='aabc', the window needs two a's, not one); the contraction step must update the need counter correctly when removing a character drops its window count below the required count. Edge cases: t longer than s — return empty string; characters in s not in t — they pad the window but you only contract them out for free. The senior signal is articulating the invariant 'I only update need when frequency matches' and walking through one tricky example aloud (s='ADOBECODEBANC', t='ABC' is the canonical one). Don't reach for two passes — one pass with the right counter does it.

Q: Container with most water

Two pointers at the ends, move the shorter one inward. Why? The area is min(h[left], h[right]) * (right - left). Moving the taller line inward can only reduce or keep width and can never increase the limiting height — so it's strictly non-improving. Moving the shorter one is the only move that can increase the area. O(n) time, O(1) space. The proof of correctness is what interviewers want to hear: 'I'm pruning all pairs that include the shorter line because no future pair with that line can beat the current area.' That's the greedy argument, and saying it unprompted earns the senior signal. Edge cases: fewer than two elements returns 0; flat arrays return 0 if length < 2 else (n-1) * h[0]. Don't confuse this with the trapping rain water problem — that one needs left-max and right-max arrays or a different two-pointer logic.

Binary search Q&A

Binary search is the question that filters out 'I memorized LeetCode' from 'I understand invariants.' The textbook version is easy. The variants — rotated arrays, first/last occurrence, search in answer space — are where careless code dies on off-by-one errors live in the interview.

Q: Search in rotated sorted array

The array was sorted, then rotated at an unknown pivot. Standard binary search fails because the array isn't monotonic globally. The trick: at every midpoint, exactly one of [left..mid] or [mid..right] is still sorted — figure out which by comparing nums[left] to nums[mid]. If the left half is sorted and target lies within nums[left]..nums[mid], recurse left; otherwise recurse right. Symmetrically for the right half. O(log n) time, O(1) space iterative. The edge cases that destroy people: duplicates (the LC version II), empty array, single element, target equals nums[left] or nums[right] exactly, the rotation point is at index 0 (no rotation). The invariant to articulate: 'at each step, one half is sorted, and I can decide in O(1) which half contains the target.' Common bug: using nums[left] <= nums[mid] vs nums[left] < nums[mid] — if duplicates exist (variant II), neither comparison alone works and you fall back to O(n) worst case. Senior candidates name that explicitly.

Q: Find first and last position of element in sorted array

Two binary searches, one for the leftmost occurrence and one for the rightmost. The mistake juniors make: do one binary search to find any occurrence, then linearly scan outward — that's O(n) worst case on an array of duplicates. The correct version modifies the binary search to bias left (when nums[mid] >= target, move right = mid; when nums[mid] < target, move left = mid + 1), and a symmetric version that biases right. Both are O(log n). The boundary condition discipline matters: the loop is while left < right, the answer is at left when the loop exits, and you check if nums[left] == target before returning to handle 'target not present.' Edge cases: empty array returns [-1,-1]; target smaller than all or larger than all; target appears once (first == last); entire array is target. The interview signal is whether you write the two boundary functions cleanly and verify them on a small example like [5,7,7,8,8,10], target=8 expecting [3,4]. Saying 'I'll write a generic lowerBound and upperBound helper' before coding is the strongest opening.

Q: Median of two sorted arrays

The famous hard one. The naive merge is O(m+n); the interviewer wants O(log(min(m,n))). Binary search on the smaller array's partition point i; the partition in the larger array is forced as j = (m+n+1)/2 - i. The partition is valid when nums1[i-1] <= nums2[j] and nums2[j-1] <= nums1[i]. If invalid, shrink i or grow i and repeat. The median is then the max of the left halves (odd total) or the average of the max-left and min-right (even total). The boundary trick: use sentinel -infinity for partition index 0 and +infinity for the array length, so you don't branch on edges. Edge cases: one array empty (median is the middle of the other); arrays of very different lengths (always binary search the smaller one or the loop blows up); duplicates across arrays; one array entirely smaller than the other. This problem rewards a calm whiteboard approach: draw the two arrays, draw the partition line, write the four boundary values, derive the validity condition. Interviewers listening for: you state the (m+n+1)/2 indexing handles both odd and even total length uniformly, and you handle the empty-array edge case before coding.

BFS/DFS / graph Q&A

Most graph problems disguise themselves. 'Number of islands' is a connected-components count. 'Course schedule' is cycle detection on a DAG. The senior skill is reframing the problem in graph language out loud, then picking BFS, DFS, or topological sort with a one-sentence justification.

Q: Number of islands

A 2D grid of '1' and '0'; count connected components of '1's where adjacency is 4-directional. Iterate every cell; when you hit an unvisited '1', start a BFS or DFS, mark every reachable '1' as visited (either flip it to '0' in place or use a visited set), increment the island count. O(m*n) time, O(m*n) space worst case for the queue/recursion. The one-line reframing to say first: 'this is connected components on an implicit grid graph.' Edge cases: empty grid; all water; all land (one island); 1xN or Nx1 strips. The implementation choice — BFS with a queue versus DFS with recursion — has a real tradeoff worth voicing: DFS is shorter to write but blows the stack on large grids (a 1000x1000 of all '1's is 10^6 deep). Senior candidates pick iterative BFS or iterative DFS with an explicit stack for production-grade code. The mutation-vs-visited-set tradeoff: mutating the grid is O(1) extra space but destroys input; visited set is cleaner but O(m*n) extra. Mention both, pick one, justify.

Q: Word ladder — shortest transformation from beginWord to endWord

BFS, because we want shortest path in an unweighted graph where nodes are words and edges connect words differing by one letter. The naive 'compare every pair of words' edge construction is O(N² * L). The trick: build a pattern map where each word maps to all its wildcard patterns ('hot' → '*ot', 'h*t', 'ho*') and use that map during BFS to find neighbors in O(L * 26) per word. O(N * L²) total. Use BFS, not DFS — DFS finds a path, not the shortest. Edge cases: endWord not in the dictionary (return 0); beginWord == endWord (0 or 1 depending on spec — clarify); no path exists (return 0). Bidirectional BFS is the optimization that earns the senior signal: search from both ends simultaneously, terminating when the frontiers meet, which roughly square-roots the visited count. State 'I'd start with single-direction BFS for correctness, then mention bidirectional BFS as the optimization.' That ordering — correctness first, optimization framed as a follow-up — is the experienced engineer's move.

Q: Course schedule — can you finish all courses given prerequisites?

This is cycle detection on a directed graph. Two clean approaches. (1) Kahn's algorithm: compute in-degrees, push all zero-in-degree nodes to a queue, repeatedly pop and decrement neighbors' in-degrees, pushing any that hit zero. If the popped count equals N, the graph is a DAG and you can finish; otherwise a cycle exists. (2) DFS with three-color marking (white/unvisited, gray/in-current-path, black/done): if DFS hits a gray node, you've found a back edge — cycle. Both are O(V+E). The follow-up 'return the order' is Course Schedule II — Kahn's gives the topological order for free; DFS gives reverse postorder. The interview signal: name the abstraction ('this is topological sort / cycle detection on a directed graph') in the first sentence. Edge cases: no prerequisites at all (trivially possible, any order); self-loop (cycle of length 1, impossible); disconnected components (don't forget to start DFS from every unvisited node).

Q: Clone graph — deep copy an undirected graph

BFS or DFS with a hash map from original node to cloned node. The map serves two purposes: it's the visited set and it's how you wire up edges to already-cloned neighbors. On visiting an original node, create its clone if not in the map, then for each neighbor: if cloned already, link to the existing clone; otherwise create the clone, link, and enqueue. O(V+E) time and space. The bug that catches juniors: creating the clone but forgetting to populate its neighbors before another node tries to link to it, leading to incomplete connections. The map-first invariant ('I always put a node in the map before traversing its neighbors') prevents infinite recursion in cycles and prevents duplicate clones. Edge cases: empty graph (return null); single node with no edges; single node with self-loop; fully disconnected graph (the function only clones the component reachable from the input node — clarify whether that's the intent). State the invariant, name the data structure, write it.

Q: Word search — does word exist in 2D grid by adjacent letters?

DFS with backtracking. Iterate every cell as a potential starting point; from each, recurse in 4 directions trying to match the next character of the word. The crucial detail: mark the current cell as visited before recursing (e.g., temporarily set it to '#') and unmark on the way back, so the path can revisit other cells but not itself within a single search. O(m * n * 4^L) time worst case, O(L) recursion depth. Edge cases: empty word (return true or clarify); word longer than total cells (early-return false); first letter not in grid at all (early return). The optimization that scores points: prune by character frequency — if any character in the word appears more often than in the grid, return false immediately. Also: search starting from the rarer end of the word (reverse if the last character is rarer than the first). Senior candidates also note the recursion-stack risk on long words. Clean backtracking style — restore state on the way out, every time — is the readability signal interviewers grade on.

Dynamic programming Q&A

DP is where candidates either earn the senior label or visibly drown. The discipline isn't memorizing recurrences — it's stating the subproblem definition in one sentence, writing the recurrence, then deciding bottom-up versus memoized top-down. Articulate the state space out loud before you touch code.

Q: Longest common subsequence (LCS)

Let dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]. Recurrence: if s1[i-1] == s2[j-1], dp[i][j] = dp[i-1][j-1] + 1, else dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Base case: dp[0][*] = dp[*][0] = 0. O(m*n) time, O(m*n) space. The first space optimization: only the previous row is needed, so O(min(m,n)) space using two rolling rows. Edge cases: either string empty returns 0; identical strings return their length; no common characters returns 0. The follow-up that often catches people: reconstruct the actual subsequence — you walk back through the dp table from dp[m][n], moving diagonally on matches and toward the larger neighbor on mismatches, building the string in reverse. Mention this when you state the approach, even before being asked: 'I'll fill the table for the length, and we can reconstruct the string in O(m+n) by walking back if needed.' That preempts the follow-up and shows you've seen the family of problems before.

Q: Coin change — fewest coins to make amount

1D DP: dp[i] = fewest coins to make amount i. Initialize dp[0] = 0 and dp[i] = infinity for i > 0. For each amount i from 1 to target, for each coin c, if c <= i, dp[i] = min(dp[i], dp[i-c] + 1). Final answer: dp[target] if not infinity, else -1. O(amount * coins) time, O(amount) space. The trap: this is unbounded knapsack (each coin used unlimited times), so the inner loop iterates over all coins for every amount — different from 0/1 knapsack where you iterate amount in reverse. Edge cases: target = 0 (zero coins); coins empty or all coins larger than target (return -1); a coin equal to target (return 1). The interview signal: state the subproblem definition in plain English ('dp[i] is the fewest coins for exactly amount i') before writing the recurrence. The greedy approach (always pick the largest coin that fits) is wrong for arbitrary denominations — call this out unprompted with a counterexample like coins=[1,3,4], target=6, where greedy gives 3 (4+1+1) but optimal is 2 (3+3). Naming why greedy fails earns serious credit.

Q: House robber — max sum non-adjacent

1D DP: dp[i] = max sum robbing houses 0..i. Recurrence: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) — either skip house i and keep the previous best, or take house i plus the best up to i-2. Base cases: dp[0] = nums[0], dp[1] = max(nums[0], nums[1]). O(n) time, O(1) space when you collapse to two rolling variables (prev, curr). Edge cases: empty array returns 0; single house returns its value; all negative numbers — clarify the spec, the standard problem has non-negative values. The follow-up House Robber II (houses in a circle) is the variant interviewers reach for next: solve it as max of two linear runs — one excluding house 0, one excluding house n-1 — because you can't rob both endpoints in a circle. Mention this preemptively if asked about extensions. The senior move: write the rolling-variable version directly instead of the array, and explain why ('we only ever look back two states, so the rest of the array is dead weight').

Q: Edit distance (Levenshtein)

dp[i][j] = min edits to transform s1[0..i-1] to s2[0..j-1]. Recurrence: if s1[i-1] == s2[j-1], dp[i][j] = dp[i-1][j-1] (no operation needed). Else dp[i][j] = 1 + min(dp[i-1][j-1] /* replace */, dp[i-1][j] /* delete from s1 */, dp[i][j-1] /* insert into s1 */). Base cases: dp[0][j] = j, dp[i][0] = i. O(m*n) time and space, with the standard rolling-row reduction to O(min(m,n)) space. Edge cases: either string empty (answer is the other's length); identical strings (zero); strings of very different length (the diagonal walk dominates). The interview signal is articulating the three operations as state transitions out loud and explaining why the diagonal step represents both 'replace' and 'match.' Senior follow-up: reconstruct the edit script (the actual sequence of ops) by walking back from dp[m][n] — each cell's choice tells you which op produced it. As with LCS, mention this preemptively: 'I'll compute the distance, and we can recover the edit sequence in O(m+n) by traceback if needed.'

Heap / priority queue Q&A

Heaps show up wherever you need 'top K,' 'merge K,' or 'streaming median.' The signal: you reach for a heap unprompted, you justify why heap beats sort (sort is O(n log n), heap of size K is O(n log K)), and you pick min-heap vs max-heap correctly the first time.

Q: Top K frequent elements

Step one: count frequencies in O(n) with a hash map. Step two: extract the top K. The naive 'sort by frequency' is O(n log n). The heap approach: maintain a min-heap of size K of (freq, value) pairs; for each entry in the frequency map, push and if size exceeds K, pop the smallest — O(n log K). The optimal approach for this specific problem: bucket sort, since frequencies are bounded by n — an array of buckets where bucket[i] holds elements with frequency i, then iterate from the back collecting K. O(n) time, O(n) space. Mention all three approaches and pick by constraints: 'if K is small relative to n, heap; if K is comparable to n or you want guaranteed O(n), bucket sort.' Edge cases: K equals n (return all elements, sorted by frequency or unsorted per spec — clarify); ties in frequency (clarify whether order matters); empty input. The senior signal is enumerating tradeoffs out loud rather than committing to one approach immediately, and asking 'what are the constraints on K and on the value range' before coding.

Q: Merge K sorted lists

Min-heap of head pointers from each of the K lists. Pop the smallest, append to the result, push the popped node's next if it exists. O(N log K) time where N is total elements, O(K) heap space. The alternative — divide and conquer pairwise merging — is also O(N log K) but uses O(log K) recursion depth and is sometimes preferred for stability or because no heap library is available. Mention both. Edge cases: K = 0 (return empty list); some lists empty (skip them when initializing the heap); K = 1 (return the single list as-is, no work). The pitfall on heaps of nodes: you need a tiebreaker for the comparator when two nodes have the same value, otherwise some languages will try to compare the node objects and crash — push a tuple (value, index, node) where index disambiguates. Naming this pitfall preemptively is a senior-level signal because it's the kind of thing that bites you in production but reads fine in pseudocode. The wrong instinct is 'concatenate everything and sort,' which is O(N log N) and ignores the structure you've been given.

Q: Find median from data stream

Two heaps: a max-heap for the lower half and a min-heap for the upper half. Invariant: max-heap.size == min-heap.size or max-heap.size == min-heap.size + 1. On addNum, push to the appropriate heap based on comparison with the max-heap's top, then rebalance by moving the top of one to the other if the size invariant breaks. Median is the max-heap's top if odd count, else the average of both tops. addNum is O(log n), findMedian is O(1). Edge cases: empty stream (return null or 0 per spec, clarify); single element; all identical; very large stream (mention that the heaps grow unboundedly — if memory is a constraint, propose a sliding-window or approximate-quantile structure like t-digest as the followup). The interview signal: state the invariant before writing code and verify it holds after every insert. Saying 'the max-heap holds the smaller half, the min-heap holds the larger half, and I keep them within size one of each other' is the whole problem's correctness argument in one sentence.

Tree Q&A

Tree problems reward clean recursion. The senior pattern: state what each recursive call returns in one sentence ('returns the LCA in this subtree, or null'), then trust that signature and write three lines of code. Most tree bugs come from candidates who can't articulate the contract.

Q: Lowest common ancestor of a binary tree

Recursive: lca(root, p, q) returns the LCA node within root's subtree, or null if neither p nor q is present. If root is null or root is p or q, return root. Recurse left and right. If both sides return non-null, the current root is the LCA. Otherwise return whichever side is non-null. O(n) time, O(h) space for recursion. The contract is the whole problem — state it before writing code: 'this function returns the LCA if found in the subtree, else returns p or q if exactly one is present, else null.' That contract makes the four cases (both null, left null, right null, both non-null) fall out naturally. Edge cases: p or q not in the tree at all (problem usually guarantees they are — confirm); p is an ancestor of q (the function still works, returns p, because the recursion at p hits the 'root == p' base case and returns p without exploring further); root itself is p (return root). The BST variant has an O(h) trick: use the BST property — descend left if both p and q are smaller than root, right if larger, else root is the LCA. Mention if the tree happens to be a BST.

Q: Serialize and deserialize binary tree

Pre-order DFS with explicit nulls. Serialize: pre-order traversal, emit node values separated by commas and 'null' for missing children. Deserialize: split, consume tokens in pre-order — each token is either a value (create node, recurse left then right) or 'null' (return null). O(n) time and space for both. Why pre-order with nulls works: the null markers preserve structure unambiguously, while in-order would be ambiguous without additional info. The level-order BFS variant is also valid and sometimes cleaner for visualizing but uses more space. Edge cases: empty tree (return 'null' or empty string and parse symmetrically); single node; deeply skewed tree (recursion depth equals n — mention iterative version with explicit stack for safety). The interview signal: pick a format, justify why it preserves structure, then implement both directions. Don't use the in-order + pre-order reconstruction trick unless asked — it's a different problem (rebuild from two traversals) and conflating them is a junior mistake. Senior candidates also mention encoding subtleties (commas inside string node values requiring escaping) before being asked.

Q: Path sum III — count paths with given sum (path can start/end anywhere)

The naive approach is two nested DFSs: outer iterates every node as a path start, inner explores all downward paths from that start counting matches. O(n²) time. The optimal is a prefix-sum hash map. Single DFS carrying currentSum; at each node, look up (currentSum - target) in a map of prefix-sum-counts seen on the path from root to here — that gives the number of paths ending at the current node with the desired sum. Add to the global count. Then recurse, adding the current prefix sum to the map; on the way back up, decrement it (the unmark step is what makes this correct, because prefix sums on a different branch must not be reachable). O(n) time, O(h) space. Initialize the map with {0: 1} so paths starting from the root itself count. The unmark-on-return discipline is the tricky bit — most candidates forget it and double-count across branches. Edge cases: empty tree returns 0; target = 0 (paths summing to zero, which can include single-node-with-value-zero paths); negative values along the path. State the prefix-sum reframing aloud first ('this is the same trick as subarray-sum-equals-K, just on a tree instead of an array') — that earns the strongest signal because it shows transfer learning across problem families.

Q: Validate binary search tree

The wrong answer: at each node, check that left.val < node.val < right.val. That's locally correct but globally wrong — a value deep in the left subtree could violate the ancestor's bound. The right answer: pass down min and max bounds. validate(node, min, max) — at each call, check min < node.val < max, then recurse left with (min, node.val) and right with (node.val, max). Use null/infinity sentinels at the root. O(n) time, O(h) space. The alternative: in-order traversal yields a sorted sequence iff the tree is a BST; track previous value and check strictly increasing. Both are O(n). Edge cases: empty tree is a valid BST (return true); single node is valid; duplicate values — the standard problem says strictly less than (no duplicates allowed), but always clarify. Articulate the bug in the naive approach unprompted: 'a local check isn't enough, I need to thread a global bound through the recursion.' That single sentence demonstrates you've seen this problem before and understand why the obvious approach fails. The seniors who articulate the wrong-answer trap before writing the right code consistently outperform those who go straight to the answer.

Practice with PhantomCode

Pattern recognition is a skill. Train it under interview conditions.

PhantomCode runs invisibly during your live coding rounds and mock practice sessions. It listens to the prompt, suggests the pattern, walks the recurrence with you, and surfaces the edge cases the interviewer is waiting to hear you call out. No tab switching. No screen-share artifacts.

Coding copilotInterview copilot

Related pages

Interview Questions Hub
Companies, roles, and round types
Behavioral round
STAR-format and senior-leveling signals
Coding copilot
Live pattern hints during real rounds