Structures Under Constraints
What is this?
Each of these hands you an interface and a complexity requirement, and the requirement is the design brief. O(1) random access rules out a linked list. O(1) minimum and maximum rules out a heap. O(1) eviction by frequency rules out sorting. What survives every time is a hash map for lookup paired with something that maintains order — and the work is keeping the two exactly in step.
O(1) min AND max → not a heap"] C --> C1["doubly linked list of count buckets"] B --> D["LFU Cache
O(1) eviction → not sorting"] D --> D1["freq → ordered dict, plus min_freq"] B --> E["Insert Delete GetRandom
O(1) random → must be an array"] E --> E1["swap with the last, then pop"] B --> F["Exam Room
maximise distance to nearest"] F --> F1["sorted seats, scan the gaps"]
💡 Fun fact: LFU Cache is usually attempted with a heap keyed by frequency, which gives
O(log n)and not the requiredO(1). The structure that does work is two levels deep: a map from frequency to an ordered collection of keys at that frequency, plus a single integermin_freq. Eviction is then "take the oldest key in themin_freqbucket" — constant time. Andmin_freqonly ever needs one specific repair: after promoting a key out of its bucket, if that bucket is now empty and it was the minimum,min_freqincreases by exactly one. It can never jump further, because the promoted key is now sitting atmin_freq + 1.
🔓 The 4 problems in this chapter are free. Sign in with Google or Microsoft to start solving.
The one-line idea: a hash map gives you
O(1)lookup and no order; a list, bucket chain or array gives you order and no lookup. Every problem here is the two of them, wired together and updated together.
1. Swap with the last, then pop
def remove(self, val):
if not self.idx[val]: return False
i = self.idx[val].pop() # any index holding val
last = self.arr[-1]
self.arr[i] = last # overwrite the hole
self.idx[last].add(i); self.idx[last].discard(len(self.arr) - 1)
self.arr.pop()
return True
Three lines of bookkeeping and every one is load-bearing. The moved element's index set must gain i and lose the old last position — and when val is the last element, add before discard is what keeps the set correct rather than emptying it.
Duplicates are why idx maps to a set of positions rather than to one. getRandom is then random.choice(self.arr), and each value appears in proportion to its multiplicity, which is precisely what the problem asks for.
2. Buckets, when a heap will not do
*All Oone* needs getMaxKeyandgetMinKeyinO(1)`, which no heap provides — a min-heap knows its minimum and a max-heap its maximum, never both. The structure is a doubly linked list of buckets in increasing count order, plus a map from key to its bucket:
head ↔ [count 1: {"a", "d"}] ↔ [count 3: {"b"}] ↔ [count 7: {"c"}] ↔ tail
inc moves a key to the next bucket, creating it if the neighbour's count is not exactly one more; dec moves it back. Max and min are the buckets adjacent to tail and head. Every operation touches a constant number of nodes, and the empty bucket must be unlinked or the ends stop being meaningful.
3. Maximise the distance to the nearest
Exam Room keeps the occupied seats sorted and, on each seat(), evaluates three candidate positions:
best = seats[0] - 0 # sit at 0: distance to the first
for a, b in zip(seats, seats[1:]):
if (b - a) // 2 > best: best, pos = (b - a) // 2, a + (b - a) // 2
if (n - 1) - seats[-1] > best: pos = n - 1 # sit at the far end
The two ends are special cases and the source of most wrong answers: sitting at seat 0 or seat n−1 has only one neighbour, so its distance is the full gap rather than half of it. Ties go to the smaller seat number, which falls out of using a strict > while scanning left to right.
4. A 30-second worked example (swap and pop)
arr = [1, 2, 2, 3], idx = {1: {0}, 2: {1, 2}, 3: {3}} — remove one 2.
i = 2 popped from idx[2] idx[2] = {1}
last = arr[-1] = 3
arr[2] = 3 arr = [1, 2, 3, 3]
idx[3].add(2) idx[3] = {2, 3}
idx[3].discard(3) idx[3] = {2}
arr.pop() arr = [1, 2, 3]
final: arr = [1, 2, 3] idx = {1:{0}, 2:{1}, 3:{2}} ✓ consistent
Order is destroyed — the surviving 2 is now before the 3 that used to be at the end — and that is fine, because nothing in the interface promised order. The only invariant that matters is that idx[v] holds exactly the positions where v lives, and after those five lines it does.
5. Where you'll actually meet this
- Cache layers. LFU is a real eviction policy, and this is how it is really implemented.
- Rate limiters. Counting per client with
O(1)hottest and coldest lookup is the All-O`one interface. - Sampling systems. Uniform selection from a mutable multiset backs A/B assignment and load shedding.
- Seat and slot allocation. Maximising distance to the nearest occupant is used in venues, parking and cell placement.
- Session and connection pools. Any "evict the least used" policy is this structure with the names changed.
6. Problems in this chapter
▶ All O`one Data Structure
Increment, decrement, and report the max and min keys, all in O(1). Doubly linked list of count buckets with a key → node map.
Pattern: bucket list + hash map. Target: O(1) every operation.
▶ Exam Room
Seat a student as far as possible from the nearest occupant, smallest index on a tie. Keep the seats sorted and evaluate both ends plus every gap.
Pattern: sorted structure with explicit boundary cases. Target: O(n) per call, or O(log n) with a gap heap.
▶ LFU Cache
Get and put in O(1), evicting the least frequently used and breaking ties by age. Frequency buckets of ordered keys, plus min_freq.
Pattern: two-level map with a minimum pointer. Target: O(1) get and put.
▶ Insert Delete GetRandom O(1) - Duplicates Allowed
A multiset with O(1) insert, remove and uniform random draw. Array of values plus a value → set of indices map.
Pattern: array + index sets, swap-and-pop. Target: O(1) average.
7. Common pitfalls 🚫
- Updating one structure and not the other. This is the bug in this chapter, every time.
discardbeforeaddin the swap-and-pop when the removed value is the last element, which empties the set.- Preserving order in
getRandom, which is not required and costs you the bound. - Reaching for a heap in LFU. It gives
O(log n), and the problem asks forO(1). - Failing to repair
min_freqafter a promotion empties its bucket. - Forgetting the two ends in Exam Room, where the distance is the whole gap, not half.
- Leaving empty buckets linked in All-O
one, which makesgetMaxKeyandgetMinKey` return nothing.
8. Key takeaways
- The stated bound is the design brief. Write it beside each method before choosing anything.
- Hash map for lookup, something ordered for order — every solution here is that pair.
- Swap-and-pop turns
O(n)deletion intoO(1)whenever order is not required. - Buckets replace the heap when you need both extremes, or constant-time eviction.
- A minimum pointer only ever moves by one after a promotion, which is why it stays
O(1). - Boundaries are where these break — the ends of a row, the last element of an array, the empty bucket.
- Why interviewers like it: the design is half the answer and the synchronisation is the other half, and both are visible in the code you write.
Order: All O`one Data Structure → Exam Room → LFU Cache → Insert Delete GetRandom O(1) - Duplicates Allowed.
Core idea: Keep keys grouped into buckets by their exact count, and chain those buckets in a doubly-linked list ordered by count. Incrementing or decrementing a key just hops it to the neighbouring bucket, and the min/max keys are sitting at the two ends of the list — so every operation is O(1).
Problem, rephrased
You're building a tiny frequency dashboard for a streaming service. Every time a song is played you call inc(song); every time a play is retracted (a skip inside the grace window) you call dec(song). At any moment a producer wants to flash two things on screen instantly: the most-played track and the least-played track currently on the board. The catch — these queries fire thousands of times a second, so each of inc, dec, getMaxKey, and getMinKey must run in O(1).
Formally, design a data structure supporting:
inc(key)— increase the count ofkeyby 1; ifkeydoes not exist, insert it with count 1.dec(key)— decrease the count ofkeyby 1; if its count hits 0, removekey. (decon a missing key is a no-op.)getMaxKey()— return any key with the largest count, or""if the structure is empty.getMinKey()— return any key with the smallest count, or""if the structure is empty.
All four operations must be average O(1).
| Operation | State after (key→count) | Return |
|---|---|---|
inc("a") |
{a:1} |
— |
inc("b") |
{a:1, b:1} |
— |
getMaxKey() |
{a:1, b:1} |
"a" or "b" (tie) |
inc("b") |
{a:1, b:2} |
— |
getMaxKey() |
{a:1, b:2} |
"b" |
getMinKey() |
{a:1, b:2} |
"a" |
dec("b") |
{a:1, b:1} |
— |
dec("a") |
{b:1} |
— (a hit 0, removed) |
getMinKey() |
{b:1} |
"b" |
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson