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.
Insert Delete GetRandom O(1) - Duplicates Allowed
Core idea: Keep every occurrence (duplicates included) in a flat array so a random index is O(1) and
getRandom's probability scales with multiplicity for free. Beside it keep a mapvalue → a SET of the indices where that value lives. To remove one copy, grab any index from that set, swap that element with the last array element, repair the moved element's bookkeeping in its set, then pop the tail — an O(1) hole-free deletion that survives duplicates.
This is the harder sibling of Insert Delete GetRandom O(1) (LeetCode 380, the no-duplicates RandomizedSet). If you haven't internalized that one — array for O(1) random, hash map for O(1) locate, swap-with-last for O(1) delete — read it first; everything here is a single, surgical upgrade of it. The twist: now a value can appear many times, and getRandom must return each value with probability proportional to how many copies it has. A scalar value → index map can no longer locate "a" copy because there are several. The fix is to make the map's values sets of indices.
Problem, rephrased
You're running the standby list at an airport gate.
- A passenger can be added with
insert(name). The same passenger may legitimately appear more than once — they bought two standby slots, or two unrelated people share a name. Either way, everyinsertadds another physical slot.insert(name)returnsTrueifnamewas not already on the list (i.e. this is its first copy) andFalseif it was already present, but it adds the copy regardless. - One slot can be removed with
remove(name): a single copy of that name comes off the list. ReturnsTrueif at least one copy existed (and one was removed),Falseif the name wasn't on the list at all. The other copies, if any, stay. getRandom()calls a name to fill an open seat, with every physical slot equally likely — so a passenger holding three of the ten slots has a 3/10 chance, not 1/10. Probability is proportional to multiplicity.
The list churns all day, and all three actions must be average O(1) no matter how long it grows.
| Operation | Input | Returns | Behavior |
|---|---|---|---|
insert(x) |
a value | True if x was absent before this call, else False |
always adds one copy |
remove(x) |
a value | True if a copy was removed, False if x absent |
removes exactly one copy |
get_random() |
— | a value currently stored | each slot equally likely → P(x) ∝ count(x) |
Two rows differ from LC380. insert's return is now "was it absent before?" (it still inserts on a True and a False). remove peels off one copy, not all of them. Those two changes are exactly what force the index set.
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