Time and Space Complexity Analysis Questions
Reasoning about algorithmic efficiency: Big-O/Theta/Omega notation, amortized analysis, recurrence solving, and the time-versus-space trade-off. Covers deriving bounds from code, comparing candidate approaches, and communicating complexity clearly under interview pressure. The analytical layer applied across every algorithm topic.
Describe an algorithmic approach to detect 'hot keys' in a distributed key-value or sharded workload so you can redistribute load before a single shard becomes a bottleneck. Compare exact counting, sampling-based detection, and heavy-hitter sketch algorithms on their time, space, and detection-latency trade-offs.
Sample Answer
Direct answer: Detecting hot keys (disproportionately accessed shard keys) in a distributed workload trades off between exact counting (accurate but memory-proportional-to-distinct-keys, similar to the exact-vs-approximate top-K trade-off), sampling (cheap and low-overhead, but noisy for keys just above the detection threshold), and heavy-hitter sketch algorithms like Count-Min Sketch (bounded memory regardless of key cardinality, with a tunable but nonzero estimation error).
Structured elaboration
- Exact counting: maintain a per-key access counter (e.g. in a shared hash map or hardware counter array), flag any key exceeding a threshold. Perfectly accurate, but memory scales with the number of DISTINCT keys ever accessed - for a workload with a very large or unbounded key space, this can itself become a scalability problem (ironic, given the goal is detecting a scalability problem).
- Sampling: only track a random subset of requests (e.g. 1-in-1000), extrapolating observed frequencies to estimate full-traffic frequencies. Memory is bounded by how many DISTINCT keys appear in the sample (much smaller than the full key space), and detection latency/overhead is low - but for a threshold near the sampling rate's noise floor, sampling can both miss genuinely hot keys (false negative, if the sample happened to under-represent them) and flag non-hot keys as hot (false positive, from sampling variance) - the accuracy trade is fundamentally statistical, not just "less precise."
- Heavy-hitter sketch (Count-Min Sketch or similar): fixed memory regardless of key cardinality, with a one-sided (always-overestimate) error bound as derived in the companion sketch survivor - directly applicable here, since detecting "this key's estimated frequency exceeds a threshold" is exactly the heavy-hitters use case these structures are designed for.
Trade-offs & pitfalls
- Detection LATENCY matters as much as accuracy for this use case: a hot key causing real-time overload needs fast detection (favoring lightweight sampling or sketch-based approaches that update in O(1) per request) over a batch-computed exact count that might only be available minutes later, by which point the overload has already caused damage.
- The action taken on detecting a hot key (redistributing load, e.g. via additional replication or key-splitting/salting) has its own cost and complexity - detection is only half the problem, and a scheme that detects too aggressively (low threshold, high false-positive rate) can trigger unnecessary, costly rebalancing.
- Combining approaches is common in practice: cheap sampling or sketch-based detection as a fast first-pass alarm, with exact counting reserved for confirming and precisely measuring an already-suspected hot key, rather than running exact counting continuously across the full key space.
Analyze the time complexity of beam search for sequence generation, in terms of sequence length L, beam width B, and vocabulary size V. Propose at least one pruning strategy (top-k candidate limiting, score-threshold pruning) that reduces the practical cost without changing the worst-case bound, and explain the accuracy/speed trade-off beam width controls.
Sample Answer
Direct answer: Beam search's naive complexity is O(L x B x V) - for each of L generation steps, you consider expanding each of the B current beam candidates by every one of V vocabulary tokens, then keep only the top B resulting sequences. The V factor (vocabulary size, often tens of thousands to hundreds of thousands for modern language models) usually dominates in practice, making vocabulary-side pruning the highest-leverage optimization.
Structured elaboration
- At each of L steps: each of the B beam candidates can be extended by any of V possible next tokens, giving B×V candidate continuations to score - scoring each candidate typically costs O(1) given the model's per-token output distribution is already computed, but SELECTING the top B from B×V candidates (if done via a full sort) costs O(BVlog(BV)), or O(BVlogB) with a more efficient top-B selection (e.g. a bounded heap) - either way, dominated by the BV term for realistic B and V.
- Total across L steps: O(L×B×V) (ignoring the smaller log factor from top-B selection, or including it as O(L×BVlogB) for a more precise accounting).
- Pruning strategies: top-k restricts each step's candidate continuations to only the k highest-probability next tokens per beam (rather than all V), reducing the per-step candidate pool to B×k instead of B×V - a direct reduction proportional to k/V, often a massive factor for large vocabularies. Score-threshold pruning discards candidates whose cumulative score falls below some margin relative to the current best candidate, adaptively narrowing the search without a fixed k.
Worked example
For L=50 (output length), B=4 (beam width), V=50,000 (a realistic vocabulary size): naive per-step candidate count is 4×50,000=200,000, across 50 steps giving 10,000,000 total candidate evaluations. Restricting to the top k=100 next-tokens per beam (top-k pruning) before beam-scoring reduces per-step candidates to 4×100=400 - a 500x reduction (200,000/400), and across 50 steps, 20,000 total candidate evaluations instead of 10 million - a dramatic, directly quantifiable win from vocabulary-side pruning specifically, which is why it's the standard first optimization applied in practice, ahead of any beam-width reduction.
Trade-offs & pitfalls
- Increasing beam width B improves output quality (explores more candidate sequences) but increases cost linearly in B - and past a certain point, wider beams give DIMINISHING quality returns (a well-documented empirical phenomenon in sequence generation, sometimes even producing worse outputs past some beam width due to length-normalization or search-error interactions) - "wider is always better" is a common but incorrect assumption.
- Vocabulary-side pruning (top-k or nucleus/top-p sampling-style restriction) is usually the highest-leverage lever since V is typically much larger than B - always check whether the vocabulary term, not the beam-width term, is the actual bottleneck before optimizing beam width.
- Batched scoring (computing model outputs for the whole current beam set in one batched forward pass rather than B separate passes) is a real-world engineering optimization that doesn't change the O(L x B x V) asymptotic bound but substantially improves wall-clock throughput on parallel hardware - worth naming as complementary to the algorithmic pruning strategies.
Walk through the standard complexity classes from O(1) up through O(2^n): constant, logarithmic, linear, linearithmic (n log n), quadratic, and exponential. For each, give a concrete operation or algorithm that lands there, then order all six by growth rate and explain why the distinction matters as input size scales.
Sample Answer
Direct answer: From fastest- to slowest-growing, the standard ladder is: constant O(1), logarithmic O(logn), linear O(n), linearithmic O(nlogn), quadratic O(n2), and exponential O(2n). Each step up means the algorithm's cost grows dramatically faster as n increases, and the gap between adjacent classes widens without bound as n→∞.
Structured elaboration
| Class | Example operation/algorithm |
|---|---|
| O(1) | Array index access, hash-table lookup (average case) |
| O(logn) | Binary search on a sorted array, balanced-BST lookup |
| O(n) | Linear scan, finding the max of an unsorted array |
| O(nlogn) | Comparison-based sorting (mergesort, heapsort) |
| O(n2) | Naive nested-loop pairwise comparison, insertion sort |
| O(2n) | Naive recursive Fibonacci, brute-force subset enumeration |
The reason the distinction matters in practice is that these classes diverge explosively: doubling n costs a constant-time algorithm nothing, an O(logn) algorithm one extra step, an O(n) algorithm double the work, an O(n2) algorithm quadruple the work, and an O(2n) algorithm makes the problem astronomically harder (one more bit doubles the whole runtime).
Worked example
Take n=1,000,000 and assume 109 operations/second (a reasonable single-core estimate):
- O(logn): log2(106)≈20 operations → effectively instant.
- O(n): 106 operations → about 1 millisecond.
- O(nlogn): 106×20=2×107 operations → about 20 milliseconds.
- O(n2): 1012 operations → about 1,000 seconds (over 16 minutes).
- O(2n): astronomically larger than the number of atoms in the observable universe; not computable in this age of the universe for n this large.
This is why "just make it O(nlogn) instead of O(n2)" is often the single highest-leverage change you can make to a slow system, and why exponential algorithms are only acceptable for genuinely tiny n (or when no polynomial algorithm is known, as with NP-hard problems).
Trade-offs & pitfalls
- These are asymptotic classes - for small n, an algorithm with worse asymptotic complexity can still be faster in wall-clock time because of a smaller constant factor (see the dedicated O(n log n) vs O(n) practical trade-off question).
- Don't conflate "quadratic" with "always bad" - for genuinely small, bounded n (say, under a few hundred), an O(n2) algorithm is often the pragmatic, simplest-to-verify choice.
- Watch for hidden multi-variable complexity: "linear in n" can hide an O(n⋅m) cost if there's a second, unstated dimension m (e.g. string length, or number of fields per record).
Explain how hash tables handle collisions via separate chaining versus open addressing, including the average-case and worst-case complexity of get/put/delete under each. Then explain how an attacker who can choose the keys can degrade every lookup to O(n) (a hash-flooding attack), and what mitigations (randomized hash seeding, safer hash functions) restore the average-case guarantee.
Sample Answer
Direct answer: Separate chaining stores colliding keys in a linked list (or small array/tree) per bucket; open addressing (linear/quadratic probing, double hashing) stores every key directly in the table itself, probing to the next slot on collision. Both give average-case O(1) get/put/delete under a good hash function and bounded load factor, but an attacker who can choose (or predict) keys that all hash to the same bucket can force every operation to O(n) - a real denial-of-service vector, not just a theoretical curiosity, mitigated by randomized hash seeding at process startup and hash functions resistant to seed-independent collision construction.
Structured elaboration
- Separate chaining: each bucket holds a small collection (commonly a linked list, or - in some modern implementations like Java 8+'s HashMap - a balanced tree once a bucket grows large) of all keys hashing there. Lookup cost is O(1 + chain length); with load factor kept bounded, expected chain length is O(1).
- Open addressing: on collision, probe subsequent slots (linear: next slot; quadratic: quadratically-increasing offsets; double hashing: a second hash function determines the probe sequence) until an empty slot is found. Avoids the pointer-chasing overhead of chaining (better cache locality, since probing stays within the contiguous backing array), but degrades faster as load factor approaches 1 (must keep load factor well below 1, commonly under 0.7, whereas chaining degrades more gracefully).
- The attack: if the hash function is known (or its output is predictable, e.g. a naive non-cryptographic hash with a fixed seed) and an attacker controls input keys (e.g. HTTP form-field names, JSON keys), they can construct a large set of keys that all collide into the same bucket(s), degrading every operation from O(1) to O(n) and creating an algorithmic denial-of-service - a single request with thousands of colliding keys can pin a server's CPU.
Worked example
This is not hypothetical: this exact vulnerability class ("hash flooding") was responsibly disclosed and patched across essentially every major web-application language runtime around 2011-2012, after researchers demonstrated that a small POST request with carefully-crafted colliding form-field names could consume many CPU-seconds parsing what should have been a millisecond-scale request. The fix adopted across languages was randomizing the hash seed per-process at startup (so the attacker can't predict the collision-inducing keys ahead of time without knowing the runtime's random seed) and, in some cases, switching to collision-resistant hash functions (like SipHash) specifically designed to make seed-independent collision construction computationally infeasible even if an attacker can observe hash outputs.
Trade-offs & pitfalls
- "Average-case O(1)" is a claim about TYPICAL inputs, not a security guarantee - any system that hashes attacker-controlled keys needs the worst-case-resistant mitigations (randomized seeding, or a cryptographically-motivated hash), not just a "good enough in practice" hash function.
- Randomized per-process seeding means hash order becomes non-deterministic across process restarts - code that accidentally depends on hash-iteration order (a common latent bug) will surface intermittently once seeding is randomized.
- Open addressing's degradation is sharper (clustering effects can compound near a full table) than chaining's, so open-addressing implementations typically resize more aggressively (lower load-factor threshold) to stay safely away from the cliff.
Explain how to design an LRU (least-recently-used) cache that supports get and put in O(1) time. Which two data structures do you combine, and why does neither one alone (just a hash map, or just a doubly linked list) achieve O(1) for both operations?
Sample Answer
Direct answer: An LRU (least-recently-used) cache combines a hash map (for O(1) key lookup) with a doubly linked list (for O(1) reordering and eviction) - a hash map alone can't track recency order in O(1), and a linked list alone can't look up a key in O(1); the combination gives each structure the job it's good at.
Structured elaboration
- A hash map alone gives O(1)
get/putby key, but has no notion of "which key was used least recently" without an O(n) scan - you'd need to store and update timestamps, then scan all entries to find the minimum, which is O(n) per eviction. - A doubly linked list alone naturally tracks recency (move a node to the front on every access, evict from the back), but finding a node by KEY to move it requires an O(n) linear search through the list.
- Combined: the hash map stores
key -> nodepointers into the linked list.get(key): hash-map lookup finds the node in O(1), then the node is unlinked and relinked at the front of the list in O(1) (doubly-linked, so both neighbors are known without a search).put(key, value): same O(1) lookup-and-move if the key exists, or O(1) insertion at the front plus (if over capacity) O(1) removal of the tail node, whose key is then also removed from the hash map. - The critical design detail: the hash map's VALUE isn't the cached value directly - it's a pointer/reference to the linked-list NODE, so that once you've found the node via the hash map, you can splice it within the list in O(1) without any further lookup.
Worked example
Trace put(1,'a'), put(2,'b'), get(1), put(3,'c') on a capacity-2 cache:
put(1,'a'): list =[1](front=back=1), map ={1: node1}.put(2,'b'): list =[2,1](2 is now most-recent, at front), map ={1: node1, 2: node2}.get(1): hash lookup finds node1 in O(1); since node1 is not already at the front, unlink it and relink at front: list =[1,2]. Returns'a'.put(3,'c'): over capacity (2 items already), so first evict the tail (2, the least-recently-used): remove node2 from the list AND delete key2from the map. Then insert 3 at the front: list =[3,1], map ={1: node1, 3: node3}.
Key 2 was correctly evicted because it was least-recently used relative to the get(1) access that promoted 1 - both the eviction (tail removal) and the promotion (move-to-front) happen in O(1) because the linked list's structure means every splice only touches a constant number of neighboring pointers.
Trade-offs & pitfalls
- If you used a SINGLY linked list instead of doubly linked, moving an arbitrary node to the front would require knowing its predecessor to unlink it - which means an O(n) search, defeating the purpose. The "doubly" part is not incidental; it's what makes O(1) splicing possible.
- Many candidates correctly identify "hash map + linked list" but then can't explain WHY neither alone suffices - be ready to name the specific operation (find-by-key for the list, recency-tracking for the map) that the other structure covers.
- This same hash-map-plus-doubly-linked-list pattern generalizes to any "O(1) lookup plus O(1) reordering" requirement, not just LRU - it's worth recognizing as a reusable pattern (e.g. LFU (least-frequently-used) caches use a similar idea with an extra layer for frequency buckets).
Unlock Full Question Bank
Get access to all 47 Time and Space Complexity Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.