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.
Implement an LRU cache in Python with get(key) and put(key, value) both running in O(1) time and a fixed capacity that evicts the least-recently-used entry on overflow. State the time and space complexity of each operation, then discuss what changes if the cache must be safe under concurrent access from multiple threads.
Sample Answer
Approach: Use Python's built-in collections.OrderedDict, which is a hash map that also maintains insertion/access order internally (backed by a doubly linked list under the hood) - move_to_end and popitem(last=False) give the O(1) reordering and eviction primitives directly, without hand-rolling the linked-list pointer surgery.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("capacity must be positive")
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key) # mark as most-recently-used
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False) # evict least-recently-used
Key points: get and put are each O(1) amortized because OrderedDict maintains its internal doubly linked list alongside the hash table, so move_to_end and popitem(last=False) are O(1) list-splice operations, not O(n) scans. Capacity is enforced on every put by evicting the oldest entry once size exceeds capacity.
Complexity: Both get and put are O(1) time. Space is O(capacity), since the cache never holds more than capacity entries.
Edge cases: capacity 0 is rejected explicitly (a cache that can hold nothing is a degenerate case worth guarding); updating an existing key's value correctly refreshes its recency without growing the cache past capacity; requesting a missing key returns a sentinel (-1 here, following the common LeetCode convention) rather than raising, though a production cache would more idiomatically raise KeyError or return None.
Worked example / execution verification
cache = LRUCache(2)
cache.put(1, 'a')
cache.put(2, 'b')
print(cache.get(1)) # promotes 1 to most-recent -> 'a'
cache.put(3, 'c') # over capacity, evicts 2 (least-recently-used)
print(cache.get(2)) # -1, was evicted
print(cache.get(1)) # 'a', still present
print(cache.get(3)) # 'c', still present
Executed: output is a, -1, a, c - exactly matching the hand-traced sequence from the conceptual companion question (key 2 is correctly evicted because the get(1) call promoted 1 ahead of it, leaving 2 as the least-recently-used entry when 3 was inserted over capacity).
Trade-offs & pitfalls
OrderedDictis not thread-safe - concurrentget/putcalls from multiple threads need an external lock (or a striped-lock / lock-free design, at the cost of significant added complexity) if this cache backs a multi-threaded service.- Hand-rolling the hash-map-plus-doubly-linked-list structure explicitly (rather than relying on
OrderedDict) is worth being able to do too - some interviewers specifically want to see the pointer manipulation, sinceOrderedDictis "using a library that already solved it." - For very large caches, consider whether the per-entry overhead of a linked-list node plus hash-map entry (roughly 3-4x a raw value's memory footprint in CPython) is acceptable, or whether a more compact structure (e.g. a ring buffer indexed by insertion slot, at the cost of O(capacity) eviction scans) is a better fit for the memory budget.
You need to maintain the top-K busiest items (API endpoints, keys) in a streaming fashion under continuous updates, with memory too limited to track every distinct item exactly. Compare an exact min-heap-plus-hashmap approach against a Count-Min-Sketch-plus-heap approximate approach: at what point does the exact approach's memory usage force you to switch, and what accuracy do you give up?
Sample Answer
Direct answer: An exact min-heap-plus-hashmap approach tracks every distinct item's exact count, giving perfectly accurate top-K results at the cost of memory proportional to the number of DISTINCT items seen - which becomes the bottleneck once the distinct-item cardinality is large or unbounded (e.g. tracking every possible API endpoint path including dynamically-generated ones, or every user ID). A Count-Min-Sketch-plus-heap approach caps memory to a fixed, chosen budget regardless of distinct-item count, at the cost of approximate (always-overestimated) counts for items that aren't clearly among the heaviest.
Structured elaboration
- Exact approach: a hash map tracks every distinct item's exact running count; a min-heap of size K tracks the current top-K by count, with the map used to look up and update the heap when an item's count changes. Memory is O(D) where D is the number of distinct items - for a workload with unbounded or very large D (e.g. per-user or per-IP tracking), this can exceed any reasonable memory budget even though only K results are ultimately reported.
- Approximate approach: replace the exact hash map with a Count-Min Sketch (fixed O(d×w) memory), updating the sketch per event and maintaining a heap of the K largest ESTIMATED counts seen so far (typically re-checking an item's current sketch estimate whenever it might displace the current K-th place). Memory is fixed regardless of D, at the cost of the sketch's inherent overestimation error potentially causing a slightly-wrong ranking near the K-th boundary (a borderline item might displace a truly-heavier one due to sketch noise, though the sketch's one-sided error means the reported "heavy" items are never UNDER-estimated).
Worked example
Tracking the top-100 busiest of potentially 50 million distinct API endpoint-plus-query-parameter combinations (a workload where D can be enormous due to parameter combinatorics): the exact approach would need memory for up to 50 million hash-map entries in the worst case - potentially many hundreds of megabytes to gigabytes, an unpredictable and workload-dependent footprint. A Count-Min Sketch sized at, say, d=5,w=10,000 (50,000 counters, roughly 200 KB at 4 bytes/counter) gives a FIXED memory footprint regardless of whether D turns out to be 50,000 or 50 million, at the cost of some estimation noise - crucially, the point at which memory savings become worth the accuracy trade is exactly when D is large or unpredictable enough that the exact approach's memory becomes a real operational risk (unbounded growth, potential OOM under adversarial or unusually diverse traffic).
Trade-offs & pitfalls
- The exact approach's memory is a function of ACTUAL distinct-item cardinality, which can be hard to bound in advance - a system designed assuming "at most a few thousand distinct items" can silently blow its memory budget if that assumption is violated by unexpected traffic patterns (this is a common real production surprise).
- The approximate approach needs its sketch dimensions (d,w) chosen with the same care as any Bloom-filter-family structure - too small a sketch introduces meaningfully wrong rankings, not just slightly noisy ones.
- A middle-ground option worth naming: track exact counts only for a bounded "candidate" subset (e.g. items seen more than some small threshold count) and fall back to approximate tracking for the long tail - this hybrid can capture most of the exact approach's accuracy for genuinely heavy items while bounding worst-case memory.
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).
Explain how memory access patterns (cache locality) affect real-world algorithm performance even when two approaches share the same Big-O complexity. Compare array-of-structures (AoS) versus structure-of-arrays (SoA) layout for iterating over one field across millions of records: same asymptotic complexity, why can one be several times faster in practice?
Sample Answer
Direct answer: Cache locality means accessing memory in a pattern that keeps the CPU's cache lines full of USEFUL data, rather than repeatedly evicting and re-fetching from slower main memory. Two algorithms with identical Big-O complexity can differ by an order of magnitude or more in real performance purely based on whether their memory access pattern is sequential (cache-friendly) or scattered (cache-hostile) - array-of-structures (AoS) versus structure-of-arrays (SoA) is the canonical example.
Structured elaboration
- Array-of-structures (AoS): each record is stored as one contiguous struct (
{id, name, score}), and an array of records places these structs one after another. Iterating over ALL records to read every field is cache-friendly (sequential access), but iterating to read just ONE field (say,score) across millions of records means the CPU loads an entire cache line's worth of struct data (includingidandname, which you don't need right now) for every few records - wasting cache bandwidth on unused fields. - Structure-of-arrays (SoA): each FIELD gets its own contiguous array (
ids[],names[],scores[]). Iterating over justscores[]for a computation touches only relevant, densely-packed data - every byte loaded into cache is useful, and the CPU's hardware prefetcher can predict the purely-sequential access pattern far more effectively. - The underlying mechanism: modern CPUs fetch memory in cache-line-sized chunks (commonly 64 bytes) and a cache miss (data not already in a fast cache) costs roughly 100-200x more cycles than a cache hit - so an access pattern that wastes cache-line capacity on unneeded data multiplies your effective memory traffic without changing the algorithm's Big-O class at all.
Worked example
Consider iterating over 10 million records, each with 4 fields (16 bytes total per struct: an 8-byte double score plus 8 bytes of other fields), summing only the score field:
- AoS: each cache line (64 bytes) holds 4 full structs, but only 8 of every 16 bytes per struct (the score) is useful - roughly 50% of loaded cache-line bytes are wasted on this specific access pattern (worse if there are more unrelated fields per struct).
- SoA: the
scoresarray alone is fully packed doubles - every byte loaded into a cache line is a score value actually being summed, 100% cache-line utilization for this access pattern.
Both approaches are Θ(n) time to sum n scores - identical Big-O - but the SoA layout does meaningfully less total memory traffic for this specific operation (bounded above by the wasted-byte fraction in the AoS case), which running an actual benchmark on representative hardware would show as a real, reproducible, and often substantial (multi-x) wall-clock difference, growing with how many "irrelevant" fields sit alongside the one you're actually scanning.
Trade-offs & pitfalls
- SoA is not universally better - if your access pattern typically touches MULTIPLE fields of the SAME record together (e.g. "for this specific user, get id, name, and score all at once"), AoS's locality-per-record wins instead, since SoA would scatter that lookup across three separate arrays.
- The right layout is a function of your DOMINANT access pattern, not a universal rule - profile the actual query/iteration shape before choosing, and be aware that a system serving mixed access patterns may need both layouts (or a hybrid) for different code paths.
- This is exactly the kind of gap that "Big-O is not the whole performance story" is pointing at - a correct complexity analysis (both are Theta(n)) is necessary but not sufficient for predicting real-world performance; memory-layout awareness is the complementary skill.
You must justify a complexity claim - for example 'this search operation is O(log n)' - to a non-technical stakeholder or in a design review. Walk through how you would explain it clearly: what analogy or concrete example you would use, what you would (and would not) simplify, and how you would address a follow-up like 'but will it scale to 10x the data?'
Sample Answer
Direct answer: Justifying a complexity claim like "this operation is O(logn)" to a non-technical stakeholder means translating the abstract bound into a concrete, relatable consequence - typically "even if the data grows 10x, 100x, or 1000x, the time this takes barely changes" - backed by one simple, memorable analogy (like a phone book or a guessing game) rather than the formal definition, and then being ready to address the natural follow-up about scaling with a concrete number, not just a repeated assurance.
Structured elaboration
- Lead with the consequence, not the notation: most stakeholders don't need to know what "O(logn)" means formally - they need to know "doubling our data roughly ADDS ONE STEP to this lookup, it doesn't double the time" - a framing anchored in what changes for THEM (cost, latency, whether the system will still work next year at 10x the data).
- Use a concrete analogy: the classic O(logn) analogy is guessing a number between 1 and 1,000,000 by always guessing the midpoint of the remaining range - it takes at most about 20 guesses, not a million, because each guess halves the remaining possibilities. This maps directly onto binary search / balanced-tree lookup and is intuitive without any notation at all.
- Address the "will it scale to 10x" follow-up with a NUMBER, not just reassurance: if the current system handles n=1,000,000 in roughly 20 "steps" (log base 2), scaling to 10,000,000 (10x) adds only about 3-4 more steps (log2(10)≈3.3) - a concrete, quantified answer beats a vague "don't worry, it scales well," and demonstrates you've actually done the math rather than asserting confidence.
Worked example
For a distributed index performing an O(logn) lookup, but where "n" is genuinely n= number of shards times log(shard size) plus network hops - the honest, precise version of the claim needs to ALSO name what's being hidden inside that Big-O: network round-trip latency per hop, consistency-protocol overhead (e.g. quorum reads needing multiple replicas to respond), none of which the asymptotic notation captures. A precise complexity argument to engineers on another team would explicitly separate "the number of comparison/routing STEPS scales logarithmically" from "the WALL-CLOCK cost per step includes a network round trip, which is a large, roughly-constant overhead per step, not itself shrinking" - conflating these two is a common way complexity claims mislead in distributed contexts specifically.
Trade-offs & pitfalls
- Over-simplifying for a non-technical audience is fine and often necessary, but silently dropping caveats that materially change the claim (e.g. "O(logn) ignoring network latency" presented as simply "fast") can come back to bite you when the system's actual behavior diverges from the simplified story - keep the honest caveats available even if you lead with the simple version.
- "Big-O" describes asymptotic GROWTH, not a guarantee about absolute latency at the current scale - a stakeholder asking "so it's fast?" deserves a distinction between "it stays proportionally cheap as data grows" and "it is fast in absolute terms right now," which are different claims.
- Prepare the "what if this assumption breaks" answer in advance (e.g. what happens if the data distribution becomes highly skewed, breaking an average-case assumption underlying the claimed bound) - a stakeholder's best follow-up question is often exactly the edge case your simplified explanation glossed over.
Unlock Full Question Bank
Get access to all Time and Space Complexity Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.