The 65 data structure and algorithm questions interviewers actually ask, with direct answers, runnable code, Big-O tables, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
65 questions with answersKey Takeaways
A data The technical sequence is a way of storing and organizing data so that the operations you care about run efficiently. An array gives instant access by index. A hash table gives near-instant lookup by key. A tree keeps data sorted while still allowing fast search. The structure you choose decides whether a feature runs in a millisecond or a full second, which is why data structure questions dominate technical interviews. Algorithms are the procedures that run on those structures: searching, sorting, and traversing. Together they're called DSA, data structures and algorithms. Interviewers care less about whether you've memorized a red-black tree and more about whether you can look at a problem, The structure that fits, and defend the choice with Big-O reasoning. Most coding rounds let you pick your language, so answer in whichever you know best. This page uses Python for its readability, but every structure and complexity here applies across languages. The official Python tutorial from the Python Software Foundation covers the built-in structures if you want to ground the fundamentals, and since the first technical round is increasingly a live AI coding interview, pair this bank with our AI interview preparation guides for the format side.
Watch: Data Structures Easy to Advanced Course: Full Tutorial from a Google Engineer
Video: Data Structures Easy to Advanced Course: Full Tutorial from a Google Engineer (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Data Structures certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
A data The technical sequence is a way of organizing data in memory so that specific operations run efficiently. The same data stored as an array, a hash table, or a tree gives completely different speeds for search, insert, and delete.
It matters because the structure you pick decides whether a feature is fast or slow. Looking up a user by ID in a list is O(n); in a hash table it's O(1). Choosing well is often the difference between a millisecond and a second, which is why interviews lead with this.
Key point: Open with one crisp definition plus one concrete speed contrast (list lookup vs hash lookup). Interviewers use this to hear whether you structures connects to real performance.
An array stores elements in one contiguous block of memory, so accessing element i is O(1) by direct address math. But inserting or deleting in the middle is O(n) because every later element shifts.
A linked list stores each element in a node that points to the next one. There's no contiguous block, so access by position is O(n), but inserting or deleting at a known node is O(1) because you just relink pointers. Arrays also iterate faster in practice because contiguous memory is cache-friendly.
# Python's list is a dynamic array
arr = [10, 20, 30]
arr[1] # O(1) access
arr.insert(1, 15) # O(n): shifts 20 and 30 right
# A simple singly linked list node
class Node:
def __init__(self, value, nxt=None):
self.value = value
self.next = nxt
head = Node(10, Node(20, Node(30))) # 10 -> 20 -> 30| Array | Linked list | |
|---|---|---|
| Access by index | O(1) | O(n) |
| Insert/delete at known node | O(n) | O(1) |
| Memory layout | Contiguous | Scattered nodes + pointers |
| Cache friendliness | High | Low |
Key point: The follow-up is almost always 'when would you choose a linked list?'. Have the answer ready: frequent insert or delete at positions you already hold a reference to, like an LRU cache.
Big-O describes how an algorithm's running time or memory grows as the input size grows, ignoring constants and lower-order terms. It answers 'does this still work when n gets large?' rather than 'how many milliseconds?'.
The common ladder from fast to slow is O(1) constant, O(log n) logarithmic, O(n) linear, O(n log n), O(n squared) quadratic, and O(2^n) exponential. Interviewers care because it's the shared language for comparing solutions: naming your solution's Big-O and improving it is the core skill a coding round measures.
How fast each complexity grows
Operations for n = 1000 on a log scale. Each step up the ladder is dramatically more work.
Key point: State both time and space complexity for your solution in practice. Forgetting space is a common miss, and it matters.
Watch a deeper explanation
Video: Big-O notation in 5 minutes (Michael Sambol, YouTube)
A stack is a Last-In-First-Out (LIFO) structure: the last item pushed is the first popped. It supports push, pop, and peek, all O(1). Think of a stack of plates: you take from the top.
Stacks appear everywhere: the call stack that tracks function calls, undo/redo features, matching brackets in a parser, and depth-first traversal. When a problem needs 'the most recent unmatched thing,' a stack is usually the answer.
stack = []
stack.append(1) # push
stack.append(2) # push
stack.pop() # 2, pop the top
stack[-1] # 1, peek without removingKey point: Bracket matching is the classic stack coding question. If you can explain that pattern, you've shown you understand LIFO in practice.
A queue is First-In-First-Out (FIFO): the first item enqueued is the first dequeued, like a line at a checkout. A stack is LIFO. That single difference decides which one fits a problem.
Use a deque (double-ended queue) for an efficient queue in Python, because popping from the front of a plain list is O(n). Queues drive breadth-first search, task scheduling, and any 'process in arrival order' workflow.
from collections import deque
q = deque()
q.append(1) # enqueue
q.append(2)
q.popleft() # 1, dequeue from the front, O(1)Key point: Mentioning that list.pop(0) is O(n) and deque.popleft() is O(1) shows you know the practical cost, not just the concept.
A hash table stores key-value pairs and gives average O(1) lookup, insert, and delete. It runs each key through a hash function that maps it to a slot in an underlying array, so finding a key means computing its hash and going straight to that slot, no scanning.
In Python the built-in dict and set are hash tables. The O(1) is an average: with a good hash function and a reasonable load factor, keys spread out evenly. Bad hashing or too many collisions can degrade it toward O(n).
phone = {"Asha": "555-0100"}
phone["Ben"] = "555-0111" # O(1) insert
phone.get("Asha") # O(1) lookup
"Ben" in phone # O(1) membershipKey point: The natural follow-up is 'what happens on a collision?'. Being ready with chaining and open addressing turns a basic answer into a strong one.
Watch a deeper explanation
Video: Data Structures: Hash Tables (HackerRank, YouTube)
A collision happens when two different keys hash to the same slot. It's unavoidable because there are more possible keys than slots, so every real hash table needs a resolution strategy.
Two main strategies: separate chaining stores a small list (or tree) of entries at each slot, so colliding keys live together and you scan that short list. Open addressing (used by CPython's dict) probes for the next free slot instead. Both keep average operations O(1) as long as the load factor stays controlled.
Key point: Don't just The strategies, say why collisions are inevitable (pigeonhole principle: more keys than slots). That reasoning is what the technical value is.
The load factor is the ratio of stored entries to available slots (n / capacity). As it climbs, collisions become more frequent and lookups slow down, because more keys share slots.
When the load factor crosses a threshold (often around 0.66 to 0.75), the table resizes: it allocates a larger array and rehashes every key into it. That single insert is O(n), but it's rare, so the amortized cost of insertion stays O(1). This is why dict insertion is fast on average despite the occasional expensive resize.
Key point: The word 'amortized' is the one to reach for here. Explaining that occasional O(n) resizes average out to O(1) shows real understanding.
Use a set when you need fast membership tests or automatic deduplication and don't care about order or duplicates. Checking 'x in list' is O(n), but 'x in set' is O(1) average, because a set is a hash table.
The single cheapest optimization in many programs is converting a lookup list to a set. Sets also give O(1) add and remove, plus union, intersection, and difference operations that would be loops on a list.
seen = set()
for item in stream:
if item in seen: # O(1) instead of O(n)
continue
seen.add(item)
unique = set([1, 2, 2, 3]) # {1, 2, 3}, dedup for freeKey point: If a coding problem asks 'have I seen this before?', a set is almost always the intended tool. Reaching for it fast indicates fluency.
A binary tree is a hierarchy where each node has at most two children, a left and a right. It's the base for many structures. A binary search tree (BST) adds one rule: for every node, all values in the left subtree are smaller and all in the right subtree are larger.
That ordering rule makes search, insert, and delete O(log n) in a balanced BST, because each comparison lets you discard half the remaining tree, just like binary search. An unbalanced BST can degrade to O(n), which is why self-balancing trees exist.
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def search(node, target):
if node is None or node.val == target:
return node
if target < node.val:
return search(node.left, target)
return search(node.right, target)Key point: Interviewers often ask why a BST beats a sorted array for a dataset that changes. The answer: O(log n) inserts and deletes versus O(n) shifting in an array.
Depth-first traversals visit a subtree fully before moving on: inorder (left, node, right), preorder (node, left, right), and postorder (left, right, node). Breadth-first (level order) visits the tree level by level using a queue.
Inorder on a BST yields sorted values, the most-tested fact. Preorder is used to copy or serialize a tree. Postorder is used to delete a tree or evaluate expressions bottom-up. Level order handles 'process by depth' problems.
def inorder(node, out):
if node:
inorder(node.left, out)
out.append(node.val) # BST -> sorted order
inorder(node.right, out)
return outKey point: Memorize that inorder of a BST is sorted. It shows up constantly, including 'validate a BST' and 'kth smallest element' questions.
A binary heap is a complete binary tree stored in an array where every parent is smaller than its children (min-heap) or larger (max-heap). That property means the min or max is always at the root, so peeking it is O(1), while insert and extract are O(log n).
A priority queue is the abstract idea 'always give me the highest-priority item next,' and a heap is the standard way to implement it. Python's heapq module is a min-heap. Use it for scheduling, Dijkstra's algorithm, and 'top k' problems.
import heapq
pq = []
heapq.heappush(pq, 5)
heapq.heappush(pq, 1)
heapq.heappush(pq, 3)
heapq.heappop(pq) # 1, the smallest, O(log n)Key point: For 'find the k largest elements,' a heap of size k is the expected answer: O(n log k), better than sorting everything at O(n log n).
The two standard representations are an adjacency list and an adjacency matrix. An adjacency list maps each node to its neighbors, usually a dict of lists, and uses O(V + E) space. An adjacency matrix is a V-by-V grid where cell [i][j] marks an edge, using O(V squared) space.
Prefer an adjacency list for sparse graphs (most real graphs), because it only stores edges that exist. Use a matrix when the graph is dense or when you need O(1) edge-existence checks. The representation you pick affects the Big-O of your traversal.
graph = {
"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A"],
"D": ["B"],
} # adjacency list, O(V + E) spaceKey point: The space trade-off out loud. Choosing an adjacency list for a sparse graph and explaining why is a small signal that indicates experience.
Breadth-first search (BFS) explores a graph level by level using a queue: it visits all neighbors at the current distance before going deeper. Depth-first search (DFS) goes as deep as possible down one path before backtracking, using a stack or recursion.
BFS finds the shortest path in an unweighted graph, because the first time it reaches a node is via the fewest edges. DFS uses less memory on deep graphs and naturally fits problems like cycle detection, topological sort, and exploring all paths. Both are O(V + E).
from collections import deque
def bfs(graph, start):
visited = {start}
q = deque([start])
while q:
node = q.popleft()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
q.append(nb)
return visitedHow BFS explores a graph
Swap the queue for a stack (or recursion) and the same loop becomes DFS: it goes deep before wide.
Key point: The deciding question is usually 'shortest path in an unweighted graph?'. If yes, say BFS. Naming the reason (level-by-level order) seals it.
Watch a deeper explanation
Video: Breadth First Search Algorithm | Shortest Path | Graph Theory (WilliamFiset, YouTube)
Binary search finds a target in a sorted array by repeatedly halving the search range. It checks the middle element: if it matches, done; if the target is larger, search the right half; if smaller, the left half. Each step throws away half the remaining elements.
That halving gives O(log n) time, dramatically faster than a linear O(n) scan for large inputs. The two hard requirements: the data must be sorted, and you must compute the midpoint without integer overflow (in Python that's a non-issue, but it matters in other languages).
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
if arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1 # not foundHow binary search narrows the range
Binary search only works on sorted data. On unsorted input it returns wrong answers, not just slow ones.
Key point: The off-by-one on the loop condition and the mid update is the classic bug. Use low <= high and mid +/- 1, and trace a two-element array to prove it terminates.
Watch a deeper explanation
Video: Binary Search Algorithm in 100 Seconds (Fireship, YouTube)
Recursion is when a function calls itself to solve a smaller version of the same problem. Every recursive function needs two parts: a base case that stops the recursion, and a recursive case that moves toward the base case.
Without a correct base case, recursion runs until it exhausts the call stack and raises a RecursionError. Recursion shines on naturally nested problems: tree traversals, divide-and-conquer sorts, and anything with self-similar structure. It trades clarity for the memory cost of the call stack.
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive caseKey point: Always The base case first when you explain a recursive solution. Interviewers watch specifically for whether you handle termination.
Walk the list once, reversing each node's next pointer to point at the previous node instead. Keep three references: previous (starts as None), current (starts at head), and a temporary to remember the next node before you overwrite the pointer.
It's O(n) time and O(1) space, and it's one of the most common warm-up questions in a coding round because it tests whether you can manipulate pointers carefully without losing the rest of the list.
def reverse(head):
prev = None
curr = head
while curr:
nxt = curr.next # save next before overwriting
curr.next = prev # reverse the pointer
prev = curr
curr = nxt
return prev # new headKey point: Saving the next node before reassigning the pointer is the step people forget. Say it out loud as you write it so the interviewer sees you know the trap.
Access by index and updating an element are O(1). Appending to the end of a dynamic array is amortized O(1). Inserting or deleting in the middle is O(n) because elements shift. Searching an unsorted array is O(n); a sorted array supports O(log n) binary search.
Knowing these cold lets you spot when a nested loop over an array is secretly O(n squared), which is the most common performance mistake in coding interviews.
| Operation | Time complexity |
|---|---|
| Access / update by index | O(1) |
| Append to end | O(1) amortized |
| Insert / delete in middle | O(n) |
| Search (unsorted) | O(n) |
| Search (sorted, binary) | O(log n) |
Key point: When you write a nested loop over an array, say its complexity out loud. Catching your own O(n squared) before the interviewer does is a strong signal.
A string is a sequence of characters, essentially an array you can index and slice. In many languages including Python, strings are immutable: any operation that seems to modify a string actually creates a new one.
That immutability matters for performance. Building a large string by repeatedly concatenating with += in a loop is O(n squared) because each step copies the whole string so far. Collect the pieces in a list and join once for O(n). This is a frequent gotcha interviewers plant.
parts = []
for row in rows:
parts.append(str(row))
result = "".join(parts) # O(n), one allocationKey point: The join idiom is the expected fix. Volunteering it when a string-building question comes up answers the follow-up before it's asked.
Use a hash table when your keys aren't small contiguous integers. Arrays index by position, so they're perfect for 'the 5th element,' but useless for 'the record for user email X.' A hash table maps arbitrary hashable keys to values with O(1) average access.
The other trigger is counting and grouping. Counting word frequencies, grouping items by category, or checking 'have I seen this key?' all map cleanly to a dict, where an array would force you to search.
from collections import Counter
words = ["a", "b", "a", "c", "b", "a"]
Counter(words) # {"a": 3, "b": 2, "c": 1}, one pass, O(n)Key point: Counter and defaultdict are the fluent tools here. Reaching for Counter on a frequency question instead of a manual loop indicates real-world experience.
A circular buffer (ring buffer) is a fixed-size array where the end wraps around to the beginning. Two indices track the head and tail, and when they reach the end they wrap to index 0 using modulo. It gives O(1) enqueue and dequeue in constant memory.
It's useful when you want the most recent N items and don't want memory to grow: streaming logs, audio buffers, sliding-window metrics, and producer-consumer queues. Old data is overwritten as new data arrives, which is exactly the behavior you want for a rolling window.
Key point: The key phrase is 'fixed memory, constant-time operations.' If a question mentions 'the last N events' with a memory limit, a ring buffer is the intended answer.
An abstract data type (ADT) is a specification: it defines what operations a type supports and their behavior, without saying how they're implemented. A stack ADT promises push, pop, and peek. A data The technical sequence is the concrete implementation that fulfills that contract.
A stack ADT can be built on an array or a linked list; both satisfy the contract with different performance. Separating the two is why interviewers ask 'implement a queue using two stacks': you're implementing one ADT with another structure.
Key point: This distinction sets up the 'implement X using Y' questions. Naming the ADT versus the implementation shows you think about interfaces, not just code.
A plain binary search tree gives O(log n) operations only when it stays balanced. Insert sorted data into a naive BST and it degrades into a linked list, making every operation O(n). Balanced trees prevent that.
AVL trees and red-black trees rebalance themselves after each insert or delete using rotations, guaranteeing O(log n) height no matter the input order. Red-black trees are the more common choice in libraries (they rebalance less aggressively), and they back structures like Java's TreeMap and C++'s std::map.
Key point: You rarely have to implement one from scratch in a fresher round. Knowing why they exist (guaranteed O(log n) despite adversarial input) is what's actually tested.
Space complexity measures how much extra memory an algorithm uses as the input grows, in the same Big-O terms as time. It counts memory you allocate beyond the input: recursion stack frames, a hash set of seen items, a result array.
The two often trade off. You can speed up a repeated computation by caching results (memoization), spending O(n) space to save time. Interviewers ask for both because a solution that's fast but uses O(n squared) memory can be as unusable as a slow one.
Key point: Count the recursion call stack toward space complexity. A recursive tree traversal is O(h) space for the tree's height, a detail candidates often miss.
A deque (double-ended queue, pronounced 'deck') lets you add and remove from both ends in O(1). A stack only touches one end (LIFO) and a queue adds at one end and removes at the other (FIFO). A deque generalizes both: use one end and it's a stack, use both ends the FIFO way and it's a queue.
In Python, collections.deque is the go-to for both stacks and queues because appending and popping from either end is O(1), while a plain list is O(n) to pop from the front. Deques also power sliding-window problems where you add on the right and drop stale items from the left.
from collections import deque
d = deque([2, 3])
d.appendleft(1) # [1, 2, 3], O(1) at the front
d.append(4) # [1, 2, 3, 4], O(1) at the back
d.popleft() # 1, O(1)
d.pop() # 4, O(1)Key point: The reason to reach for a deque over a list is the O(1) front operations. Naming that cost difference is what the question is checking.
For candidates with working experience: algorithm trade-offs, common patterns, and the questions that separate people who use structures from those who understand them.
All three sort in O(n log n) on average, but the trade-offs differ. Quicksort is usually the fastest in O(log n) space, but it's O(n squared) worst case and not stable is the practice path. Merge sort is always O(n log n) and stable, but needs O(n) extra space. Heap sort is always O(n log n) with O(1) space, but it's not stable and has poor cache behavior.
Pick quicksort as the default for in-memory sorting where average speed matters. Pick merge sort when you need stability or you're sorting linked lists or data too big for memory (external sort). Pick heap sort when you need guaranteed O(n log n) with minimal memory and don't need stability.
| Algorithm | Average | Worst | Space | Stable |
|---|---|---|---|---|
| Quicksort | O(n log n) | O(n squared) | O(log n) | No |
| Merge sort | O(n log n) | O(n log n) | O(n) | Yes |
| Heap sort | O(n log n) | O(n log n) | O(1) | No |
Key point: The follow-up is 'why is quicksort O(n squared) in the worst case?'. Answer: bad pivots (smallest or largest each time) on already-sorted input. Randomized or median-of-three pivots avoid it.
Watch a deeper explanation
Video: Quick sort in 4 minutes (Michael Sambol, YouTube)
A stable sort preserves the relative order of elements that compare equal. If two records have the same key, they stay in their original order after sorting. Merge sort and Python's Timsort are stable; quicksort and heap sort are not.
It matters for multi-level sorting. To sort employees by department, then by name within each department, you sort by name first, then by department with a stable sort, and the name order survives. Without stability that second sort would scramble the first. Python's sorted() and list.sort() are stable, which is why this pattern works.
rows = [("Asha", "eng"), ("Ben", "eng"), ("Cara", "ops")]
rows.sort(key=lambda r: r[0]) # by name first
rows.sort(key=lambda r: r[1]) # then by team, name order preservedKey point: The multi-level sort trick is the practical payoff of stability. Demonstrating it shows you understand why stability is a feature, not trivia.
Keep a second stack that tracks the minimum so far. Each time you push a value, also push the current minimum (the smaller of the new value and the previous minimum) onto the min-stack. Each pop removes from both. The top of the min-stack is always the current minimum.
This gives O(1) push, pop, top, and getMin, at the cost of O(n) extra space. It's a classic because the naive approach (scanning for the min on each call) is O(n), and the question needs to see you trade space for time with a second stack.
class MinStack:
def __init__(self):
self.stack = []
self.mins = []
def push(self, x):
self.stack.append(x)
self.mins.append(min(x, self.mins[-1]) if self.mins else x)
def pop(self):
self.mins.pop()
return self.stack.pop()
def get_min(self):
return self.mins[-1]Key point: The insight is storing the running minimum alongside each element, not recomputing it. If you get stuck, ask yourself 'what do I need to remember at push time?'.
Two pointers use two indices moving through a data structure, often from opposite ends or at different speeds, to solve a problem in one pass instead of nested loops. It typically turns an O(n squared) brute force into O(n).
Use it on sorted arrays for pair-sum problems (move the pointers inward based on whether the sum is too big or small), on linked lists for cycle detection (slow and fast pointers), and for in-place operations like removing duplicates. The prerequisite is often sorted input or a structure you can traverse with two coordinated positions.
def two_sum_sorted(arr, target):
lo, hi = 0, len(arr) - 1
while lo < hi:
s = arr[lo] + arr[hi]
if s == target:
return (lo, hi)
if s < target:
lo += 1 # need a bigger sum
else:
hi -= 1 # need a smaller sum
return NoneKey point: When you see 'sorted array' and 'find a pair,' think two pointers before nested loops. Naming the O(n) versus O(n squared) improvement is the point.
A sliding window maintains a contiguous range (the window) over an array or string and slides it forward, expanding and shrinking to satisfy a condition, so you compute answers for all subarrays or substrings in one O(n) pass instead of re-scanning.
Fixed-size windows solve 'maximum sum of any k consecutive elements' by adding the new element and subtracting the one that left. Variable-size windows solve 'longest substring without repeating characters' by growing the right edge and shrinking the left when the condition breaks. Both avoid the O(n squared) of recomputing each window from scratch.
def max_sum_k(arr, k):
window = sum(arr[:k])
best = window
for i in range(k, len(arr)):
window += arr[i] - arr[i - k] # slide: add new, drop old
best = max(best, window)
return bestHow a variable-size sliding window scans a string
Fixed-size windows skip the shrink step: they add the new element and drop the one k positions back on every move.
Key point: The tell is 'contiguous subarray or substring' plus a size or condition. Recognizing it and coding the add-new-drop-old update is what the question screens for.
For a directed graph, run DFS and track three states per node: unvisited, in-progress (on the current path), and done. If DFS reaches a node that's already in-progress, you've found a back edge, which means a cycle. For an undirected graph, DFS and check whether you reach a visited node that isn't the parent you came from.
The alternative for directed graphs is Kahn's algorithm (BFS-based topological sort): if you can't remove all nodes because some always have incoming edges, a cycle exists. Both are O(V + E). Cycle detection underlies dependency resolution, deadlock detection, and validating build graphs.
def has_cycle(graph):
state = {} # 0 = unvisited, 1 = in progress, 2 = done
def dfs(node):
state[node] = 1
for nb in graph.get(node, []):
if state.get(nb) == 1: # back edge
return True
if state.get(nb) != 2 and dfs(nb):
return True
state[node] = 2
return False
return any(dfs(n) for n in graph if state.get(n) != 2)Key point: The three-color (in-progress) trick is the directed-graph answer. For undirected graphs, remembering the parent check avoids a false positive on the edge you just crossed.
A topological sort orders the nodes of a directed acyclic graph (DAG) so every edge points forward: if A must come before B, A appears first. It only exists when the graph has no cycles.
Two standard approaches: Kahn's algorithm repeatedly removes nodes with no incoming edges (in-degree zero) using a queue, and DFS-based sort pushes nodes onto a stack as their DFS finishes and reverses it. Both are O(V + E). It powers build systems, task scheduling with dependencies, course prerequisites, and package managers.
from collections import deque
def topo_sort(graph, indegree):
q = deque([n for n in graph if indegree[n] == 0])
order = []
while q:
node = q.popleft()
order.append(node)
for nb in graph[node]:
indegree[nb] -= 1
if indegree[nb] == 0:
q.append(nb)
return order if len(order) == len(graph) else None # None = cycleKey point: Note that a failed topological sort (fewer nodes than the graph) proves a cycle. dual purpose.
A trie (prefix tree) stores strings by their characters: each node represents one character, and a path from the root spells a prefix. Words that share a prefix share the same path, and a flag marks where a complete word ends. Lookup and insert are O(m) where m is the word length, independent of how many words are stored.
It beats a hash table for prefix queries. Autocomplete, 'find all words starting with pre,' and spell-checking are natural on a trie because all words with a given prefix live under one subtree. A hash table can check exact membership in O(1) but can't answer 'which keys this' without scanning everything comes first.
class Trie:
def __init__(self):
self.root = {}
def insert(self, word):
node = self.root
for ch in word:
node = node.setdefault(ch, {})
node["$"] = True # end-of-word marker
def starts_with(self, prefix):
node = self.root
for ch in prefix:
if ch not in node:
return False
node = node[ch]
return TrueKey point: The winning line is 'tries answer prefix queries that hash tables can't.' If the problem says autocomplete or prefix, reach for a trie.
Watch a deeper explanation
Video: Implement Trie (Prefix Tree) - Leetcode 208 (NeetCode, YouTube)
Memoization caches the result of each subproblem the first time you compute it, so repeated calls return instantly instead of recomputing. It's the top-down form of dynamic programming: write the natural recursion, then add a cache keyed by the arguments.
Dynamic programming applies when a problem has overlapping subproblems and optimal substructure. Naive recursive Fibonacci recomputes the same values exponentially; memoizing it makes each value compute once, turning O(2^n) into O(n). The bottom-up (tabulation) form fills a table iteratively instead of recursing.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2) # each value computed onceKey point: Say the two conditions for DP: overlapping subproblems and optimal substructure. Naming them shows you know when DP applies, not just how to memoize.
Naive recursion is O(2^n) time because it recomputes the same subproblems exponentially, and it can blow the stack. Memoized (top-down) recursion caches each value for O(n) time and O(n) space. Bottom-up tabulation fills an array iteratively, also O(n) time, and can drop to O(1) space by keeping only the last two values.
This is a favorite because it demonstrates the whole DP progression on one problem: recognize the overlapping subproblems, add a cache, then optimize the space. The O(1)-space iterative version is the answer that closes the loop.
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b # O(n) time, O(1) space
return aKey point: End on the O(1)-space iterative version. Showing you can optimize memory after getting the O(n) time is what pushes this from a pass to a strong signal.
For the minimum-coins version, use bottom-up DP. Build an array dp where dp[amount] is the fewest coins to make that amount. Start dp[0] = 0, and for each amount from 1 up to the target, try every coin and take the best: dp[a] = min over coins c of dp[a - c] + 1. It's O(amount * number of coins).
The greedy 'take the largest coin first' approach fails for arbitrary coin sets (it works for standard currency by design, but not for something like coins of 1, 3, and 4 making 6). Interviewers use this specifically to see whether you reach for DP over a tempting but wrong greedy.
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1Key point: Mention that greedy fails on non-standard coin sets. Explaining why DP is needed instead of greedy is the exact reasoning this problem tests.
Use recursion when the problem is naturally self-similar: tree and graph traversal, divide-and-conquer, and backtracking all read cleanly as recursion. Use iteration when the problem is a simple sequential process, or when recursion depth would risk a stack overflow.
The trade-off is clarity versus the call-stack cost. Recursion uses O(depth) stack space and, in languages without tail-call optimization (Python included), can hit a recursion limit on deep inputs. Any recursion can be rewritten as iteration with an explicit stack, which is exactly what you do when depth is a concern.
# recursive DFS
def dfs(node, visited):
visited.add(node)
for nb in graph[node]:
if nb not in visited:
dfs(nb, visited)
# iterative DFS with an explicit stack (no recursion limit)
def dfs_iter(start):
stack, visited = [start], set()
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
stack.extend(graph[node])Key point: Knowing that any recursion converts to iteration with an explicit stack is the mature answer. It's the fix when an interviewer says 'the input can be a million deep.'
Use Floyd's tortoise-and-hare algorithm: two pointers, one moving one node at a time (slow) and one moving two (fast). If there's a cycle, the fast pointer eventually laps the slow one and they meet. If the fast pointer hits the end (None), there's no cycle.
It's O(n) time and O(1) space, better than the naive approach of storing every visited node in a set, which is O(n) space. The same two-speed idea also finds the cycle's start and the middle of a list, which is why interviewers like it.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return FalseKey point: The O(1)-space fast-slow trick is the expected answer. the naive O(n)-space set version first, then improving it, matters.
Choose a hash table when you only need exact key lookup, insert, and delete, and you want O(1) average. Choose a balanced BST when you need the data kept in sorted order or you need range queries, ordered iteration, or the nearest key to a value.
A hash table can't answer 'give me all keys between 10 and 50' or 'what's the smallest key larger than x' without scanning everything. A balanced BST (or a structure like a skip list) does those in O(log n) because it maintains order. The trade is O(log n) operations versus the hash table's O(1) average.
| Need | Hash table | Balanced BST |
|---|---|---|
| Exact key lookup | O(1) avg | O(log n) |
| Sorted iteration | Not supported | O(n) |
| Range query | Not supported | O(log n + k) |
| Nearest / floor / ceiling key | Not supported | O(log n) |
Key point: The deciding question is 'do you need order or range queries?'. If yes, BST; if no, hash table. Framing it as an ordering trade-off is the clean answer.
An in-place algorithm transforms its input using O(1) extra space (or O(log n) for the recursion stack), modifying the data directly instead of allocating a separate copy. Reversing an array by swapping ends inward is in-place; building a new reversed array is not.
It matters when memory is tight or the data is huge. In-place quicksort sorts without a second array; the two-pointer 'remove duplicates from a sorted array' runs in O(1) space. Interviewers often add 'do it in-place' as a constraint precisely to see whether you can avoid the easy extra-space solution.
Key point: When a problem says 'in-place' or 'O(1) extra space,' it's steering you toward swaps and pointers rather than a new data structure. Treat it as a strong hint.
For non-negative weights, use Dijkstra's algorithm: keep a min-heap (priority queue) of nodes by their tentative distance, repeatedly pop the closest unfinished node, and relax its neighbors. With a heap it's O((V + E) log V). For unweighted graphs, plain BFS already gives the shortest path in O(V + E).
If the graph has negative edge weights, Dijkstra breaks; use Bellman-Ford (O(V * E)), which also detects negative cycles. For all-pairs shortest paths on a small dense graph, Floyd-Warshall is O(V cubed). Naming the right algorithm for the weight condition is the whole point of the question.
import heapq
def dijkstra(graph, start):
dist = {start: 0}
pq = [(0, start)]
while pq:
d, node = heapq.heappop(pq)
if d > dist.get(node, float('inf')):
continue
for nb, w in graph[node]:
nd = d + w
if nd < dist.get(nb, float('inf')):
dist[nb] = nd
heapq.heappush(pq, (nd, nb))
return distKey point: Match the algorithm to the weights: unweighted is BFS, non-negative is Dijkstra, negative is Bellman-Ford. Stating that mapping in practice is exactly what the question checks.
Amortized analysis measures the average cost of an operation over a sequence, so occasional expensive operations are spread across many cheap ones. It's not the same as average-case (which is about random inputs); amortized is a guarantee over any sequence.
The classic example is appending to a dynamic array. Most appends are O(1), but when the array fills, it doubles its capacity and copies everything, an O(n) operation. Because doublings are rare and each copies roughly what all the cheap appends since the last doubling added, the amortized cost per append is O(1).
Key point: Use the dynamic-array doubling example and the word 'amortized O(1).' It's the cleanest demonstration and the one the question expects to hear.
Combine a hash table with a doubly linked list. The hash table maps keys to nodes for O(1) lookup. The doubly linked list keeps nodes in usage order: most-recently-used at the front, least-recently-used at the back. On access, move the node to the front; on insert past capacity, evict the tail.
Both get and put are O(1) because the hash table finds the node instantly and the doubly linked list lets you unlink and re-insert in constant time. In Python, collections.OrderedDict (or a plain dict since 3.7, using move_to_end) gives this for free, which is worth mentioning after you've shown you know the underlying design.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.cap = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark recently used
return self.cache[key]
def put(self, key, value):
self.cache[key] = value
self.cache.move_to_end(key)
if len(self.cache) > self.cap:
self.cache.popitem(last=False) # evict LRUKey point: Describe the hash-table-plus-doubly-linked-list design first, then mention OrderedDict as the shortcut. Leading with the design proves you understand why it's O(1).
The reliable way is to pass down a valid range for each node. The root can be anything; a left child must be less than its parent and within the parent's lower bound; a right child must be greater and within the upper bound. Recurse with tightening bounds and check each node falls inside its range.
The common wrong answer only compares each node to its immediate children, which misses violations deeper in the tree. An alternative correct method is an inorder traversal: a valid BST produces strictly increasing values, so you check that each visited value is greater than the previous one.
def is_valid_bst(node, low=float('-inf'), high=float('inf')):
if node is None:
return True
if not (low < node.val < high):
return False
return (is_valid_bst(node.left, low, node.val) and
is_valid_bst(node.right, node.val, high))Key point: Call out the trap: checking only parent-child pairs is wrong. The min/max bounds approach or the inorder-is-increasing property is what makes it correct.
Backtracking builds a solution incrementally and abandons a partial candidate the moment it can't lead to a valid answer, then undoes the last choice and tries another. It's a smarter brute force that prunes dead branches instead of enumerating every full combination.
It fits constraint problems: generating permutations and subsets, solving Sudoku or N-queens, and word searches. The The technical sequence is a recursive 'choose, explore, un-choose' loop. Pruning early is what makes it feasible where naive brute force would be hopelessly slow.
def permutations(nums):
result = []
def backtrack(path, remaining):
if not remaining:
result.append(path[:])
return
for i in range(len(remaining)):
path.append(remaining[i]) # choose
backtrack(path, remaining[:i] + remaining[i+1:]) # explore
path.pop() # un-choose
backtrack([], nums)
return resultKey point: The 'choose, explore, un-choose' skeleton is the pattern to internalize. If you can write that loop from memory, most backtracking problems become fill-in-the-blank.
The building blocks: x & 1 tests the lowest bit (odd/even), x << k multiplies by 2^k, x >> k divides, x & (x - 1) clears the lowest set bit, and XOR (^) toggles bits. XOR has a useful property: a ^ a is 0 and a ^ 0 is a.
That XOR property solves the 'find the single number where every other appears twice' problem in O(n) time and O(1) space: XOR all numbers and the pairs cancel, leaving the unique one. Bit manipulation shows up in problems about sets of flags, counting set bits, and space-tight uniqueness checks.
def single_number(nums):
result = 0
for n in nums:
result ^= n # pairs cancel, unique survives
return resultKey point: The XOR-cancels-pairs trick is the one interviewers love. Explaining why (a ^ a == 0) matters more than reciting the operator.
A prefix sum array stores, at each index, the running total of all elements up to that point. Once built in O(n), it answers 'what's the sum of the range from i to j?' in O(1) by subtracting prefix[i] from prefix[j+1], instead of re-adding the range each time.
It turns repeated range-sum queries from O(n) each into O(1) each, a big win when you have many queries on a static array. The same idea extends to 2D grids for rectangle sums, and pairs with a hash table to solve 'count subarrays that sum to k' in one pass.
def build_prefix(arr):
prefix = [0]
for x in arr:
prefix.append(prefix[-1] + x)
return prefix
p = build_prefix([2, 4, 6, 8])
p[3] - p[1] # sum of indices 1..2 = 4 + 6 = 10Key point: The trade to state: prefix sums give O(1) range queries but O(n) to update, so they fit static arrays. If updates are frequent, A Fenwick or segment tree instead matters.
advanced rounds probe design judgment, scale, and the trade-offs behind the structures. Expect every answer to draw a follow-up.
Start from the access pattern, not the data. Profile which operation dominates (read-heavy, write-heavy, range scans, point lookups) and its required latency, then pick the structure whose Big-O matches, and only then consider memory and cache behavior. A structure that's theoretically optimal but cache-hostile can lose to a simpler contiguous one.
At scale the answer often stops being a single in-memory structure and becomes a system: a hash index for point lookups, a B-tree or LSM-tree for on-disk sorted access (why databases use them), a bloom filter to skip disk reads for absent keys. The production signal is reasoning from workload to structure, and knowing when the structure lives on disk, not just in RAM.
Key point: Lead with 'what's the dominant operation and its latency budget?'. Reasoning from the workload rather than naming a favorite The technical sequence is what separates production-ready answers.
A B-tree is a balanced tree where each node holds many keys and has many children, keeping the tree very shallow. Databases store data on disk, and disk reads happen in blocks (pages). A B-tree node is sized to one page, so a single disk read loads many keys at once, minimizing the number of slow disk seeks.
A binary search tree has only two children per node, so its height is much larger and every step down the tree could be a separate disk read. For n keys, a B-tree of order m has height around log_m(n) versus log_2(n) for a BST, a big difference in disk I/O. The whole design optimizes for the fact that disk access dominates cost.
Key point: The core insight is 'minimize disk seeks by packing many keys per node.' Tying tree shape to the block-based nature of disk I/O is the answer that indicates database experience.
A bloom filter is a space-efficient probabilistic structure that answers 'is this element possibly in the set?' with two possible replies: definitely not, or probably yes. It uses a bit array and several hash functions; adding an element sets several bits, and a query checks whether all those bits are set.
The trade-off: it can produce false positives (says 'probably yes' when the element is absent) but never false negatives. That's acceptable when a false positive just triggers a cheap fallback check. Uses include skipping disk lookups for keys that definitely aren't stored, deduplicating a massive stream, and CDN cache checks. You spend a little accuracy to save enormous memory.
Key point: The one-liner is 'no false negatives, occasional false positives, tiny memory.' Naming a real use (skip a disk read for an absent key) grounds it.
Consistent hashing maps both keys and servers onto a ring (a hash space). A key belongs to the first server clockwise from its position. When a server is added or removed, only the keys in its arc move; the rest stay put.
It solves the rehashing storm of naive hash-mod-N sharding. With hash(key) % N, changing the number of servers remaps almost every key, which is catastrophic for a distributed cache or database. Consistent hashing limits movement to roughly 1/N of keys per change, and virtual nodes smooth out the load distribution. It's foundational to systems like distributed caches and sharded stores.
Key point: Contrast it directly with hash-mod-N to show the payoff: naive sharding remaps everything on a resize, consistent hashing moves only a fraction. That contrast is the point.
Union-find tracks a collection of disjoint sets and supports two operations: find (which set is this element in?) and union (merge two sets). It's built as a forest where each element points toward its set's representative root.
Two optimizations make it near-constant time: path compression flattens the tree during find, and union by rank attaches the smaller tree under the larger. Together they give nearly O(1) amortized (inverse Ackermann) per operation. It powers Kruskal's minimum spanning tree, detecting whether adding an edge creates a cycle, and grouping connected components, like counting islands or friend circles.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # path compression
x = self.parent[x]
return x
def union(self, a, b):
self.parent[self.find(a)] = self.find(b)Key point: If a problem involves 'connected components,' 'grouping,' or 'does this edge close a cycle,' union-find is often the intended tool. Naming path compression shows you know why it's fast.
A log-structured merge-tree buffers writes in memory (a memtable, often a balanced tree or skip list), then flushes them as sorted immutable files (SSTables) to disk. Background compaction merges those files to keep read cost bounded and reclaim space from overwritten or deleted keys.
It's chosen for write-heavy workloads because writes are sequential appends, which disks and SSDs handle far faster than the random writes a B-tree makes when updating pages in place. The cost is slower reads (a key might live in several SSTables) and write amplification from compaction, which bloom filters and tiered compaction mitigate. It backs many modern stores; the B-tree-versus-LSM choice is a read-optimized-versus-write-optimized trade.
Key point: Frame it as the write-optimized counterpart to the read-optimized B-tree. Naming sequential writes as the win and read amplification as the cost is the balanced production-ready answer.
A skip list is a layered linked list: the bottom layer holds all elements in order, and higher layers hold progressively sparser 'express lanes' that let you skip ahead. Searching drops down levels, giving O(log n) expected search, insert, and delete, the same as a balanced tree.
The appeal is simplicity and concurrency. Skip lists are far easier to implement correctly than a red-black tree, and they lend themselves to lock-free concurrent implementations because operations touch localized pointers. That's why Redis uses them for sorted sets and Java's ConcurrentSkipListMap exists. The trade is probabilistic balance (based on random levels) rather than a strict guarantee.
Key point: The two selling points are simpler implementation and easier concurrency than balanced trees. Citing Redis sorted sets as a real user makes the answer concrete.
Modern CPUs are far faster than main memory, so they cache recently and nearby data in lines of about 64 bytes. Structures with contiguous memory (arrays, vectors) get many useful elements per cache line and iterate fast. Pointer-chasing structures (linked lists, node-based trees) scatter data, causing cache misses that can dominate runtime.
This is why an O(n) linear scan over an array often beats an O(log n) search over a scattered tree for small-to-medium n, and why a hash table with open addressing can outperform one with chaining. The senior point: Big-O ignores constants, but cache misses are a huge constant, so on real hardware the memory layout can matter as much as the asymptotic complexity.
Key point: The nuance the technical value is is 'Big-O hides constants, and cache misses are a giant constant.' Giving a case where the worse Big-O wins in practice shows depth.
The core difficulty is that operations which look atomic (read-modify-write) aren't, so two threads can interleave and corrupt shared state or see torn reads. Coarse locking (one lock for the whole structure) is correct but kills throughput; fine-grained locking (per-node or per-bucket) scales better but invites deadlock and is hard to reason about.
Lock-free structures use atomic compare-and-swap to make progress without locks, avoiding deadlock but battling the ABA problem and subtle memory-ordering rules. The pragmatic answer for most systems: reach for battle-tested concurrent structures from the standard library (concurrent maps, queues) before hand-rolling one, because correct lock-free code is genuinely expert territory.
Key point: The mature closing line is 'use proven library structures before writing lock-free code.' It signals you respect how easy concurrency is to get subtly wrong.
A count-min sketch is a probabilistic structure that estimates the frequency of elements in a stream using sublinear memory. It's a 2D array of counters with several hash functions; each element increments one counter per row, and a query returns the minimum across those rows to reduce overcounting.
You reach for it when exact counts are too memory-expensive: finding heavy hitters in a huge traffic stream, tracking approximate frequencies for rate limiting, or trending-item detection. Like a bloom filter, it trades accuracy for space, it can overestimate but never underestimate, which is acceptable when an approximate top-k is enough.
Key point: Group it mentally with bloom filters as a probabilistic streaming structure: trade exactness for tiny memory. 'heavy hitters' or 'approximate top-k' matters.
A persistent data structure preserves previous versions when modified: updating it returns a new version while the old one stays valid and unchanged. Naively that means copying everything, which is too slow. The trick is structural sharing: the new version reuses most of the old one and only creates the nodes along the path that changed.
A persistent tree update copies just the O(log n) nodes from the changed leaf to the root and shares the rest. This backs functional languages' immutable collections, version control's history, undo systems, and Clojure's data structures. The payoff is cheap versioning and thread-safety by default, because nothing is ever mutated in place.
Key point: Structural sharing is the key phrase: only the changed path is copied, the rest is reused. That's what makes immutability O(log n) instead of O(n) per update.
Use external merge sort. Read the data in chunks that fit in memory, sort each chunk, and write it back to disk as a sorted run. Then merge the sorted runs together with a k-way merge using a min-heap, reading a little from each run at a time and writing the merged output sequentially.
It's O(n log n) comparisons like in-memory sorting, but the design minimizes disk I/O by doing sequential reads and writes rather than random access. The number of merge passes depends on how many runs fit in memory at once. This is how databases and big-data tools sort terabytes on machines with gigabytes of RAM.
import heapq
def k_way_merge(sorted_runs):
# sorted_runs: list of already-sorted iterables (disk chunks)
return list(heapq.merge(*sorted_runs)) # min-heap merge, streamingKey point: The The technical sequence is 'sort chunks that fit, then k-way merge with a heap.' Emphasizing sequential I/O over random access is what marks the answer as production-aware.
First, store it compactly: an adjacency list (or compressed formats like CSR, compressed sparse row) rather than a matrix, since real graphs are sparse. If it still won't fit, partition it across machines and process with a vertex-centric model (like Pregel or GraphX) where each vertex computes based on messages from its neighbors, iterating in supersteps.
The design pressure is minimizing cross-partition edges, because messages crossing machines are the bottleneck, so graph partitioning quality drives performance. For streaming-scale problems you may accept approximate answers (sampling, sketches) instead of exact traversals. The production signal is recognizing that at scale the algorithm choice bends around the memory and network layout, not the other way around.
Key point: The escalation ladder is: compact representation, then partition, then approximate. Naming CSR and vertex-centric processing shows you've thought past in-memory graphs.
A rolling hash computes the hash of a fixed-size window over a sequence, and when the window slides by one, updates the hash in O(1) by removing the outgoing element's contribution and adding the incoming one, instead of rehashing the whole window.
It powers the Rabin-Karp string search: to find a pattern in text, hash the pattern once and roll a same-size hash across the text, comparing full strings only when hashes match. Average O(n + m). Rolling hashes also drive content-defined chunking in deduplication and rsync-style diffing. The trade is handling hash collisions with a verification step.
Key point: The O(1) window update is the whole trick. Mentioning Rabin-Karp and that you verify on a hash match (to guard against collisions) rounds out a strong answer.
A monotonic stack keeps its elements in sorted order (increasing or decreasing) by popping any element that would violate the order before pushing a new one. Because each element is pushed and popped at most once, a whole class of problems drops from O(n squared) to O(n).
It solves 'next greater element,' 'largest rectangle in a histogram,' 'daily temperatures,' and 'trapping rain water.' The pattern: as you scan, the stack holds candidates still waiting for their answer, and you resolve them the moment a qualifying element arrives. Recognizing that a problem asks for the nearest larger or smaller element in a sequence is the cue to reach for it.
def next_greater(nums):
result = [-1] * len(nums)
stack = [] # holds indices, values decreasing
for i, n in enumerate(nums):
while stack and nums[stack[-1]] < n:
result[stack.pop()] = n
stack.append(i)
return resultKey point: The recognition cue is 'nearest greater or smaller element.' If you spot that, a monotonic stack turns an O(n squared) scan into O(n), which is the whole reason the pattern exists.
A segment tree is a binary tree over an array where each node stores an aggregate (sum, min, max) of a range, so you can query the aggregate of any range and update any element, both in O(log n). Building it is O(n).
It's worth it when you have many interleaved range queries and point updates on the same array. A prefix-sum array answers range sums in O(1) but can't handle updates without an O(n) rebuild; a segment tree keeps both operations O(log n). Fenwick trees (binary indexed trees) do the same for simpler aggregates with less code and memory, so mention them as the lighter alternative when the aggregate is a sum.
Key point: The trigger is 'range queries plus updates.' Contrasting it with prefix sums (fast query but O(n) update) shows you know exactly when the extra structure earns its cost.
Clarify first: how many queries per second, do results need ranking (by popularity), is the dataset static or updated live, and what latency budget? Those answers change the design. Then propose: a trie or compressed trie (radix tree) for prefix matching, with each node caching its top-k completions so a lookup returns ranked results without traversing the whole subtree.
The closing step is scale and operations: the trie can be sharded by prefix across machines, popularity scores feed the ranking, and for fuzzy matching you'd add edit-distance handling or an n-gram index. The technical sequence, clarify then core structure then ranking then scale, is what a advanced round is really scoring, not any single data structure.
Key point: This is a system-design question in a data-structures costume. Clarifying requirements before naming a structure, and caching top-k at each trie node, is the pair of moves that lands it.
Amortized O(1) means the average cost over many operations is constant, but individual operations can spike. A dynamic array's occasional doubling copies the whole array in one O(n) step, and a hash table's resize rehashes everything at once. That's fine for throughput but bad for latency-sensitive systems, where one request unluckily eats the full resize and blows the tail latency (p99).
The fix is to spread the expensive work out. Incremental resizing rehashes a few entries per operation instead of all at once, keeping every operation bounded, which is what low-latency stores and real-time systems do. The production insight is that amortized analysis optimizes total work, but tail latency cares about the worst single operation, so the two goals can conflict.
Key point: The distinction to draw is amortized cost versus worst-case per-operation latency. Naming p99 tail latency and incremental resizing as the fix is what signals real production experience.
Picking a The technical sequence is a trade-off between how fast each operation runs and how much memory it costs. Arrays give O(1) access by index but O(n) insertion in the middle. Linked lists flip that: O(1) insertion at a known node but O(n) to find anything. Hash tables give O(1) average search and insert but no ordering and worst-case O(n) under bad collisions. Balanced trees keep data sorted with O(log n) across the board. Interviewers ask you to reason through this table out loud, because naming the operation your problem does most often is how you land on the right structure. The complexities below are average or expected cost; note where the worst case differs.
| Structure | Access | Insert | Search | Best at |
|---|---|---|---|---|
| Array (dynamic) | O(1) | O(n) | O(n) | Index access, cache-friendly iteration |
| Linked list | O(n) | O(1) at node | O(n) | Frequent insert/delete at known positions |
| Hash table | N/A | O(1) avg | O(1) avg | Key lookup, deduplication, counting |
| Balanced BST | O(log n) | O(log n) | O(log n) | Sorted data with fast search and range queries |
| Heap (binary) | O(1) peek | O(log n) | O(n) | Repeatedly getting the min or max |
Prepare in layers, and practice out loud. Most DSA rounds move from a concept check to a live coding problem where the interviewer watches you think. Rehearse the coding-round approach as its own skill: the candidates who fail rarely fail on knowledge, they fail by diving into code before understanding the problem.
The coding-round approach the question needs to see
Earlier rounds increasingly run as AI coding interviews. This clarify-examples-approach-code-test loop is exactly what automated evaluation indicates strong problem-solving.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, where data structures and algorithms are the core of what gets evaluated. These questions reflect what actually gets asked inside the DSA interviews we host for 5,000+ HR teams.
See how the AI Coding Interviewer works