When to use data structure design
Use these designs when you need:
- A cache with a size limit that keeps the useful items and drops the rest: an LRU (least recently used) or LFU (least frequently used) cache.
- A structure that answers an extra question instantly, such as the minimum of a stack, the maximum of a window, or a random element.
- A count over a recent period, for example hits in the last five minutes or requests per second for rate limiting.
- An interview answer where the prompt says "all operations must be O(1)".
Use something else when:
- A built-in structure already fits: Python's
OrderedDict, Java'sLinkedHashMap, or a library cache such as Caffeine or Guava. Use the library in production code. - The cache must be shared between many servers: use an external cache such as Redis or Memcached, and see the System Design lessons.
- You only need the top k items: a heap is simpler.
Do not write your own cache in production unless you have a reason. Thread safety, expiry and statistics are hard to get right, and good libraries exist. These problems teach you how to combine structures, which is the real skill.
How a design problem works
A design problem asks you to build a small structure with a list of operations, each with a target cost. The method is always the same:
- List the operations and the cost each must have, for example
getandputin O(1). - Find a structure for each cost. A hash map gives O(1) lookup. A linked list gives O(1) insert and remove at a known place. A stack gives O(1) at the top. A queue gives O(1) at both ends.
- Combine them so each covers the other's weakness.
No single basic structure does everything. The skill is gluing two of them together so that every operation has the right cost.
The three classics:
- LRU cache = hash map + doubly linked list. The map finds a key in O(1) but knows nothing about order. The list keeps the keys in order of use but cannot find one quickly. The map stores key to list node, so the node is found in O(1) and moved to the front in O(1). The back of the list is the least recently used, and is evicted when the cache is full. The list must be doubly linked so a node can be removed without searching for the one before it.
- Min stack = a stack that remembers the minimum. Store with each value the smallest value at or below it. The minimum is the number on top. A pop removes both, and the previous minimum is already stored underneath.
- Hit counter = a queue of timestamps. Add each hit at the back. To count, drop timestamps from the front that are older than the window, then return the size. Each timestamp is added once and removed at most once.
An LFU cache is the next step: it evicts the key used the fewest times, and breaks ties by evicting the least recently used of those. It keeps a map from each use count to a list of keys, plus the smallest count in the cache, so it is still O(1).
Try it
Pick one of the three problems, then press Run and step through the operations.
Design problems
Step 1 of 10 · Press Space to play, arrow keys to stepOperations separated by ; for example put 1 1; get 1
Cache, most recently used first (key:value)
The cache is empty.
Hash map keys: none
An LRU cache holds at most 2 items. A hash map finds an item in O(1), and a list keeps them in order of use so the least recently used one is easy to drop.
- hits:
- 0
- misses:
- 0
- evictions:
- 0
- items in cache:
- 0
- capacity:
- 2
Every operation here costs constant time on average, however many items are stored.
Things to try:
- LRU cache, "Classic": with capacity 2, the operations
put 1 1; put 2 2; get 1; put 3 3; get 2; put 4 4; get 1; get 3; get 4give 3 hits, 2 misses and 2 evictions. Reading key 1 saves it, so key 2 is evicted when key 3 arrives. - Min stack, "Push and pop": after
push 5; push 3; push 7,minreturns 3. After onepopit is still 3, and after a secondpopit returns 5, with no scanning. - Hit counter, "Busy then quiet" with a 5-second window: three hits at seconds 1, 2 and 3 count as 3 at second 4. At second 7 only the hits at seconds 3 and 6 are inside the window, so the answer is 2. At second 20 the answer is 0.
Data structure design operations
LRU get(key).
- Look the key up in the hash map. If it is missing, return -1.
- Move its node to the front of the list, and return the value.
LRU put(key, value).
- If the key exists, update its value and move the node to the front.
- Otherwise add a new node at the front and add the key to the map.
- If the size is now over the capacity, remove the node at the back and delete its key from the map.
Min stack.
push(v): storevandmin(v, the stored minimum below).pop(): remove the top item and its stored minimum.getMin(): return the stored minimum on top.
Hit counter. hit(t) adds t at the back. count(t) removes timestamps at or before t - window from the front, then returns the number left.
Interview fact. The hash map stores the node itself, not just the value. That is the trick that makes "find it" and "move it" both O(1).
Data structure design time complexity
| Structure | Main operations | Time | Memory |
|---|---|---|---|
| LRU cache | get, put | O(1) on average | O(capacity) |
| LFU cache | get, put | O(1) on average | O(capacity) |
| Min stack | push, pop, top, getMin | O(1) | O(n) |
| Hit counter (queue) | hit, count | O(1) amortised | O(hits in one window) |
| Hit counter (one bucket per second) | hit, count | O(1) and O(window) | O(window) |
The hash map operations are O(1) on average. The hit counter is amortised because one count call may drop many old timestamps, but each timestamp is dropped only once.
Design structures vs other approaches
n is the number of stored items.
| What you need | Combined design | Hash map only | Sorted structure | Scan every time |
|---|---|---|---|---|
| Look up by key | O(1) | O(1) | O(log n) | O(n) |
| Know which item is least recently used | O(1) | O(n) | O(log n) | O(n) |
| Minimum of a stack | O(1) | Not applicable | O(log n) | O(n) |
| Hits in the last window | O(1) amortised | O(n) | O(log n) | O(n) |
The bold cells show where each wins. The combined designs match the hash map on lookup and beat everything else on the extra question. They cost more memory and more code, and they are not safe for several threads without a lock.
How to explain a design problem in an interview
- Clarify first: capacity, what
getreturns on a miss, whether an update counts as a use, whether it must be thread-safe. - List the operations and their target costs, then say which structure gives each cost and why one is not enough.
- For the LRU cache, say the key sentence: "the map stores the node, so I can find it and move it in O(1), and the list must be doubly linked."
- Name the edge cases: capacity 1, updating an existing key, a miss, an empty structure.
- State the costs: O(1) time per operation, O(capacity) memory, and name the built-in that does this for you, for example
LinkedHashMap.