Data Structures & Algorithms interview questions & answers
The coding-round fundamentals: complexity, arrays and hashing, linked lists, stacks and queues, trees, graphs, sorting, and the common problem-solving patterns.
Official reference: MIT 6.006 Introduction to Algorithms
57 Data Structures & Algorithms interview questions study by subtopic
1.What is Big-O notation, and how do you work out the time complexity of a piece of code?easy
Big-O describes how an algorithm's running time (or memory) grows as the input size
ngrows. It is an upper bound on the growth rate, so we drop constants and lower-order terms:3n² + 5n + 2isO(n²).To analyze code, I count how many times the dominant operation runs as a function of
n:- Sequential blocks add:
O(n) + O(n²)isO(n²) - Nested loops multiply: a loop over
ninside a loop overnisO(n²) - Halving or doubling the problem each step gives
O(log n) - Recursion: write a recurrence, or draw the recursion tree and sum the work per level
- Separate inputs keep separate variables:
O(a + b)orO(a · b), notO(n²)
I also watch for hidden linear costs inside a single line, such as
x in some_list, slicing, or copying a list.The common classes, fastest to slowest:
O(1),O(log n),O(n),O(n log n),O(n²),O(2ⁿ),O(n!).What interviewers listen for- Upper bound on growth as input size grows
- Drop constants and lower-order terms
- Sequential steps add; nested loops multiply
- Halving the input each step gives
O(log n) - Watch hidden costs like
inon a list
Likely follow-up: What is the complexity of a loop where
jdoubles each step, nested inside a loop overn? · How do you analyze the complexity of a recursive function?- Sequential blocks add:
2.Given an array of integers and a target, return the indices of two numbers that add up to the target (Two Sum). What is your approach?easy
The brute force checks every pair:
O(n²)time,O(1)space. That is my baseline, but the bottleneck is the inner search for the complement, so I replace it with a hash map.I walk the array once. For each
xat indexi, the number I need istarget - x. If it is already in the map, I have my pair; otherwise I storex -> iand move on. Checking before inserting means an element is never paired with itself, and duplicates like[3, 3]with target6still work.That is
O(n)time andO(n)extra space, relying on average-caseO(1)hash lookups.If the array were already sorted, I would use two pointers from both ends instead:
O(n)time andO(1)space. Sorting first costsO(n log n)and loses the original indices unless I sort (value, index) pairs.def two_sum(nums, target): seen = {} # value -> index for i, x in enumerate(nums): if target - x in seen: return [seen[target - x], i] seen[x] = i # insert after checking return []What interviewers listen for- Brute force over all pairs is
O(n²) - Hash map from value to index, single pass
- Check for the complement before inserting
O(n)time,O(n)space on average- Sorted input: two pointers with
O(1)space
Likely follow-up: How would you solve 3Sum? · What changes if you must return all unique pairs?
- Brute force over all pairs is
3.What are the differences between an array and a linked list, and when would you choose each?easy
An array stores elements in one contiguous block, so index access is
O(1)by address arithmetic and iteration is very cache-friendly. The cost is that inserting or deleting in the middle shifts elements,O(n), and a fixed-size array must be reallocated to grow (dynamic arrays hide this behind amortizedO(1)appends).A linked list stores nodes scattered in memory, each pointing to the next (and to the previous, if doubly linked). Accessing the i-th element or searching is
O(n), but once you hold a reference to a node, inserting or removing next to it isO(1)with no shifting. Each node carries pointer overhead, and traversal has poor cache locality.In practice I default to a dynamic array (Python
list, JavaArrayList) because locality usually wins. I pick a linked list when I needO(1)insert and remove at positions I already hold, like the doubly linked list inside an LRU cache, or when splicing lists together.What interviewers listen for- Array: contiguous,
O(1)index access, cache-friendly - Array middle insert/delete is
O(n)from shifting - Linked list:
O(n)access,O(1)insert at a known node - Linked lists pay pointer overhead and poor locality
- Default to dynamic arrays; lists for LRU-style splicing
Likely follow-up: Why is linked-list insertion "O(1)" often misleading in practice? · How would you find the middle of a linked list in one pass?
- Array: contiguous,
4.How does a hash table work, and why are its lookups
O(1)on average?easyA hash table stores key-value pairs in an array of buckets. To insert or find a key, it runs the key through a hash function to get an integer, then maps that to a bucket index, typically
hash(key) % capacity. Jumping straight to the bucket instead of scanning is what makes it fast.Two keys can land in the same bucket, a collision, handled by chaining (each bucket holds a small list) or open addressing (probe for another free slot). The table tracks its load factor, entries divided by buckets, and past a threshold it allocates a bigger array and rehashes everything. That resize is
O(n)but rare, so inserts stay amortizedO(1).With a good hash function and a bounded load factor, buckets stay small on average, so get, put and delete are
O(1)average. The worst case isO(n)when many keys collide.Keys must be hashable and stable: equal keys need equal hashes, and mutating a key after inserting it breaks lookups.
What interviewers listen for- Hash function maps a key to a bucket index
- Collisions: chaining or open addressing
- Resize and rehash when the load factor passes a threshold
O(1)average,O(n)worst case- Equal keys must produce equal hashes
Likely follow-up: What is the difference between chaining and open addressing? · Why shouldn't you use a mutable object as a dictionary key?
5.Walk me through how you approach a coding problem in an interview.easy
I follow the same loop every time and talk through each step:
- Clarify: restate the problem; ask about input size, value ranges, duplicates, negatives, empty input, and what to return when there is no answer.
- Examples: work one normal case and a couple of edge cases by hand, which often reveals the pattern.
- Brute force: state the obvious solution and its complexity, so there is a correct baseline.
- Optimize: find the bottleneck and reach for a pattern: hash map, sorting, two pointers, sliding window, heap, BFS/DFS, binary search or DP. Constraints hint at the target:
naround10⁵usually needsO(n log n)or better, whilen ≤ 20allows exponential search. - Code: write clean code with clear names, narrating the key decisions.
- Test: dry-run my example, then edge cases, and fix bugs calmly.
- Complexity: state the final time and space, plus trade-offs or what I would improve with more time.
What interviewers listen for- Clarify inputs, constraints and edge cases first
- Brute force with its complexity as a baseline
- Optimize by finding the bottleneck and a pattern
- Use constraints to infer the target complexity
- Dry-run tests, then state time and space
Likely follow-up: What do you do if you get stuck on the optimization step? · How do you choose which edge cases to test?
6.How do you reverse a singly linked list? Can you do it both iteratively and recursively?easy
Iteratively, I walk the list with three references:
prev(the already-reversed part, starting atNone),curr, and a savednxt. For each node I savecurr.next, pointcurr.nextback atprev, then advance both. WhencurrisNone,previs the new head. That isO(n)time andO(1)extra space. The classic bug is overwritingcurr.nextbefore saving it, which loses the rest of the list.Recursively, I first reverse everything after the head, which returns the new head (the old tail). Now
head.nextis the last node of the reversed part, sohead.next.next = headappends the current node, andhead.next = Nonecuts the old link. It is alsoO(n)time but usesO(n)call-stack space, so a long list can hit Python's recursion limit. I prefer the iterative version in real code.def reverse_list(head): prev, curr = None, head while curr: nxt = curr.next # save the rest first curr.next = prev # flip the pointer prev, curr = curr, nxt return prev # new head def reverse_rec(head): if head is None or head.next is None: return head new_head = reverse_rec(head.next) head.next.next = head # hang head after its old successor head.next = None return new_headWhat interviewers listen for- Track
prevandcurr; savenextbefore relinking - Iterative:
O(n)time,O(1)space - Recursive:
head.next.next = head, thenhead.next = None - Recursive version uses
O(n)stack space - Handle empty and single-node lists
Likely follow-up: How would you reverse only the nodes between positions
mandn? · How would you reverse a list in groups ofk?- Track
7.Given a string of brackets like
"([]{})", how do you check whether it is valid?easyBrackets must close in the reverse order they opened, which is exactly last-in, first-out, so I use a stack.
I scan left to right and push every opening bracket. For a closing bracket, the stack must be non-empty and its top must be the matching opener; if so I pop it, otherwise the string is invalid right away. At the end the string is valid only if the stack is empty, because any leftover openers were never closed.
A map from each closer to its opener keeps the code short. It is
O(n)time andO(n)space in the worst case, such as a string of only opening brackets.Edge cases I test: the empty string (valid), a lone closer
")", interleaving like"([)]"(invalid) and"(("(invalid). With only one bracket type, a counter that never goes negative and ends at zero is enough, inO(1)space.def is_valid(s): pairs = {')': '(', ']': '[', '}': '{'} stack = [] for ch in s: if ch in pairs: # closing bracket if not stack or stack.pop() != pairs[ch]: return False else: stack.append(ch) # opening bracket return not stack # nothing left unclosedWhat interviewers listen for- Last-opened must close first, so use a stack
- Push openers; a closer must match and pop the top
- Valid only if the stack ends empty
O(n)time,O(n)space- One bracket type: a counter is enough
Likely follow-up: How would you find the longest valid parentheses substring? · How would you generate all valid combinations of
npairs?8.What is the difference between a stack, a queue and a deque, and how would you implement each in Python?easy
A stack is last-in, first-out: you push and pop at the same end. It models function calls, undo, DFS and bracket matching. A Python
listworks well, sinceappendandpop()at the end are amortizedO(1).A queue is first-in, first-out: you add at the back and remove from the front. It models scheduling, buffering and BFS. I use
collections.dequewithappendandpopleft, bothO(1). I avoidlist.pop(0)because it shifts every remaining element, which isO(n).A deque (double-ended queue) supports
O(1)insert and remove at both ends, so it can act as either. It is the tool behind sliding-window maximum and 0-1 BFS.For thread-safe producer-consumer queues Python has
queue.Queue. A priority queue is a different structure again, usually a binary heap viaheapq.What interviewers listen for- Stack is LIFO:
list.appendandlist.pop() - Queue is FIFO:
deque.appendanddeque.popleft() list.pop(0)isO(n); avoid it for queues- Deque:
O(1)operations at both ends - Stacks drive DFS; queues drive BFS
Likely follow-up: How would you implement a queue using two stacks? · How would you design a stack that returns its minimum in
O(1)?- Stack is LIFO:
9.What is the difference between BFS and DFS, and when would you use each?easy
Both visit every reachable vertex and edge once, so both are
O(V + E)time with an adjacency list. They differ in the order they explore.BFS (breadth-first search) uses a queue and explores level by level: every node at distance 1, then distance 2, and so on. That makes it the right choice for the shortest path in an unweighted graph, the minimum number of moves, and level-order tree traversal. Its memory grows with the widest frontier.
DFS (depth-first search) uses a stack or recursion and goes as deep as possible before backtracking. It suits exhaustive exploration: connected components, cycle detection, topological sort, path finding and backtracking. Its memory grows with the depth, and deep recursion can overflow the call stack, so for very deep graphs I use an explicit stack.
In both, I mark a node visited when I first discover it (for BFS, when I enqueue it) so it is never processed twice.
What interviewers listen for- Both
O(V + E)with an adjacency list - BFS: queue, level by level, unweighted shortest paths
- DFS: stack or recursion, goes deep, then backtracks
- DFS suits cycle detection, topo sort, components
- Mark visited on discovery to avoid repeats
Likely follow-up: Why does BFS give shortest paths only when edges are unweighted? · How would you run BFS on a grid with obstacles?
- Both
10.How do you detect whether a linked list has a cycle, ideally in
O(1)extra space?easyThe simple approach stores every visited node in a hash set and reports a cycle when it sees a node twice:
O(n)time but alsoO(n)space.For
O(1)space I use Floyd's tortoise and hare. Two pointers start at the head;slowmoves one step andfastmoves two. Without a cycle,fastreaches the end. With a cycle, both end up circling it, and each step the gap between them shrinks by one, sofastmust land onslowwithin about one lap. It isO(n)time.The loop condition
fast and fast.nextprotects both hops of the fast pointer. I compare nodes by identity (is), not by value, since values can repeat.To find where the cycle starts, after they meet I move one pointer back to the head and advance both one step at a time; they meet again at the cycle's entry node.
def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: # compare nodes, not values return True return FalseWhat interviewers listen for- A hash set works but costs
O(n)space - Slow moves one step, fast moves two
- They meet if and only if there is a cycle
- Guard the loop with
fast and fast.next - Reset one pointer to head to find the entry
Likely follow-up: Why does resetting one pointer to the head find the cycle start? · How would you compute the length of the cycle?
- A hash set works but costs
11.How does binary search work, and what are the common bugs when implementing it?easy
Binary search finds a target in a sorted array by repeatedly halving the search range. I keep
loandhibounds, look at the middle element, and discard the half that cannot contain the target. Each step halves the range, so it runs inO(log n)time andO(1)space when written iteratively.The common bugs:
- Off-by-one boundaries: decide whether
hiis inclusive (len - 1, loop whilelo <= hi) or exclusive (len, loop whilelo < hi), and keep every update consistent with that choice. - Infinite loops: writing
lo = midwheremid + 1is needed can stop the range from shrinking. - Overflow: in Java or C++,
(lo + hi) / 2can overflow for large indices;lo + (hi - lo) / 2avoids it. Python integers never overflow. - Broken precondition: the input must be sorted, or more generally the yes/no condition must be monotonic.
In Python I would also mention
bisect_leftandbisect_rightfrom the standard library.def binary_search(a, target): lo, hi = 0, len(a) - 1 # inclusive bounds while lo <= hi: mid = lo + (hi - lo) // 2 if a[mid] == target: return mid if a[mid] < target: lo = mid + 1 # discard the left half else: hi = mid - 1 # discard the right half return -1What interviewers listen for- Requires sorted input or a monotonic condition
- Halves the range each step:
O(log n) - Pick inclusive or exclusive
hiand stay consistent - Use
mid + 1/mid - 1so the range shrinks lo + (hi - lo) / 2avoids overflow in Java/C++
Likely follow-up: How would you find the first occurrence of a duplicated value? · How would you search a rotated sorted array?
- Off-by-one boundaries: decide whether
12.Compare quicksort and mergesort. Which would you choose, and why?mid
Both are divide-and-conquer
O(n log n)sorts, but they split the work differently.Quicksort picks a pivot, partitions the array in place into smaller and larger elements, then recurses on each side. It averages
O(n log n), but consistently bad pivots (for example, always the first element on already-sorted input) giveO(n²). Random or median-of-three pivots make that very unlikely. It needs onlyO(log n)expected stack space, has excellent cache behavior, and is not stable.Mergesort splits in half, sorts both halves, and merges them. It is
O(n log n)in every case and stable, but merging arrays needsO(n)extra space. It is the natural choice for linked lists (no random access needed) and for external sorting of data that does not fit in memory.Real libraries blend ideas: Python's
sortedand Java's object sorts use Timsort, a stable merge/insertion hybrid, while C++std::sortis typically introsort, a quicksort that falls back to heapsort.What interviewers listen for- Both
O(n log n)on average - Quicksort worst case
O(n²); random pivots mitigate it - Quicksort: in place, cache-friendly, not stable
- Mergesort: always
O(n log n), stable,O(n)extra - Mergesort suits linked lists and external sorting
Likely follow-up: How does pivot choice affect quicksort? · How would you sort a linked list in
O(n log n)?- Both
13.How would you find the kth largest element in an unsorted array?mid
There are three standard options:
- Sort and index:
O(n log n)time. Simple, and fine as a first answer. - Min-heap of size k: push each element and pop whenever the heap grows past
k. The heap always holds the k largest values seen so far, so its root is the kth largest. That isO(n log k)time andO(k)space, and it also works on a stream. - Quickselect: partition like quicksort, but recurse only into the side that contains the target index. It averages
O(n), with anO(n²)worst case made unlikely by a random pivot. It works in place but reorders the input.
In an interview I present the heap as the robust default, mention quickselect for the best average time, and note that
heapq.nlargest(k, nums)[-1]does the heap approach in one line.I also clarify that duplicates count: in
[3, 3, 2]the 2nd largest is3.import heapq def kth_largest(nums, k): heap = [] # min-heap of the k largest so far for x in nums: heapq.heappush(heap, x) if len(heap) > k: heapq.heappop(heap) # drop the smallest of them return heap[0] # root = kth largestWhat interviewers listen for- Sorting works in
O(n log n) - Min-heap of size k:
O(n log k)time,O(k)space - The heap root is the kth largest
- Quickselect: average
O(n), worstO(n²) - The heap approach also works on streams
Likely follow-up: How does quickselect differ from quicksort? · How would you track the kth largest value in a stream?
- Sort and index:
14.Find the length of the longest substring without repeating characters. How do you get it to
O(n)?midBrute force checks every substring for duplicates, which is
O(n²)substrings with extra work each. The optimization is a sliding window: keep a window[start, i]that never contains a repeat, and extend it one character at a time.I store the last index where each character was seen. When the current character already appears inside the window (its last index is at least
start), I movestartto just past that previous copy. Then I record the current index and update the best length,i - start + 1.The
>= startcheck matters: a character last seen before the window began is not a conflict. In"abba", when the finalaarrives, its old index0is already outside the window, sostartmust not move backwards.Each character is handled once:
O(n)time, andO(min(n, alphabet size))space for the map. Quick tests:""gives 0,"bbbb"gives 1,"pwwkew"gives 3.def length_of_longest_substring(s): last = {} # char -> last index seen start = best = 0 for i, ch in enumerate(s): if last.get(ch, -1) >= start: # repeat inside the window start = last[ch] + 1 # jump past the old copy last[ch] = i best = max(best, i - start + 1) return bestWhat interviewers listen for- Sliding window that never contains a repeat
- Map each character to its last index
- Move
startonly if the repeat is inside the window O(n)time,O(min(n, alphabet))space- Test
"abba"to catch the backwards-jump bug
Likely follow-up: How would you solve it if up to
kdistinct characters were allowed? · How would you return the substring itself?15.Given a list of intervals, merge all overlapping intervals. Walk me through your approach.mid
The key step is to sort by start. Once sorted, any interval that overlaps the current merged block must come right after it, so one pass is enough.
I keep a result list. For each interval, if the result is empty or the interval starts after the last merged block ends, it opens a new block. Otherwise it overlaps, so I extend the last block's end to
max(last_end, end). Themaxmatters because an interval can sit entirely inside the previous one, like[1, 10]followed by[2, 3].I clarify whether touching intervals such as
[1, 2]and[2, 3]count as overlapping; using<=merges them,<keeps them apart.Sorting dominates:
O(n log n)time, plusO(n)space for the output and the sorted copy. I usesorted()rather thanintervals.sort()so the caller's list is not mutated.def merge(intervals): merged = [] for start, end in sorted(intervals, key=lambda iv: iv[0]): if merged and start <= merged[-1][1]: # overlaps last block merged[-1][1] = max(merged[-1][1], end) else: merged.append([start, end]) return mergedWhat interviewers listen for- Sort intervals by start first
- Overlap when start is at most the last end
- Extend with
maxto handle containment - Clarify whether touching intervals merge
O(n log n)time,O(n)space
Likely follow-up: How would you insert a new interval into a sorted, non-overlapping list? · What is the minimum number of meeting rooms these intervals need?
16.What is dynamic programming, and what is the difference between memoization and tabulation?mid
Dynamic programming solves a problem by combining answers to smaller subproblems. It applies when two properties hold: optimal substructure (the best answer is built from best answers to subproblems) and overlapping subproblems (the same subproblems recur, so caching them pays off). Naive recursive Fibonacci is the classic sign:
fib(n - 2)is recomputed again and again, giving exponential time.- Memoization is top-down: write the natural recursion and cache each result, for example with
functools.cache. It computes only the states actually reached and is easy to derive, but it pays call overhead and can hit the recursion limit. - Tabulation is bottom-up: fill a table in an order where every dependency is already computed. It uses plain loops and often allows space savings, like keeping only the previous row.
Both have the same time complexity: number of states times work per state. My recipe: define the state, write the recurrence, set base cases, choose the evaluation order, then optimize space.
What interviewers listen for- Needs optimal substructure and overlapping subproblems
- Memoization: top-down recursion plus a cache
- Tabulation: bottom-up loops in dependency order
- Time = number of states times work per state
- Tabulation enables rolling-array space savings
Likely follow-up: How do you recognize that a problem needs DP? · When would you prefer memoization over tabulation?
- Memoization is top-down: write the natural recursion and cache each result, for example with
17.Design an LRU cache that supports
getandputinO(1)time.midAn LRU cache evicts the least recently used entry when it is full. To make both operations
O(1)I combine two structures:- A hash map from key to node, for
O(1)lookup. - A doubly linked list ordered by recency, most recent at one end. With the node in hand, unlinking and re-inserting it is
O(1).
getlooks up the key, moves its node to the most-recent end, and returns the value (or-1).putupdates or inserts the node at the most-recent end; if the size now exceeds capacity, it removes the node at the least-recent end and deletes its key from the map, which is why each node also stores its key. Sentinel head and tail nodes remove empty-list edge cases.In Python,
OrderedDictalready is this combination, as shown. Interviewers often ask for the hand-built version too. In Java, aLinkedHashMapin access order that overridesremoveEldestEntrydoes the same.from collections import OrderedDict class LRUCache: def __init__(self, capacity): self.cap, self.data = capacity, OrderedDict() def get(self, key): if key not in self.data: return -1 self.data.move_to_end(key) # now most recent return self.data[key] def put(self, key, value): self.data[key] = value self.data.move_to_end(key) if len(self.data) > self.cap: self.data.popitem(last=False) # evict least recentWhat interviewers listen for- Hash map gives
O(1)key lookup - Doubly linked list keeps recency order
- Every access moves the node to the recent end
- Evict from the old end; nodes store their keys
- Sentinel nodes avoid null edge cases
Likely follow-up: How would you make the cache thread-safe? · How would you design an LFU cache instead?
- A hash map from key to node, for
18.Explain in-order, pre-order, post-order and level-order traversal of a binary tree, and when you would use each.easy
The three depth-first orders differ only in when the node is visited relative to its subtrees:
- Pre-order (node, left, right): parents before children. Useful for copying or serializing a tree.
- In-order (left, node, right): on a BST it yields values in sorted order, so it powers validation and kth-smallest.
- Post-order (left, right, node): children before parents. Useful for deleting a tree or computing heights and subtree sizes.
- Level-order (breadth-first): a queue visits the tree level by level. Useful for per-level answers, the right-side view, or minimum depth.
All four are
O(n)time. The depth-first orders useO(h)stack space, wherehis the height:O(log n)for a balanced tree butO(n)for a skewed one. Level-order uses space for the widest level, which can be aboutn / 2in a complete tree. Any recursive version can be rewritten with an explicit stack.from collections import deque def inorder(node, out): if node: inorder(node.left, out) # pre-order: append before this line out.append(node.val) inorder(node.right, out) # post-order: append after this line def level_order(root): out, q = [], deque([root] if root else []) while q: node = q.popleft() out.append(node.val) q.extend(c for c in (node.left, node.right) if c) return outWhat interviewers listen for- Pre: node first; in: middle; post: last
- In-order of a BST is sorted
- Level-order uses a queue
- All four are
O(n)time - DFS space is
O(h); BFS space is the widest level
Likely follow-up: Write an iterative in-order traversal with an explicit stack. · How would you rebuild a tree from its pre-order and in-order traversals?
19.What is a binary search tree, and how would you check whether a binary tree is a valid BST?mid
A BST keeps every value in a node's left subtree smaller than the node and every value in its right subtree larger. That ordering makes search, insert and delete
O(h)for heighth:O(log n)when balanced, butO(n)if it degenerates into a chain, for example after inserting keys in sorted order.The common wrong answer compares each node only with its direct children. That misses a root
5whose left child3has a right child6: every parent-child pair looks fine, but6sits in the left subtree of5.The fix is to pass down the valid range. The root may hold anything; going left tightens the upper bound to the node's value, going right tightens the lower bound, and every node must fall strictly inside its range. That is
O(n)time andO(h)stack space.An alternative is an in-order traversal that checks the values are strictly increasing. I also clarify how duplicates should be treated.
def is_valid_bst(node, lo=float('-inf'), hi=float('inf')): if node is None: return True if not (lo < node.val < hi): # must fit the inherited range return False return (is_valid_bst(node.left, lo, node.val) and is_valid_bst(node.right, node.val, hi))What interviewers listen for- Left subtree smaller, right subtree larger, recursively
- Operations are
O(h); a degenerate tree isO(n) - Checking only direct children is wrong
- Pass down min and max bounds
- Or: in-order values must be strictly increasing
Likely follow-up: How would you find the kth smallest element in a BST? · How would you find the lowest common ancestor of two nodes in a BST?
20.How do you analyze space complexity? Does the recursion stack count?easy
Space complexity measures the memory an algorithm needs as a function of input size. I usually report auxiliary space, the memory beyond the input itself, and I say so explicitly, because some people count the input too.
I add up:
- Data structures I allocate: a hash map with
nentries isO(n), while a fixed 26-letter count array isO(1). - The recursion stack: yes, it counts. Each active call holds a frame, so the cost is proportional to the maximum recursion depth, not the total number of calls. Recursive DFS on a balanced tree is
O(log n); on a skewed tree it isO(n). Memoized Fibonacci usesO(n)for the cache plusO(n)of stack. - Hidden copies: slicing,
sorted(), string building and list comprehensions all allocate.
The output is usually excluded unless the question says otherwise. Replacing recursion with a loop, or keeping only the previous DP row, are the usual ways to cut space.
What interviewers listen for- Report auxiliary space, and say so
- The recursion stack counts, by maximum depth
- Balanced-tree DFS
O(log n); skewedO(n) - Watch hidden copies: slicing,
sorted() - Output is usually excluded from the count
Likely follow-up: What is the space complexity of merge sort on an array? · How would you reduce a 2D DP table to a single row?
- Data structures I allocate: a hash map with
21.What is a heap, and how is it used to implement a priority queue?easy
A binary heap is a complete binary tree with the heap property: in a min-heap every parent is less than or equal to its children, so the minimum sits at the root. A max-heap is the mirror image. Beyond that it is not sorted; siblings have no order.
Because the tree is complete, it lives compactly in an array: the children of index
iare at2i + 1and2i + 2, and its parent is at(i - 1) // 2.- Peek:
O(1), the root isa[0]. - Push: append, then sift up,
O(log n). - Pop: move the last element to the root, then sift down,
O(log n). - Build from
nitems:O(n)with bottom-up heapify.
A priority queue is the abstract "give me the highest-priority item" interface, and a heap is its usual implementation. Python's
heapqis a min-heap over a list; for a max-heap, push negated keys. Heaps power Dijkstra, top-k, merging sorted lists and schedulers.What interviewers listen for- Complete binary tree; parent at most its children
- Array layout: children at
2i+1and2i+2 - Peek
O(1); push and popO(log n) - Bottom-up heapify builds a heap in
O(n) heapqis a min-heap; negate keys for max
Likely follow-up: Why is building a heap
O(n)rather thanO(n log n)? · How would you mergeksorted lists with a heap?- Peek:
22.Given an array, return the
kmost frequent elements. Can you beatO(n log n)?midFirst I count frequencies with a hash map (
Counter), which isO(n). Then I need thekkeys with the highest counts, amongmdistinct values:- Sort the distinct values by count:
O(m log m), theO(n log n)baseline. - Min-heap of size k keyed by count:
O(m log k).heapq.nlargest(k, count, key=count.get)does it in one line. - Bucket sort: every frequency lies between
1andn, so I createn + 1buckets where bucketcholds the values seen exactlyctimes, then walk from the highest bucket down until I havekvalues. That isO(n)time andO(n)space.
I mention
Counter.most_common(k)as the idiomatic library call, then write the bucket version when asked to beatO(n log n). I also clarify ties: when several values share the cut-off frequency, any of them is usually acceptable.from collections import Counter def top_k_frequent(nums, k): count = Counter(nums) buckets = [[] for _ in range(len(nums) + 1)] # index = frequency for x, c in count.items(): buckets[c].append(x) out = [] for c in range(len(nums), 0, -1): # highest frequency first for x in buckets[c]: out.append(x) if len(out) == k: return out return outWhat interviewers listen for- Count frequencies first in
O(n) - Heap of size k:
O(m log k) - Frequencies are at most
n, so bucket them - Bucket sort gives
O(n)time and space - Clarify tie-breaking at the cut-off
Likely follow-up: How would you handle a stream too large to fit in memory? · How would you return the
kmost frequent words, alphabetical on ties?- Sort the distinct values by count:
23.What is the two-pointer technique, and what kinds of problems does it solve?easy
Two pointers means walking an array or string with two indices instead of nested loops, moving them by a rule so the total work is
O(n)rather thanO(n²). There are two main shapes:- Opposite ends: one pointer at each end, moving inward. On a sorted array, pair sum works this way: if
a[lo] + a[hi]is too small, moveloright; if too large, movehileft. Each move safely discards candidates, givingO(n)time andO(1)space. Palindrome checks, in-place reversal and container-with-most-water fit here too. - Same direction (read and write pointers): a fast pointer scans while a slow one marks where to write. Removing duplicates from a sorted array in place, moving zeros to the end, and merging two sorted arrays work this way.
3Sum combines both: sort, fix one element, then run the opposite-ends search on the rest, for
O(n²)overall.Signals to reach for it: sorted input, pairs or triples, in-place rewriting, or a symmetric check.
What interviewers listen for- Coordinated indices replace nested loops
- Opposite ends on sorted input:
O(n),O(1)space - Same direction: read/write pointers for in-place edits
- Each move must safely discard candidates
- 3Sum: sort plus two pointers,
O(n²)
Likely follow-up: Why is it safe to move the shorter side in container-with-most-water? · How do you skip duplicate triplets in 3Sum?
- Opposite ends: one pointer at each end, moving inward. On a sorted array, pair sum works this way: if
24.You can climb 1 or 2 steps at a time. How many distinct ways are there to reach step
n?easyTo land on step
n, the last move was either a 1-step fromn - 1or a 2-step fromn - 2. Those groups do not overlap and together cover every path, soways(n) = ways(n - 1) + ways(n - 2), withways(0) = ways(1) = 1. That is the Fibonacci recurrence.Plain recursion is exponential, roughly
O(1.6ⁿ), because the same values are recomputed many times. That repeated work is the overlapping subproblems signal for DP.- Memoization: cache the recursive function so each
nis computed once:O(n)time,O(n)space for the cache and the stack. - Tabulation: loop upward keeping only the last two values:
O(n)time,O(1)space.
I present the bottom-up version as the final answer. Variations interviewers like: steps of 1, 2 or 3 (sum the last three), an arbitrary set of step sizes (a coin-change style loop), or a cost per step (min-cost climbing stairs).
from functools import cache @cache def climb_memo(n): return 1 if n <= 1 else climb_memo(n - 1) + climb_memo(n - 2) def climb(n): # bottom-up, O(1) space a, b = 1, 1 # ways(0), ways(1) for _ in range(n - 1): a, b = b, a + b return bWhat interviewers listen for- Last move was 1 or 2 steps:
f(n) = f(n-1) + f(n-2) - Base cases
f(0) = f(1) = 1 - Naive recursion is exponential
- Memoized:
O(n)time and space - Bottom-up with two variables:
O(1)space
Likely follow-up: What if you could take 1, 2 or 3 steps? · How would you handle a cost for landing on each step?
- Memoization: cache the recursive function so each
25.Given coin denominations and an amount, find the minimum number of coins needed. Why does a greedy approach fail?mid
Greedy (always take the largest coin that fits) works for coin systems like
1, 5, 10, 25, but not in general. With coins[1, 3, 4]and amount6, greedy picks4 + 1 + 1(3 coins), while3 + 3needs only 2.So I use DP. Let
dp[a]be the fewest coins that make amounta. The last coin used is somec, leavinga - c, sodp[a] = 1 + min(dp[a - c])over coinsc <= a, withdp[0] = 0. Amounts that cannot be made stay at infinity, and I return-1for them.It is
O(amount × number of coins)time andO(amount)space. That is pseudo-polynomial: polynomial in the numeric value ofamount, not in the number of digits needed to write it.The sibling problem, counting the number of ways to make the amount, puts coins in the outer loop so each combination is counted once rather than once per ordering.
def coin_change(coins, amount): INF = float('inf') dp = [0] + [INF] * amount # dp[a] = fewest coins for a for a in range(1, amount + 1): for c in coins: if c <= a and dp[a - c] + 1 < dp[a]: dp[a] = dp[a - c] + 1 return dp[amount] if dp[amount] != INF else -1What interviewers listen for- Greedy fails: coins
[1, 3, 4], amount 6 dp[a] = 1 + min(dp[a - c]), withdp[0] = 0- Unreachable amounts stay infinite; return
-1 O(amount × coins)time,O(amount)space- Counting combinations: coins in the outer loop
Likely follow-up: How would you count the number of combinations instead? · Could you solve it with BFS instead of DP?
- Greedy fails: coins
26.What are the trade-offs between recursion and iteration? Can every recursive solution be made iterative?easy
Recursion expresses a problem in terms of smaller copies of itself, with a base case that stops it. It is the natural fit for trees, divide and conquer, backtracking and DFS, and the code often mirrors the recurrence directly.
The costs: each call pushes a stack frame, so space is
O(depth), and deep recursion can overflow the stack. Python's default recursion limit is1000and it has no tail-call optimization, so recursing down a 100,000-node linked list fails. Calls also add constant-factor overhead.Iteration uses loops and explicit state. It avoids stack overflow, is usually faster, and makes memory use obvious.
Yes, every recursion can be converted: the call stack is just a stack, so you can manage one yourself, as in iterative DFS or iterative in-order traversal. Tail-recursive and simple linear recursions become plain loops.
My rule: use recursion when it is clearer and the depth is bounded, such as
O(log n)on a balanced tree, and switch to iteration when depth can grow withn.What interviewers listen for- Recursion needs a base case and shrinking input
- Each call costs a frame:
O(depth)space - Python: default limit 1000, no tail-call optimization
- Any recursion converts to a loop plus explicit stack
- Prefer iteration when depth grows with
n
Likely follow-up: Convert a recursive pre-order traversal into an iterative one. · What is tail recursion, and why does it matter?
27.What is amortized analysis? Explain why appending to a dynamic array is
O(1)amortized.midAmortized analysis bounds the total cost of a sequence of operations and divides by the number of operations. It is a worst-case guarantee on the average over the sequence, which is different from average-case analysis: no probability or random inputs are involved.
A dynamic array (Python
list, JavaArrayList) keeps spare capacity. Most appends just write into the next free slot,O(1). When it is full, it allocates a bigger block and copies every element,O(n)for that one append.If capacity doubles, the copies across
nappends total1 + 2 + 4 + ...up ton, a geometric series below2n. Sonappends costO(n)overall,O(1)amortized each. What matters is geometric growth: any constant factor above 1 works (Java'sArrayListgrows by about 1.5x, and CPython over-allocates by a smaller fraction). Growing by a fixed number of slots instead makes the sequenceO(n²).The formal tools are the aggregate method (used above), the accounting method and the potential method.
What interviewers listen for- Total cost of a sequence divided by its length
- Not average case: no probability involved
- Rare
O(n)resize, otherwiseO(1)writes - Geometric growth keeps total copying under
2n - Fixed-size growth degrades to
O(n²)total
Likely follow-up: Why do some implementations grow by 1.5x rather than 2x? · What goes wrong if you halve the array whenever it becomes half empty?
28.What is the difference between Big-O, Big-Θ and Big-Ω? Is Big-O the same as the worst case?mid
All three bound how a function grows, for large enough
nand up to a constant factor:- Big-O is an upper bound:
f(n) = O(g(n))iff(n) ≤ c · g(n)for some constantcand everynpast somen₀. - Big-Ω is a lower bound: eventually
f(n) ≥ c · g(n). - Big-Θ is a tight bound: both at once, so
fgrows likeg.
So
3n² + nisΘ(n²). It is also technicallyO(n³), true but uninformative. In interviews people say Big-O but usually mean the tight bound.And no, Big-O is not "the worst case". Best, average and worst case choose which inputs you analyze; O, Ω and Θ describe what kind of bound you state about that cost. Insertion sort is
Θ(n²)in the worst case andΘ(n)in the best case, on already-sorted input. The comparison-sort lower bound is an Ω statement: any comparison sort needsΩ(n log n)comparisons in the worst case.What interviewers listen for- O is upper, Ω is lower, Θ is tight
- Holds up to a constant, for large
n - Interview "Big-O" usually means the tight bound
- Cases pick inputs; notations describe bounds
- Insertion sort: worst
Θ(n²), bestΘ(n)
Likely follow-up: Is
2ⁿ⁺¹inO(2ⁿ)? What about2²ⁿ? · What is the average-case complexity of quicksort, and why?- Big-O is an upper bound:
29.How do you implement a queue using two stacks, and what does each operation cost?mid
I use an inbox stack for pushes and an outbox stack for pops.
- Push: append to the inbox,
O(1). - Pop or peek: if the outbox is empty, pop everything off the inbox onto the outbox. That reverses the order, so the oldest element ends up on top. Then pop from the outbox.
The key detail is to transfer only when the outbox is empty. Transferring while it still holds items would put newer elements above older ones and break FIFO order.
A single pop can cost
O(n)when it triggers a transfer, but each element is pushed onto the inbox once, moved once, and popped from the outbox once. Sonoperations costO(n)in total, which makes every operation amortizedO(1), withO(n)space.The reverse puzzle, a stack built from queues, also works, but then push or pop costs
O(n)every time rather than amortized.class MyQueue: def __init__(self): self.inbox, self.outbox = [], [] def push(self, x): self.inbox.append(x) def pop(self): if not self.outbox: # refill only when empty while self.inbox: self.outbox.append(self.inbox.pop()) return self.outbox.pop()What interviewers listen for- Inbox for pushes, outbox for pops
- Transfer only when the outbox is empty
- Transfer reverses order, oldest on top
- Each element is moved at most once
- Amortized
O(1)per operation
Likely follow-up: What is the worst case for a single pop? · How would you implement a stack using queues?
- Push: append to the inbox,
30.How do you represent a graph in code? Compare an adjacency list with an adjacency matrix.easy
The two standard representations, for
Vvertices andEedges:- Adjacency list: each vertex maps to a list (or set) of its neighbors, for example a dict of lists in Python. Space is
O(V + E)and listing a vertex's neighbors costsO(degree), so BFS and DFS run inO(V + E). Checking one specific edge isO(degree), orO(1)on average if neighbors are kept in sets. - Adjacency matrix: a
V × Vgrid wherem[u][v]holds 1 or the edge weight. Edge lookup isO(1), but space isO(V²)however few edges there are, and listing neighbors costsO(V), so BFS becomesO(V²).
Most real graphs are sparse, with
Efar belowV², so I default to adjacency lists. A matrix makes sense for small or dense graphs, frequent edge queries, or matrix-shaped algorithms like Floyd-Warshall.An edge list of
(u, v, weight)tuples is a third option, handy for Kruskal's algorithm, and grids are implicit graphs whose neighbors are computed on the fly.What interviewers listen for- List:
O(V + E)space, neighbors inO(degree) - Matrix:
O(V²)space,O(1)edge check - Traversal:
O(V + E)with lists,O(V²)with a matrix - Default to lists for sparse graphs
- Edge lists suit Kruskal; grids are implicit graphs
Likely follow-up: How would you store a weighted, directed graph? · When would you choose Floyd-Warshall over running Dijkstra from every node?
- Adjacency list: each vertex maps to a list (or set) of its neighbors, for example a dict of lists in Python. Space is
31.Explain the sliding window technique. How do you know when it applies, and when does it break?mid
A sliding window maintains a contiguous range
[left, right]over an array or string and updates an aggregate incrementally as the window moves, instead of recomputing it for every subarray. That turnsO(n²)or worse intoO(n).- Fixed size: for "max sum of any
kconsecutive elements", add the element entering on the right and subtract the one leaving on the left,O(1)per step. - Variable size: extend
righteach step; while the window violates the condition, shrink fromleft. Each index enters and leaves at most once, so it isO(n)overall despite the innerwhile. Longest substring without repeats and shortest subarray with sum at leasttargetwork this way.
It applies when validity is monotonic in the window: shrinking a valid window keeps it valid, or growing an invalid one keeps it invalid. That is why "shortest subarray with sum at least target" needs non-negative numbers. With negatives, adding an element can lower the sum, so I switch to prefix sums with a hash map, or a monotonic deque.
What interviewers listen for- Contiguous range with an incrementally updated aggregate
- Fixed size: add the incoming, remove the outgoing
- Variable size: extend right, shrink left while invalid
- Each index enters and leaves once:
O(n) - Needs monotonic validity; negatives break sum windows
Likely follow-up: How would you find the minimum window substring containing all characters of
t? · How would you count subarrays with exactlykdistinct values?- Fixed size: for "max sum of any
32.What are prefix sums, and how would you count the subarrays that sum to
k?midA prefix sum array stores running totals:
P[0] = 0andP[i + 1] = P[i] + a[i]. AfterO(n)preprocessing, the sum of any rangea[i..j]isP[j + 1] - P[i]inO(1), so repeated range-sum queries become cheap. The idea extends to 2D grids, and its inverse, a difference array, applies range updates inO(1)each.For "count subarrays summing to
k": a subarray ending at the current index sums tokexactly when some earlier prefix equalsprefix - k. So I sweep once, keep a hash map from each prefix sum to how many times it has occurred, and addseen[prefix - k]at every step. The map starts as{0: 1}so subarrays that begin at index 0 are counted.That is
O(n)time andO(n)space, and unlike a sliding window it works with negative numbers. The brute force, summing every subarray, isO(n²).def subarray_sum(nums, k): count = prefix = 0 seen = {0: 1} # prefix sum -> times seen for x in nums: prefix += x count += seen.get(prefix - k, 0) seen[prefix] = seen.get(prefix, 0) + 1 return countWhat interviewers listen forP[i+1] = P[i] + a[i]; range sum isP[j+1] - P[i]O(n)preprocessing,O(1)per query- Count earlier prefixes equal to
prefix - k - Seed the map with
{0: 1} - Handles negatives, unlike a sliding window
Likely follow-up: How would you find the longest subarray with sum
k? · How do 2D prefix sums answer rectangle-sum queries?33.What is a topological sort, and how would you compute one, for example to order courses with prerequisites?mid
A topological order of a directed graph lists the vertices so that every edge
u -> vhasubeforev. It exists only for a DAG (directed acyclic graph); a cycle means no valid order. Typical uses are build systems, task scheduling and course prerequisites.Kahn's algorithm is BFS-based: compute every in-degree and queue the vertices with in-degree 0, since nothing must precede them. Repeatedly pop a vertex, append it to the order, and decrement its neighbors' in-degrees, queuing any that reach 0. If the order ends up shorter than
V, the leftover vertices sit on a cycle, so this doubles as cycle detection.The DFS alternative appends each vertex after all its descendants finish (post-order), then reverses the list; cycle detection there needs an "on the current path" marker.
Both are
O(V + E)time and space. The order is usually not unique; using a min-heap instead of a queue yields the lexicographically smallest one.from collections import deque def topo_sort(n, edges): # (u, v): u must come before v graph, indeg = [[] for _ in range(n)], [0] * n for u, v in edges: graph[u].append(v) indeg[v] += 1 q, order = deque(i for i in range(n) if indeg[i] == 0), [] while q: u = q.popleft() order.append(u) for v in graph[u]: indeg[v] -= 1 if indeg[v] == 0: q.append(v) return order if len(order) == n else None # None: there is a cycleWhat interviewers listen for- Every edge
u -> vputsubeforev - Only possible for a DAG
- Kahn's: repeatedly remove in-degree-0 vertices
- Output shorter than
Vmeans a cycle O(V + E); reversed DFS post-order also works
Likely follow-up: How would you report which courses form the cycle? · How would you find the minimum number of semesters needed to finish all courses?
- Every edge
34.How does Dijkstra's algorithm work, what is its complexity, and when does it fail?hard
Dijkstra finds shortest paths from one source in a graph with non-negative edge weights. It is BFS with a priority queue: always expand the vertex with the smallest known distance, because with no negative edges nothing found later can make that distance shorter.
I keep a
distmap and a min-heap of(distance, vertex)pairs. Pop the smallest; if it is stale (larger than the recorded distance), skip it. Otherwise relax each outgoing edge: ifd + wbeats the neighbor's best distance, record it and push the new pair. This lazy deletion avoids needing a decrease-key operation.With a binary heap it is
O((V + E) log V)time andO(V + E)space. A Fibonacci heap improves the bound toO(E + V log V), and a plain array scan givesO(V²), which suits dense graphs.It fails with negative edges: a vertex can be finalized before a cheaper path through a negative edge is discovered. Then I use Bellman-Ford,
O(V · E), which also detects negative cycles. For unweighted graphs, plain BFS is enough.import heapq def dijkstra(graph, src): # graph: {u: [(v, weight), ...]} dist = {src: 0} pq = [(0, src)] while pq: d, u = heapq.heappop(pq) if d > dist[u]: continue # stale entry: u was already improved for v, w in graph.get(u, []): nd = d + w if nd < dist.get(v, float('inf')): dist[v] = nd heapq.heappush(pq, (nd, v)) return distWhat interviewers listen for- Non-negative edge weights only
- Min-heap always expands the closest vertex
- Relax edges; skip stale heap entries
- Binary heap:
O((V + E) log V) - Negative edges: use Bellman-Ford instead
Likely follow-up: How would you reconstruct the actual path, not just the distance? · How does A* search improve on Dijkstra?
35.Compare chaining and open addressing for resolving hash collisions. What role do the load factor and the worst case play?hard
With separate chaining, each bucket holds a list (or tree) of the entries that hash there. It is simple, deletion is easy, and the load factor can exceed 1, with performance degrading gradually as chains grow. The cost is a pointer per entry and weaker cache locality. Java's
HashMapchains, resizes past a default load factor of0.75, and converts very long buckets into balanced trees.With open addressing, entries live in the array itself, and a collision probes for another slot: linear probing, quadratic probing or double hashing. It is compact and cache-friendly, which is why CPython's
dictuses it, but the load factor must stay well below 1 because probe sequences lengthen sharply as the table fills. Linear probing also suffers primary clustering, and deletion needs tombstones so later searches do not stop early.Either way,
O(1)average needs a well-spreading hash and geometric resizing. The worst case isO(n)when keys collide, which attackers can force (hash flooding); keyed, randomized hashing like Python's SipHash-based string hash, and Java's tree bins, limit the damage.What interviewers listen for- Chaining: per-bucket lists; load factor may exceed 1
- Open addressing: probe inside the array, cache-friendly
- Open addressing needs a low load factor and tombstones
- Linear probing suffers primary clustering
- Worst case
O(n); keyed hashing resists flooding
Likely follow-up: Why does deleting from a linear-probing table need tombstones? · What happens to a Java
HashMapbucket that collects many collisions?36.What is a greedy algorithm, how do you know when greedy is correct, and when does it fail?mid
A greedy algorithm builds a solution by making the locally best choice at each step and never revisiting it. It is correct when the problem has the greedy-choice property (some optimal solution starts with the greedy choice) and optimal substructure (what remains is a smaller instance of the same problem).
Classic cases where greedy is provably optimal:
- Interval scheduling: to fit the most non-overlapping intervals, always pick the one that ends earliest.
- Fractional knapsack: take items by value-to-weight ratio.
- Huffman coding, Dijkstra, and Kruskal or Prim for minimum spanning trees.
Where it fails: 0/1 knapsack, where the best-ratio item can waste capacity; coin change with arbitrary coins, like
[1, 3, 4]for amount6; and longest-path problems. Those need DP or search.To justify greedy in an interview I use an exchange argument: take any optimal solution, swap in the greedy choice, and show it gets no worse. If I cannot make that argument, I hunt for a small counterexample, and if I find one I move to DP.
What interviewers listen for- Locally best choice, never revisited
- Needs greedy-choice property and optimal substructure
- Works: earliest-end interval scheduling, MST, Huffman
- Fails: 0/1 knapsack, arbitrary coin change
- Prove with an exchange argument, or find a counterexample
Likely follow-up: Why does picking the earliest end time work, but earliest start time does not? · Can you give a counterexample for greedy 0/1 knapsack?
37.What is backtracking? Walk me through generating all subsets of a list.mid
Backtracking is DFS over a tree of partial solutions. At each step I choose an option, explore further recursively, then un-choose it to restore state before trying the next option. It fits problems that ask for all solutions or any valid configuration: subsets, permutations, combination sum, N-queens, Sudoku and word search.
For subsets, each call records the current path as one subset, then tries adding each remaining element from
startonward. Recursing withi + 1keeps elements in increasing index order, so[1, 2]and[2, 1]are not both produced. I append a copy of the path, because the list itself keeps changing.There are
2ⁿsubsets and copying each costs up toO(n), so it isO(n · 2ⁿ)time, withO(n)recursion depth beyond the output. Permutations costO(n · n!).The main optimization is pruning: abandon a branch as soon as it cannot lead to a valid answer, for example when a running sum passes the target, or skip duplicates after sorting.
def subsets(nums): out, path = [], [] def backtrack(start): out.append(path[:]) # record a copy of the current subset for i in range(start, len(nums)): path.append(nums[i]) # choose backtrack(i + 1) # explore path.pop() # un-choose backtrack(0) return outWhat interviewers listen for- Choose, explore, un-choose
- DFS over partial solutions
- Append a copy of the current path
- Subsets
O(n · 2ⁿ); permutationsO(n · n!) - Prune branches that cannot succeed
Likely follow-up: How would you generate subsets when the input contains duplicates? · How would you solve N-queens with backtracking?
38.Beyond detecting a cycle, where else is the fast and slow pointer technique useful?mid
Fast and slow pointers move through a sequence at different speeds, usually one and two steps. Besides detecting a cycle, the pattern solves:
- Middle of a linked list in one pass: when
fastreaches the end,slowis in the middle. Starting both at the head withwhile fast and fast.next, an even-length list gives the second middle node. - Start of a cycle: after they meet, reset one pointer to the head and step both by one; they meet at the entry. If the head is
asteps from the entry and they metbsteps into a cycle of lengthc, thena + bis a multiple ofc, soamore steps from the meeting point reach the entry. - Palindrome linked list: find the middle, reverse the second half, compare.
- Implicit sequences: "happy number" and "find the duplicate number" treat
x -> f(x)as a linked list with a cycle.
All of these run in
O(n)time andO(1)space.What interviewers listen for- Two pointers at different speeds, usually 1 and 2
- Finds the middle of a list in one pass
- Reset one pointer to head to find the cycle entry
- Palindrome list: middle, reverse half, compare
- Implicit cycles: happy number, find the duplicate
Likely follow-up: Why is
a + ba multiple of the cycle length at the meeting point? · How would you find the duplicate in an array ofn + 1integers inO(1)space?- Middle of a linked list in one pass: when
39.What is a monotonic stack, and how does it find the next greater element for every item in
O(n)?hardA monotonic stack keeps its contents in sorted order (increasing or decreasing) by popping anything that would break the order before pushing. It answers "nearest greater or smaller element to the left or right" for every position in one pass.
For next greater element, I scan left to right with a stack of indices whose answer is still unknown; their values decrease from bottom to top. When a new value
xarrives, every index on top with a smaller value has just found its next greater element,x, so I pop it and record the answer. Then I push the current index. Whatever remains at the end has no greater element, so it keeps-1.It looks nested, but each index is pushed once and popped at most once, so the total is
O(n)time (amortizedO(1)per element) andO(n)space.The same pattern solves daily temperatures (store the index distance instead), stock span, largest rectangle in a histogram and trapping rain water. For a circular array, loop over
2npositions usingi % n.def next_greater(nums): res = [-1] * len(nums) stack = [] # indices; their values decrease upward for i, x in enumerate(nums): while stack and nums[stack[-1]] < x: res[stack.pop()] = x # x is the next greater for that index stack.append(i) return resWhat interviewers listen for- Stack kept sorted by popping violators first
- Unresolved indices wait on the stack
- A pop means the new value is their answer
- Each index is pushed and popped once:
O(n) - Also: daily temperatures, histogram, rain water
Likely follow-up: How would you find the largest rectangle in a histogram? · How would you handle a circular array?
40.What does it mean for a sorting algorithm to be stable, and why does it matter?easy
A sort is stable if elements that compare equal keep their original relative order. Sorting
[("bob", 25), ("amy", 30), ("cat", 25)]by age, a stable sort guaranteesbobstill comes beforecat.It matters mostly for multi-key sorting: sort by the secondary key first, then stable-sort by the primary key, and ties on the primary key stay ordered by the secondary one. Tables where you click one column after another rely on this, and so does LSD radix sort.
- Stable: merge sort, insertion sort, bubble sort, counting sort and Timsort. Python's
sortedandlist.sortare guaranteed stable, and so is Java's object sort. - Not stable in their usual form: quicksort, heapsort and selection sort, which move elements long distances past equal ones.
Any sort can be made stable by using the original index as a tie-breaker, at the cost of extra space. For plain integers stability is invisible, since equal values are indistinguishable, which is why Java can sort an
int[]with a non-stable quicksort.What interviewers listen for- Equal keys keep their original relative order
- Enables multi-key sorting in successive passes
- Stable: merge, insertion, counting, Timsort
- Unstable: quicksort, heapsort, selection sort
- Original index as tie-breaker forces stability
Likely follow-up: How would you sort people by last name, then first name, using a stable sort? · Why must the per-digit sort inside LSD radix sort be stable?
- Stable: merge sort, insertion sort, bubble sort, counting sort and Timsort. Python's
41.Why can no comparison-based sorting algorithm do better than
Ω(n log n)comparisons in the worst case?hardModel any comparison sort as a decision tree. Each internal node is one comparison, like "is
a[i] < a[j]?", with a branch per outcome, and each leaf is a final ordering. To be correct on every input ofndistinct values, the algorithm must be able to produce alln!permutations, so the tree needs at leastn!leaves.A binary tree of height
hhas at most2ʰleaves, so2ʰ ≥ n!, which givesh ≥ log₂(n!). The height is the worst-case number of comparisons. By Stirling's approximation,log₂(n!)isΘ(n log n); a quick argument is that the larger half of the factors are each at leastn/2, son! ≥ (n/2)^(n/2).So merge sort and heapsort are asymptotically optimal comparison sorts, and a similar argument bounds the average case too.
The bound only covers algorithms that learn about the input by comparing elements. Counting sort and radix sort read key values or digits directly, so they can run in linear time when the key range or key length is bounded.
What interviewers listen for- Decision tree: comparisons are nodes, orderings are leaves
- It must distinguish all
n!permutations - Height
h ≥ log₂(n!), which isΘ(n log n) - Merge sort and heapsort are asymptotically optimal
- Non-comparison sorts escape it using key values
Likely follow-up: How many comparisons do you need to find both the minimum and maximum of
nitems? · Does the bound apply to sorting integers from a small known range?42.What is a trie, and when would you use one instead of a hash set?mid
A trie (prefix tree) stores strings character by character. Each node represents a prefix and has children keyed by the next character, plus an end-of-word flag, because a word like
"car"can also be a prefix of"card". In Python each node is often just a dict of children.Insert and exact search walk one node per character,
O(L)for a word of lengthL, no matter how many words are stored. The real advantage is prefix queries: "does any word start withpre?" is alsoO(L), and collecting every completion is a DFS from that node. A hash set cannot answer prefix questions without scanning every key.Use cases: autocomplete, spell checking, word games like Word Search II (prune the grid DFS as soon as the path is not a prefix), and longest-prefix matching as in IP routing tables.
The downside is memory: many small nodes with pointers, especially with fixed 26-slot child arrays. Compressed tries (radix trees) merge single-child chains to save space. For plain membership checks, a hash set is simpler and usually faster.
What interviewers listen for- Tree of characters; each node is a prefix
- End-of-word flag separates words from prefixes
- Insert and search in
O(L)for word lengthL - Prefix queries and autocomplete beat a hash set
- Memory-heavy; radix trees compress chains
Likely follow-up: How would you build autocomplete that returns the top 3 suggestions? · How does a trie speed up Word Search II?
43.What is union-find (disjoint set union), and what makes its operations nearly constant time?hard
Union-find tracks a partition of elements into disjoint sets with two operations:
find(x)returns the representative (root) ofx's set, andunion(a, b)merges two sets. Each set is a tree stored in aparentarray, where a root is its own parent.Naively, trees can turn into long chains and
findbecomesO(n). Two optimizations fix that:- Union by rank or size: attach the smaller tree under the larger root, which keeps height
O(log n). - Path compression: during
find, point every visited node straight at the root, flattening the tree for next time.
Either one alone gives roughly
O(log n)per operation. With both, operations are amortizedO(α(n)), whereαis the inverse Ackermann function, at most 4 for any realisticn, so effectively constant.Uses: counting connected components, detecting a cycle in an undirected graph (an edge whose endpoints already share a root closes a cycle), Kruskal's minimum spanning tree, and grouping problems like accounts merge. It handles edges arriving online, but it cannot split sets apart.
class DSU: def __init__(self, n): self.parent, self.size = list(range(n)), [1] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) # path compression return self.parent[x] def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra == rb: return False # already connected if self.size[ra] < self.size[rb]: ra, rb = rb, ra self.parent[rb] = ra # union by size self.size[ra] += self.size[rb] return TrueWhat interviewers listen forfindreturns the root;unionmerges two sets- Union by size or rank keeps trees shallow
- Path compression flattens trees during
find - Both together: amortized
O(α(n)), effectively constant - Uses: components, undirected cycles, Kruskal
Likely follow-up: How does Kruskal's algorithm use union-find? · Why can union-find not detect cycles in a directed graph?
- Union by rank or size: attach the smaller tree under the larger root, which keeps height
44.How do you detect a cycle in an undirected graph, and how is that different for a directed graph?hard
The two cases need different rules, and mixing them up is a classic mistake.
Undirected: run DFS or BFS and remember each vertex's parent. Every edge appears in both directions, so seeing the parent again is not a cycle; reaching any other already-visited vertex is. Union-find also works: if an edge's endpoints already share a root, that edge closes a cycle. A handy sanity check is that a connected undirected graph is acyclic exactly when it has
V - 1edges.Directed: "already visited" is not enough, because two paths can reach the same vertex without any cycle, as in a diamond. I use DFS with three colors: white (unvisited), gray (on the current recursion path) and black (finished). An edge to a gray vertex is a back edge, which means a cycle; an edge to a black vertex is fine. Alternatively, run Kahn's topological sort: if it cannot output all
Vvertices, there is a cycle.All of these are
O(V + E)time, and I start a search from every unvisited vertex so disconnected components are covered.What interviewers listen for- Undirected: a visited neighbor other than the parent
- Undirected alternative: union-find over the edges
- Directed: three colors; an edge to gray is a cycle
- Directed alternative: Kahn's outputs fewer than
V - Cover every component;
O(V + E)time
Likely follow-up: How would you handle parallel edges in the undirected case? · How would you return the vertices that form the cycle?
45.Why do we need self-balancing BSTs? Compare AVL trees and red-black trees at a high level.hard
A plain BST's operations cost
O(h), and inserting keys in sorted order produces a chain withh = n, so everything degrades toO(n). Self-balancing trees restore balance with rotations, local pointer rearrangements that preserve BST order, and guaranteeO(log n)search, insert and delete.- AVL trees keep the heights of every node's two subtrees within 1 of each other. That strict balance keeps height close to the minimum, about
1.44 log₂ nat worst, so lookups are very fast. The price is more rebalancing work on inserts and deletes. - Red-black trees color nodes red or black under rules such as: a red node has no red child, and every root-to-leaf path has the same number of black nodes. Height can reach about
2 log₂ n, looser than AVL, but an insert needs at most two rotations and a delete at most three, so writes are cheaper.
Rule of thumb: AVL for read-heavy workloads, red-black for mixed ones. Red-black trees back Java's
TreeMapand are the usual implementation of C++std::map.What interviewers listen for- Sorted inserts turn a plain BST into a chain
- Rotations rebalance while keeping BST order
- AVL: subtree heights differ by at most 1
- Red-black: looser balance, cheaper updates
- AVL for reads; red-black for mixed;
TreeMapis red-black
Likely follow-up: Walk me through a left rotation. · Why would a B-tree beat both for a database index?
- AVL trees keep the heights of every node's two subtrees within 1 of each other. That strict balance keeps height close to the minimum, about
46.Why does building a heap from
nelements takeO(n)rather thanO(n log n)?hardPushing elements one at a time costs
O(log n)each,O(n log n)in total. Bottom-up heapify (Floyd's method) instead treats the array as a complete tree already and sifts down every non-leaf node, from the last one at indexn // 2 - 1back to the root.The key is that sifting down costs at most the node's height, not
log n, and most nodes are near the bottom. About half the nodes are leaves and do no work, a quarter have height 1, an eighth height 2, and so on. The total is aboutn/4 · 1 + n/8 · 2 + n/16 · 3 + ..., which isntimes the seriesΣ h / 2^(h+1), and that series converges to 1. So building isO(n).Building by sift-up gets no such benefit, because the many bottom nodes are the ones that may travel farthest.
So
heapq.heapify(list)isO(n), and top-k code often heapifies once instead of pushing in a loop. Heapsort is stillO(n log n), since itsnpops costO(log n)each.What interviewers listen for- Sift down each non-leaf, from
n//2 - 1to 0 - Sift-down costs the node height, not
log n - Half the nodes are leaves and do no work
Σ h / 2^(h+1)converges, so the total isO(n)heapq.heapifyisO(n); heapsort is stillO(n log n)
Likely follow-up: Why is building a heap by repeated sift-up
O(n log n)? · How does heapsort use an in-place max-heap?- Sift down each non-leaf, from
47.In a sorted array with duplicates, how do you find the first and last positions of a target in
O(log n)?midPlain binary search stops at any match, so I change it to find a boundary instead of a value. The cleanest tool is a lower-bound search: the first index whose value is
>= target. I use a half-open range[lo, hi)withhi = len(a). Ifa[mid] < target, the answer lies to the right, solo = mid + 1; otherwisemidmight be the answer, sohi = mid. The loop ends whenlo == hi, which is the boundary.- First occurrence: the lower bound, if it is in range and holds the target.
- Last occurrence: the first index with a value
> target(the upper bound), minus one.
Each search is
O(log n), so the whole thing staysO(log n)even when every element equals the target. Finding one match and scanning outward would degrade toO(n).These are exactly Python's
bisect_leftandbisect_right, and the same template counts occurrences (upper - lower) or finds the insert position.def bound(a, target, upper=False): # first index with a[i] >= target (or a[i] > target when upper=True) lo, hi = 0, len(a) while lo < hi: mid = (lo + hi) // 2 if a[mid] < target or (upper and a[mid] == target): lo = mid + 1 else: hi = mid return lo def search_range(a, target): first, last = bound(a, target), bound(a, target, upper=True) - 1 return [first, last] if first <= last else [-1, -1]What interviewers listen for- Search for a boundary, not a match
- Half-open
[lo, hi);hi = midkeeps the candidate - First = lower bound; last = upper bound minus one
- Stays
O(log n)even if all values are duplicates - Same as
bisect_leftandbisect_right
Likely follow-up: How would you count the occurrences of a target? · How would you find the insert position that keeps the array sorted?
48.What does "binary search on the answer" mean? Give an example.hard
Sometimes you do not search the array; you search the range of possible answers. It works when feasibility is monotonic: if answer
xworks, every larger (or every smaller) answer works too. The answers then look likeno, no, no, yes, yes, and binary search finds the firstyes.Example, Koko eating bananas: given piles and
hhours (at least one hour per pile), eating at speedkper hour, one pile at a time, find the minimumkthat finishes in time. Pileptakesceil(p / k)hours, and if speedkfinishes in time, any faster speed does too. So I binary searchkover[1, max(piles)], and each check sums the hours inO(n). Total:O(n log M), whereMis the largest pile.The same template solves shipping packages within
Ddays, splitting an array intokparts to minimize the largest sum, and integer square root.The recipe: bound the answer space, write a
feasible(x)check, confirm it is monotonic, then find the boundary with the lower-bound template.def min_eating_speed(piles, h): def hours(k): # total hours at speed k return sum((p + k - 1) // k for p in piles) lo, hi = 1, max(piles) while lo < hi: # smallest feasible k mid = (lo + hi) // 2 if hours(mid) <= h: hi = mid # feasible: try slower else: lo = mid + 1 # too slow: go faster return loWhat interviewers listen for- Search the range of answers, not the array
- Requires monotonic feasibility
- Write a
feasible(x)check, usuallyO(n) - Koko: search speed over
[1, max(piles)] - Total
O(n log M)for answer rangeM
Likely follow-up: How would you split an array into
ksubarrays minimizing the largest sum? · How do you choose the bounds of the search range?49.How do you find the length of the longest increasing subsequence, first in
O(n²)and then inO(n log n)?hardA subsequence keeps order but may skip elements. The
O(n²)DP: letdp[i]be the length of the longest increasing subsequence ending at indexi. For every earlierjwitha[j] < a[i],dp[i] = max(dp[i], dp[j] + 1). The answer ismax(dp), and back-pointers can reconstruct the sequence.The
O(n log n)method keepstails, wheretails[k]is the smallest possible tail of an increasing subsequence of lengthk + 1seen so far.tailsstays sorted, so for eachxI binary search for the first tail>= x. If none exists,xextends the longest subsequence and is appended; otherwise it replaces that tail, since a smaller tail leaves more room to grow. The answer islen(tails).Two cautions:
tailsis not itself a valid subsequence, only its length is meaningful; andbisect_leftgives strictly increasing, whilebisect_rightgives non-decreasing. For[10, 9, 2, 5, 3, 7, 101, 18], the LIS length is 4, for example[2, 3, 7, 18].from bisect import bisect_left def length_of_lis(nums): tails = [] # tails[k] = smallest tail of an increasing run of length k+1 for x in nums: i = bisect_left(tails, x) if i == len(tails): tails.append(x) # x extends the longest run else: tails[i] = x # x is a smaller tail for length i+1 return len(tails)What interviewers listen fordp[i]= LIS ending ati:O(n²)tails[k]= smallest tail for lengthk + 1- Binary search each element into
tails tailsis not the actual subsequencebisect_leftstrict;bisect_rightnon-decreasing
Likely follow-up: How would you reconstruct the actual subsequence in
O(n log n)? · How does LIS solve the Russian doll envelopes problem?50.How do you compute the longest common subsequence (LCS) of two strings?mid
Let
dp[i][j]be the LCS length of the firsticharacters ofaand the firstjcharacters ofb, with row and column 0 set to 0 for empty prefixes. Then:- If
a[i-1] == b[j-1], that character extends a common subsequence:dp[i][j] = dp[i-1][j-1] + 1. - Otherwise one of the two characters goes unused:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
The answer is
dp[m][n], inO(m · n)time and space. Each row depends only on the previous one, so the length alone needs justO(min(m, n))space. To recover the subsequence itself, keep the full table and walk back fromdp[m][n], moving diagonally on matches.For
"abcde"and"ace", the LCS is"ace", length 3.I clarify subsequence versus substring: a longest common substring must be contiguous, so on a mismatch its cell resets to 0 instead of taking the max. LCS underlies
difftools, and edit distance is a close relative with insert, delete and replace costs.What interviewers listen fordp[i][j]over prefixes; row and column 0 are 0- Match: diagonal value plus 1
- Mismatch: max of the top and left cells
O(m · n)time; rolling rows cut space- Substring variant resets to 0 on a mismatch
Likely follow-up: How would you compute the edit distance between two strings? · How would you print the LCS, not just its length?
- If
51.Explain the 0/1 knapsack problem and its DP solution. Why is it called pseudo-polynomial?hard
Given items with weights and values and a capacity
W, choose the subset with maximum total value that fits. "0/1" means each item is taken whole or not at all. Greedy by value-to-weight ratio fails here; it is only correct for the fractional version.Define
dp[i][c]as the best value using the firstiitems within capacityc. For each item there are two choices: skip it (dp[i-1][c]) or, if it fits, take it (dp[i-1][c - w] + v), and I keep the max. That isO(n · W)time.Each row reads only the previous one, so I compress to a 1D array and iterate capacity downwards. Going down guarantees
dp[c - w]still holds the previous row's value, so each item is used at most once. Iterating upwards would let an item be reused, which is exactly the unbounded knapsack.It is pseudo-polynomial because
O(n · W)is polynomial in the numeric valueW, which can be exponential in the number of bits needed to write it. The general problem is NP-hard.def knapsack(weights, values, capacity): dp = [0] * (capacity + 1) # dp[c] = best value within c for w, v in zip(weights, values): for c in range(capacity, w - 1, -1): # downwards: each item once dp[c] = max(dp[c], dp[c - w] + v) return dp[capacity]What interviewers listen for- Each item is taken whole or not at all
- Take or skip:
max(dp[c], dp[c - w] + v) - Iterate capacity downwards so each item is used once
- Upward iteration gives the unbounded knapsack
O(n · W)is pseudo-polynomial; NP-hard in general
Likely follow-up: How would you recover which items were chosen? · How does partition equal subset sum reduce to knapsack?
52.Which bit manipulation tricks should you know for coding interviews?easy
Beyond
&,|,^(xor),~and the shifts<<and>>, the tricks that come up most:- Check, set, clear, toggle bit
i:(x >> i) & 1,x | (1 << i),x & ~(1 << i),x ^ (1 << i). - Clear the lowest set bit:
x & (x - 1). Repeating it until zero counts set bits, andx > 0 and (x & (x - 1)) == 0tests for a power of two. - Isolate the lowest set bit:
x & -x, the core of Fenwick trees. - XOR cancels pairs:
a ^ a = 0anda ^ 0 = a, so XOR-ing every element finds the one value that appears once when all others appear twice, inO(1)space. - Bitmasks as sets: an integer can encode a subset of a small collection, for bitmask DP.
Python specifics: integers never overflow, and negatives act like infinitely sign-extended two's complement, so mask with
& 0xFFFFFFFFto emulate 32-bit behavior.int.bit_count()(Python 3.10+) counts set bits.What interviewers listen forx & (x - 1)clears the lowest set bit- Power of two:
x > 0and(x & (x-1)) == 0 x & -xisolates the lowest set bit- XOR cancels pairs: find the single number
- Python ints never overflow; mask to emulate 32 bits
Likely follow-up: How would you find the two numbers that appear once when all others appear twice? · How would you count the set bits of every number from 0 to
n?- Check, set, clear, toggle bit
53.Given a list of meeting time intervals, what is the minimum number of rooms required?mid
The answer is the maximum number of meetings in progress at the same moment. Two standard
O(n log n)approaches:- Min-heap of end times: sort meetings by start. For each meeting, if the earliest-ending room (the heap's root) has finished by this start, pop it to reuse that room; then push this meeting's end. The heap never shrinks, so its final size is the answer.
- Sweep with two sorted arrays: sort all starts and all ends separately. Walk through the starts; if a start comes before the earliest end not yet used, a new room is needed, otherwise a room frees up and I advance the end pointer. The peak count is the answer.
Both cost
O(n log n)for sorting andO(n)space.I clarify the boundary rule: if one meeting ends at 10 and another starts at 10, they can usually share a room, so the reuse check is
end <= start. The simpler variant, "can one person attend every meeting?", just sorts by start and checks that no meeting starts before the previous one ends.What interviewers listen for- Answer is the maximum simultaneous overlap
- Min-heap of end times; reuse a room if it is free
- Or sweep sorted starts and ends with two pointers
O(n log n)time,O(n)space- Clarify whether an end equal to a start can share
Likely follow-up: How would you report which meeting goes in which room? · How would you solve it without sorting if times are small integers?
54.How do counting sort and radix sort achieve linear time, and when are they a good choice?hard
Neither compares elements, so the
Ω(n log n)comparison lower bound does not apply.Counting sort handles integer keys in a known range of size
k. Count the occurrences of each key, turn the counts into prefix sums so each key knows where its block starts, then copy elements into those positions in input order, which keeps the sort stable. That isO(n + k)time and space: excellent whenkis aboutnor smaller, hopeless when keys span a huge range like all 64-bit integers.Radix sort handles larger keys one digit at a time. LSD radix sort goes from the least significant digit to the most, using a stable counting sort per digit, so the order from earlier digits survives later passes. With
ddigits in baseb, it isO(d · (n + b)), which is linear innfor fixed-width integers.The trade-offs: extra memory, keys must be integers or fixed-length strings, and for small or general inputs a good comparison sort often wins on constants.
What interviewers listen for- No comparisons, so the
n log nbound does not apply - Counting sort:
O(n + k)for key rangek - Prefix sums place elements stably
- LSD radix: stable pass per digit,
O(d · (n + b)) - Needs bounded integer-like keys and extra memory
Likely follow-up: Why must each digit pass in LSD radix sort be stable? · How would you sort a million 32-bit integers as fast as possible?
- No comparisons, so the
55.How would you find the maximum of every sliding window of size
kinO(n)time?hardBrute force takes the max of each window,
O(n · k). A heap with lazy removal of expired entries givesO(n log n). The optimal approach is a monotonic deque of indices whose values decrease from front to back.For each index
i:- Pop from the front if that index has left the window (it is
<= i - k). - Pop from the back while those values are
<=the new value; they can never be a window maximum again, because the new element is at least as large and stays in the window longer. - Push
ionto the back. - Once
i >= k - 1, the front of the deque holds the current window's maximum.
Each index is pushed and popped at most once, so the total is
O(n)time andO(k)space for the deque. Storing indices rather than values is what makes the expiry check possible.The same structure helps DP problems where each state needs the maximum of the previous
kstates.What interviewers listen for- Deque of indices with decreasing values
- Drop expired indices from the front
- Drop smaller values from the back before pushing
- The front is the window maximum
- Each index pushed and popped once:
O(n)
Likely follow-up: How would you adapt it to the sliding window minimum? · Why store indices rather than values in the deque?
- Pop from the front if that index has left the window (it is
56.How does heapsort work, and why is quicksort usually faster in practice despite heapsort having a better worst case?mid
Heapsort runs in two phases on the array itself:
- Build a max-heap with bottom-up heapify,
O(n). - Extract repeatedly: swap the root (the maximum) with the last element of the heap, shrink the heap by one, and sift the new root down,
O(log n)each. Aftern - 1rounds the array is sorted ascending.
That is
O(n log n)in the best, average and worst case withO(1)extra space, a stronger guarantee than quicksort'sO(n²)worst case. It is not stable.Quicksort usually wins in practice because of cache behavior: partitioning scans memory sequentially, while sift-down jumps from index
ito2i + 1, touching memory far apart in a large heap. Heapsort also tends to do more comparisons and swaps.That is why introsort, the usual C++
std::sort, runs quicksort and switches to heapsort only when recursion gets too deep, combining quicksort's speed with a guaranteedO(n log n). Heapsort alone suits cases where memory is tight and worst-case time matters.What interviewers listen for- Build a max-heap in
O(n) - Swap the root to the end, shrink, sift down
O(n log n)in all cases,O(1)extra space- Not stable, with poor cache locality
- Introsort falls back to heapsort for bad cases
Likely follow-up: Why does heapsort use a max-heap to sort in ascending order? · What triggers introsort to switch to heapsort?
- Build a max-heap with bottom-up heapify,
57.What is divide and conquer, and how do you analyze its running time with the master theorem?hard
Divide and conquer splits a problem into independent subproblems, solves them recursively, and combines the results. Merge sort, quicksort, binary search and fast multiplication algorithms all follow this shape. Unlike DP, the subproblems do not overlap, so there is nothing to cache.
The running time usually looks like
T(n) = a · T(n / b) + f(n):asubproblems of sizen / b, plusf(n)to split and combine. The master theorem comparesf(n)withn^(log_b a), the total work at the leaves:- If
f(n)grows polynomially slower, the leaves dominate:Θ(n^(log_b a)). - If they match, every level does equal work:
Θ(n^(log_b a) · log n). - If
f(n)grows polynomially faster (and meets a regularity condition), the root dominates:Θ(f(n)).
Examples: merge sort,
2T(n/2) + n, isΘ(n log n); binary search,T(n/2) + 1, isΘ(log n); Karatsuba,3T(n/2) + n, is aboutΘ(n^1.585). Recurrences likeT(n - 1) + ndo not fit the form, so I sum them directly or draw a recursion tree.What interviewers listen for- Split into independent subproblems, recurse, combine
- Recurrence
T(n) = a · T(n/b) + f(n) - Compare
f(n)withn^(log_b a) - Merge sort
Θ(n log n); binary searchΘ(log n) - Other recurrences: sum directly or use a recursion tree
Likely follow-up: What is the complexity of
T(n) = 2T(n/2) + n²? · How does the recurrence explain quicksort'sO(n²)worst case?- If
No questions match that filter.
Prefer multiple choice? All 27 Data Structures & Algorithms MCQs with answers →