Skip to content

Hard Data Structures and Algorithms Interview Questions

45 hard questions with answers. Design, trade-off and deep-dive questions asked for senior roles and at top product companies.

Topics in the hard set

Sorting, Searching and Backtracking: 8 Dynamic Programming: 7 Linked Lists, Stacks and Queues: 6 Heaps, Priority Queues and Greedy: 6 Trees and Binary Search Trees: 5 Graphs and Graph Algorithms: 5 Complexity Analysis and Recursion: 4 Arrays, Strings and Hashing: 4
Solved — hardStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

What is the Master Theorem and how do you apply it?

Save to ▾
Show answer
The Master Theorem solves recurrences of the form T(n) = a T(n/b) + f(n). Compare f(n) with n to the power log base b of a. If f(n) is smaller, the answer is n to that power; if equal, multiply by log n; if larger, f(n) dominates. Merge sort has a = 2, b = 2, f(n) = n, giving O(n log n). Binary search has a = 1, b = 2, f(n) = 1, giving O(log n).
02

Your solution passes the sample tests but gets Time Limit Exceeded on large inputs. How do you analyse and fix it?

Save to ▾
Show answer
First I estimate the complexity of my code against the constraints and find the dominant loop. Often a nested loop or repeated search inside a loop makes it O(n squared). I then look for a better pattern such as a hash map for lookups, sorting plus two pointers, prefix sums or memoisation of repeated subproblems. I also check hidden costs like string concatenation in loops or list.pop(0) in Python.
03

You can use an O(n log n) algorithm with high constant factors or a simple O(n squared) one. For what inputs might the O(n squared) choice be better?

Save to ▾
Show answer
Big-O ignores constants, so for small n the simpler algorithm can be faster in practice. Insertion sort beats merge sort for arrays below roughly 16 to 32 elements, which is why library sorts such as Timsort switch to insertion sort for small runs. I would benchmark on realistic input sizes, choose the asymptotically better algorithm when n can grow, and use a hybrid when both ranges occur.
04

A report generation function loops over all users and, for each user, scans all orders. It was fine at launch but is now very slow. How do you explain and fix it?

Save to ▾
Show answer
The loop is O(users times orders), so when both grow ten times, the work grows a hundred times. I would explain this with numbers and a quick profile. The fix is to group orders by user ID once using a hash map, making the total O(users plus orders), or to push the grouping into a database query with an index. I would add a performance test with production-scale data.
05

How do the KMP and Rabin-Karp string matching algorithms work?

Save to ▾
Show answer
Naive matching is O(n times m). KMP precomputes a longest prefix-suffix array for the pattern, so after a mismatch it shifts the pattern without re-reading text characters, giving O(n plus m) worst case. Rabin-Karp computes a rolling hash of each text window and compares characters only when hashes match, averaging O(n plus m) and extending well to multiple patterns, but collisions can make the worst case O(n times m).
06

You have a 100 GB file of integers and only 4 GB of RAM. How would you find the duplicates?

Save to ▾
Show answer
The data does not fit in memory, so I would partition it. Read the file in streaming fashion and write each number to one of, say, 50 bucket files based on hash of the number modulo 50, so equal numbers land in the same bucket. Each bucket of about 2 GB fits in memory, where a hash set finds duplicates. Alternatively use external merge sort and scan adjacent values. A Bloom filter can pre-screen candidates.
07

A hash map lookup in your service suddenly becomes very slow under certain traffic. What could cause it?

Save to ▾
Show answer
If many keys produce the same hash, they pile into a few buckets and operations degrade toward O(n). This can come from a poor custom hashCode, such as returning a constant or using only one field, or from attackers sending crafted keys. Fixes include a better hash function combining all fields, randomised hashing, which Python uses for strings, and tree-based buckets as in Java 8 HashMap, which caps the worst case at O(log n).
08

Return an array where each element is the product of all other elements, without using division.

Save to ▾
Show answer
def product_except_self(nums): n = len(nums) out = [1] * n prefix = 1 for i in range(n): # product of everything to the left out[i] = prefix prefix *= nums[i] suffix = 1 for i in range(n - 1, -1, -1): # multiply by everything to the right out[i] *= suffix suffix *= nums[i] return out # [1,2,3,4] -> [24,12,8,6]; O(n) time, O(1) extra space besides output
09

How does a monotonic deque find the maximum of every sliding window in O(n)?

Save to ▾
Show answer
Keep a deque of indices whose values are in decreasing order. For each new element, pop from the back while the back value is smaller, since those elements can never be the maximum again, then push the new index. Pop from the front if its index has left the window. The front is always the maximum of the current window. Every index enters and leaves once, so the total time is O(n) instead of O(n times k).
10

Why are linked lists often slower than arrays in practice, and what is a skip list?

Save to ▾
Show answer
Linked list nodes are scattered in memory, so traversal causes cache misses, and each node needs extra memory for pointers. Arrays are contiguous and benefit from CPU caching and prefetching, so even O(n) shifts can be fast for moderate sizes. A skip list is a layered linked list with express lanes chosen randomly, giving O(log n) average search, insert and delete; Redis uses it for sorted sets.
11

Two linked lists merge at some node and share the rest. How do you find the merge point efficiently?

Save to ▾
Show answer
A hash set of the first list's nodes works in O(m plus n) time but uses O(m) space. The O(1) space trick uses two pointers: walk each list, and when a pointer reaches the end, redirect it to the head of the other list. Both pointers travel m plus n steps, so they meet at the intersection, or both become null if there is none. Comparing node references, not values, is essential.
12

Design a rate limiter that allows at most 100 requests per user in any rolling 60-second window.

Save to ▾
Show answer
For each user keep a deque of request timestamps. On a request, pop timestamps older than now minus 60 seconds from the front, then allow the request only if fewer than 100 remain, pushing the new timestamp at the back. This sliding window log is exact but uses memory per request. At scale I would use a sliding window counter or token bucket stored in Redis with atomic operations to save memory and work across servers.
13

Reverse a linked list in groups of k nodes.

Save to ▾
Show answer
def reverse_k_group(head, k): # check that k nodes are available node, count = head, 0 while node and count < k: node = node.next count += 1 if count < k: return head # leave the remainder as is prev, cur = None, head for _ in range(k): # reverse k nodes cur.next, prev, cur = prev, cur, cur.next head.next = reverse_k_group(cur, k) # old head is now the group tail return prev # 1-2-3-4-5, k=2 -> 2-1-4-3-5; O(n) time
14

Tell me about a time you received critical feedback on your code.

Save to ▾
Show answer
Situation: a senior reviewer rejected my pull request because my queue processing code was O(n squared) and had no tests. Task: fix it and learn from it. Action: I asked for a short walkthrough, replaced repeated list removals with a deque, added unit tests for edge cases and asked for a second review. Result: the change merged with a ten times speed-up, and I now self-review complexity and tests before raising any pull request.
15

Compare segment trees and Fenwick trees.

Save to ▾
Show answer
Both answer range queries with point updates in O(log n). A Fenwick tree, or binary indexed tree, is compact, simple to code and ideal for prefix sums and counts, but supports mainly invertible operations. A segment tree stores an aggregate for each segment, handles any associative operation such as min, max or gcd, and supports range updates with lazy propagation, at the cost of about 4n memory and longer code.
16

What is a B-tree and why do databases use it for indexes?

Save to ▾
Show answer
A B-tree is a balanced search tree where each node holds many keys and has many children, often hundreds. This keeps the tree very shallow, so a lookup among millions of rows touches only three or four nodes. Each node is sized to a disk or memory page, minimising expensive I/O. B+ trees, used by MySQL InnoDB and PostgreSQL, keep all values in linked leaves, making range scans efficient.
17

A recursive tree traversal crashes on a skewed tree with one million nodes. How do you fix it?

Save to ▾
Show answer
A skewed tree has height about n, so recursion needs a million stack frames and overflows. Rewrite the traversal iteratively with an explicit stack stored on the heap, or use Morris traversal, which threads temporary links through the tree to achieve O(1) extra space. If the tree is built by our code, also consider why it is skewed and switch to a self-balancing tree so height stays O(log n).
18

A game leaderboard with millions of players needs fast score updates and queries like what rank is this player. How would you design it?

Save to ▾
Show answer
Sorting on every query is too slow. I would bucket scores and use a Fenwick tree or segment tree over score values, storing how many players have each score; a player's rank is the count of players with higher scores, found in O(log S). Updates move a player between buckets in O(log S). An order-statistic balanced tree also works, and in production Redis sorted sets, built on skip lists, provide this directly.
19

Serialize a binary tree to a string and deserialize it back.

Save to ▾
Show answer
def serialize(root): out = [] def dfs(node): if not node: out.append('#') return out.append(str(node.val)) dfs(node.left) dfs(node.right) dfs(root) return ','.join(out) def deserialize(data): vals = iter(data.split(',')) def build(): v = next(vals) if v == '#': return None node = TreeNode(int(v)) node.left = build() node.right = build() return node return build() # Preorder with null markers uniquely encodes the tree: O(n) both ways.
20

How does Huffman coding work?

Save to ▾
Show answer
Huffman coding builds an optimal prefix-free binary code for compression. Put each character with its frequency into a min-heap. Repeatedly remove the two least frequent nodes, merge them into a new node whose frequency is their sum, and push it back. The final tree gives codes: left edges are 0 and right edges are 1, so frequent characters get shorter codes. It runs in O(n log n) and is used in ZIP and JPEG.
21

How do you prove that a greedy algorithm is correct?

Save to ▾
Show answer
The common method is an exchange argument: take any optimal solution, show that replacing its first choice with the greedy choice does not make it worse, then apply the same reasoning to the remaining subproblem by induction. Another method is showing greedy stays ahead, meaning after each step its partial solution is at least as good as any other. In interviews, stating the argument briefly and testing small counterexamples builds confidence.
22

You need to merge K sorted log files from different servers into one sorted output. How would you do it efficiently?

Save to ▾
Show answer
Use a min-heap of size K holding the current smallest line from each file, keyed by timestamp. Pop the smallest, write it out, and push the next line from the same file. Each line is pushed and popped once, so total time is O(N log K) for N lines, and memory is O(K) plus buffers, which works even when files are larger than RAM. Buffered reading and writing keeps I/O efficient.
23

A monitoring service must report the median response time over a live stream of requests. How would you design it?

Save to ▾
Show answer
For an exact all-time median, use two heaps, giving O(log n) per insert and O(1) per query. For the median of only the last N minutes, you need deletions, so use two heaps with lazy deletion or a balanced order-statistic tree. At very high volume, exact medians are costly, so production systems use histograms or sketches such as t-digest or HDR histogram to give accurate percentiles with fixed memory.
24

CPU tasks labelled by type must be scheduled with a cooldown of n slots between two tasks of the same type. How do you minimise total time?

Save to ▾
Show answer
Greedily run the most frequent available task each slot. Use a max-heap of remaining counts and a queue of tasks cooling down with the time they become available. Each slot, move ready tasks back to the heap, pop the most frequent, decrement and enqueue it with time plus n. A closed formula also exists: max of total tasks and (maxFreq minus 1) times (n plus 1) plus the number of tasks with maxFreq.
25

Implement a data structure that returns the median of all numbers added so far.

Save to ▾
Show answer
import heapq class MedianFinder: def __init__(self): self.low = [] # max-heap via negation (smaller half) self.high = [] # min-heap (larger half) def add(self, x): heapq.heappush(self.low, -x) heapq.heappush(self.high, -heapq.heappop(self.low)) if len(self.high) > len(self.low): heapq.heappush(self.low, -heapq.heappop(self.high)) def median(self): if len(self.low) > len(self.high): return -self.low[0] return (-self.low[0] + self.high[0]) / 2 # add is O(log n), median is O(1)
26

Compare Bellman-Ford and Floyd-Warshall.

Save to ▾
Show answer
Bellman-Ford finds single-source shortest paths with negative weights by relaxing all edges V minus 1 times, O(V times E), and one more pass detects negative cycles. Floyd-Warshall finds shortest paths between all pairs using dynamic programming over intermediate vertices, O(V cubed) time and O(V squared) space, suitable for small dense graphs of a few hundred nodes. For all pairs on large sparse graphs, running Dijkstra from each vertex is faster.
27

How does cycle detection differ in directed and undirected graphs?

Save to ▾
Show answer
In an undirected graph, DFS finds a cycle when it reaches an already visited vertex that is not the parent of the current vertex; Union-Find also works by checking whether an edge joins vertices already in the same set. In a directed graph you need three states, unvisited, in the current recursion stack and finished, and a cycle exists when DFS reaches a vertex still on the stack. Kahn's algorithm also detects directed cycles.
28

Find the cheapest flight from one city to another using at most K stops. Why does plain Dijkstra not work directly?

Save to ▾
Show answer
Plain Dijkstra finalises the cheapest path to a city, but that path may use too many stops, while a costlier path with fewer stops could still lead to a valid answer. One fix is Bellman-Ford limited to K plus 1 rounds, copying distances each round, O(K times E). Another is Dijkstra on states of city and stops used, or a BFS by levels that tracks cost, pruning states that are worse in both cost and stops.
29

A network has 10 million devices and connections are added continuously. You must answer whether two devices are connected in real time. How?

Save to ▾
Show answer
Running BFS for every query is too slow. Since connections are only added, Union-Find is ideal: each new link is a union, and each query compares find results, both nearly O(1) amortised with path compression and union by size. Memory is two integer arrays of 10 million entries, about 80 MB. If links can also be removed, the problem becomes much harder and needs offline processing or specialised dynamic connectivity structures.
30

Implement Union-Find with path compression and union by size.

Save to ▾
Show answer
class DSU: def __init__(self, n): self.parent = list(range(n)) self.size = [1] * n def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] # path halving x = self.parent[x] return x def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra == rb: return False if self.size[ra] < self.size[rb]: ra, rb = rb, ra self.parent[rb] = ra self.size[ra] += self.size[rb] return True # Nearly O(1) amortised per operation
31

What is bitmask DP and when is it used?

Save to ▾
Show answer
Bitmask DP represents a subset of up to about 20 items as the bits of an integer, so the state can be the set of items already used plus perhaps the last item. The travelling salesman problem uses dp[mask][last], the minimum cost to visit the cities in mask ending at last, giving O(2 to the power n times n squared) instead of n factorial. It is used for assignment, scheduling and covering problems with small n.
32

What is DP on trees? Give an example.

Save to ▾
Show answer
DP on trees computes values for each node from the values of its children, usually in a postorder DFS. For the maximum independent set, where no two chosen nodes are adjacent, each node keeps two values: the best if it is taken, which is its value plus the not-taken values of children, and the best if it is not taken, the sum of each child's better option. This runs in O(n) and also solves house robber on a tree.
33

What is interval DP? Explain with matrix chain multiplication.

Save to ▾
Show answer
Interval DP defines states over a range i to j and combines smaller ranges. In matrix chain multiplication, dp[i][j] is the minimum scalar multiplications to multiply matrices i through j. Try every split point k between i and j: dp[i][k] plus dp[k+1][j] plus the cost of multiplying the two results. Fill by increasing interval length, O(n cubed). Similar patterns solve burst balloons and palindrome partitioning.
34

How do you find the longest increasing subsequence in O(n log n)?

Save to ▾
Show answer
The simple DP is O(n squared): dp[i] is the LIS ending at i. The faster method keeps an array tails, where tails[k] is the smallest possible last value of an increasing subsequence of length k plus 1. For each number, binary search tails for the first value not less than it and replace it, or append if none. The length of tails is the LIS length. This patience sorting idea also helps in envelope nesting problems.
35

A company has a fixed budget and several projects with costs and expected returns. How would you choose projects to maximise return?

Save to ▾
Show answer
If each project is all or nothing, this is the 0/1 knapsack problem: budget is capacity, cost is weight and return is value, solved with DP in O(projects times budget), and tracking choices lets me list selected projects. If the budget is very large, I would scale costs to units such as lakhs to keep the table small, or use DP over value instead. Extra constraints like dependencies change the state design.
36

Your knapsack DP has n equal to 100 items but capacity up to 10 to the power 9, so the table does not fit in memory. What do you do?

Save to ▾
Show answer
A table indexed by capacity is impossible, so I change the state. If item values are small, use dp[v] as the minimum weight needed to achieve value v, then pick the largest v whose weight fits the capacity; that is O(n times total value). If n is about 40, meet in the middle: enumerate subsets of each half, sort one side and binary search. Otherwise approximation or branch and bound are options.
37

Compute the edit distance between two words.

Save to ▾
Show answer
def edit_distance(a, b): m, n = len(a), len(b) prev = list(range(n + 1)) # distances from empty prefix of a for i in range(1, m + 1): cur = [i] + [0] * n for j in range(1, n + 1): if a[i - 1] == b[j - 1]: cur[j] = prev[j - 1] else: cur[j] = 1 + min(prev[j], # delete cur[j - 1], # insert prev[j - 1]) # replace prev = cur return prev[n] # edit_distance('horse', 'ros') -> 3; O(m * n) time, O(n) space
38

Why can comparison sorts not beat O(n log n), and how do counting and radix sort get around it?

Save to ▾
Show answer
Any comparison sort can be modelled as a decision tree with n factorial leaves, one per possible ordering, so its height, the number of comparisons, is at least log of n factorial, which is about n log n. Counting sort avoids comparisons by counting occurrences of each key in a small range k, giving O(n plus k). Radix sort applies a stable counting sort digit by digit, O(d times (n plus base)), great for fixed-length integers.
39

What does binary search on the answer mean?

Save to ▾
Show answer
Instead of searching an array, you binary search over possible answers when there is a monotonic feasibility check: if capacity X works, any larger capacity also works. Examples include minimum ship capacity to deliver within D days, minimum eating speed, or the largest minimum distance between placed items. Each check is often O(n) greedy, so total time is O(n log of the answer range), which is very efficient.
40

What is Timsort and why do Python and Java use it?

Save to ▾
Show answer
Timsort is a hybrid of merge sort and insertion sort. It scans the data for already sorted runs, extends short runs with insertion sort and merges runs using a stack with balancing rules and galloping mode. Real data is often partially sorted, so Timsort is close to O(n) on such input while keeping O(n log n) worst case and stability. Python uses it for all sorts and Java for sorting objects.
41

What is quickselect and when is it better than sorting?

Save to ▾
Show answer
Quickselect finds the kth smallest element by partitioning like quicksort but recursing only into the side that contains position k. On average it does about n plus n over 2 plus n over 4 work, which is O(n), compared with O(n log n) for sorting. The worst case is O(n squared), reduced by random pivots or median of medians for guaranteed O(n). C++ nth_element uses this idea.
42

You must sort a 10 GB file of records on a machine with 1 GB of RAM. How would you do it?

Save to ▾
Show answer
Use external merge sort. Read the file in chunks of about 800 MB, sort each chunk in memory and write it to disk as a sorted run, giving around 13 runs. Then perform a K-way merge: open all runs, keep a min-heap with the current record from each, repeatedly output the smallest and refill from that run, using buffered I/O. Total work is O(n log n) with roughly two passes over the data.
43

A Sudoku solver using plain backtracking is too slow on hard puzzles. How would you speed it up?

Save to ▾
Show answer
Add stronger pruning and better choice ordering. Keep bitmasks of used digits for each row, column and box, so validity checks are O(1). Always fill the empty cell with the fewest candidates first, the minimum remaining values heuristic, which shrinks the search tree dramatically. Apply constraint propagation, filling cells with a single candidate before guessing. For the hardest cases, exact cover with Dancing Links is very fast.
44

Search for a target in a rotated sorted array.

Save to ▾
Show answer
def search_rotated(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[lo] <= nums[mid]: # left half sorted if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 else: # right half sorted if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1 # [4,5,6,7,0,1,2], 0 -> 4; O(log n)
45

Tell me about a time you made slow code significantly faster at work.

Save to ▾
Show answer
Situation: a search API for our catalogue took two seconds because it filtered and sorted all items on every request. Task: bring p95 latency under 200 milliseconds. Action: I profiled it, pre-sorted items by the main key at load time, replaced linear filtering with binary search on sorted ranges and cached popular queries, validating results against the old code. Result: p95 dropped to 120 milliseconds and server costs fell by about a third.

Frequently asked questions

Who should practise hard Data Structures and Algorithms questions?

Candidates targeting senior roles or top product companies, and anyone who finds the medium set easy.

How should I practise these?

Answer each question aloud before opening the answer, mark it as solved only when your answer matches the key idea, and save misses to Last-Minute Revision.

Other difficulty levels