Builds on Hash Map, which you haven’t finished yet.
Array + Index Map (Swap-with-Last Delete)
Build a class, RandomizedSet, that stores a set of distinct integers and can hand back one of them at random. Every operation has to take O(1) time on average.
RandomizedSet()creates an empty set.bool insert(int val)addsvalif it is missing. It returnstruewhenvalwas added andfalsewhen it was already there.bool remove(int val)deletesvalif it is there. It returnstruewhenvalwas deleted andfalsewhen it was not in the set.int getRandom()returns one of the stored values, chosen so that every value in the set at that moment is equally likely.
insert(1) adds 1. remove(2) returns false: 2 is not stored. insert(2) adds 2, so the set is {1, 2} and the first getRandom() may return 1 or 2, each with probability 1/2 (LeetCode shows 2). remove(1) leaves {2}, insert(2) returns false because 2 is already stored, and the last getRandom() can only return 2.Insert Delete GetRandom
values list + index map ➔ move the last value into the gap ➔ O(1) delete and uniform pickO(1) on average.| 1 | import random |
| 2 | |
| 3 | |
| 4 | class RandomizedSet: |
| 5 | """Insert Delete GetRandom O(1): a gap-free list for the random pick, a map for each value's slot. |
| 6 | Time: O(1) average per call | Space: O(N) |
| 7 | """ |
| 8 | |
| 9 | def __init__(self) -> None: |
| 10 | self.values: list[int] = [] # Every value once, no gaps: random.choice is uniform |
| 11 | self.index: dict[int, int] = {} # value -> its slot in self.values |
| 12 | |
| 13 | def insert(self, val: int) -> bool: |
| 14 | # 1. Reject A Duplicate, Else Append At The Next Free Slot: |
| 15 | # The list is always gap-free, so its current length is exactly the slot the new value will occupy. |
| 16 | if val in self.index: |
| 17 | return False |
| 18 | self.index[val] = len(self.values) # The next free slot is the end |
| 19 | self.values.append(val) |
| 20 | return True |
| 21 | |
| 22 | def remove(self, val: int) -> bool: |
| 23 | # 1. Fill val's Slot With The Last Value, Then Drop The Last Slot: |
| 24 | # A set has no order, so moving the last value into the freed slot keeps the list gap-free |
| 25 | # without shifting anything; the moved value's own map entry must be fixed before val's entry is deleted. |
| 26 | if val not in self.index: |
| 27 | return False |
| 28 | i = self.index[val] |
| 29 | last = self.values[-1] |
| 30 | self.values[i] = last # Fill the hole with the last value |
| 31 | self.index[last] = i # Update the moved value's slot first... |
| 32 | del self.index[val] # ...then delete: right even when last == val |
| 33 | self.values.pop() # The last slot is now a copy: drop it in O(1) |
| 34 | return True |
| 35 | |
| 36 | def getRandom(self) -> int: |
| 37 | return random.choice(self.values) # One slot per value: each has a 1-in-n chance |
self.values holds every stored value exactly once with no gaps, and self.index[v] is always the slot of v in it.A pick where every value is equally likely needs the values packed in a list, random.choice(self.values), and a list can only drop its last slot without shifting the rest, so remove fills the hole with the last value instead of shifting: self.values[i] = last, then self.values.pop(). A set has no order, so moving one value changes nothing a caller can see, and self.index finds i with one lookup; the list stays gap-free, so every value keeps exactly one slot and the same 1-in-n chance. The trap is the order of the two map writes: self.index[last] = i must come before del self.index[val]. When val is itself the last value, last == val, and deleting first writes val straight back into the map at a slot that pop() removes, so a later insert(val) returns False.
See it run
Insert Delete GetRandom: swap with the last value, ops = insert(1), insert(2), insert(3), remove(2), remove(3), insert(3)
index {}values []
The row is the list values, one box per slot, numbered from 0. The readouts show the dictionary index.
newthe slot that insert just filledslot ithe slot of the value being removedlastthe last slot, which holds the value that movesi is lastthe value to remove is already in the last slot
The set is empty. values has no values, and index has no entries.
Gotcha 1 of 3
- The trap
Gotcha 1: Deleting
self.index[val]before writingself.index[last]Mistakedel self.index[val] self.index[last] = iFixself.index[last] = i del self.index[val]What happens: When
valis the last value,last == val. Deleting first removesval, thenself.index[last] = iwrites it back.pop()removes the slot, but the map still says thatvalis present. A laterinsert(val)then returns False.Example: insert(1), remove(1), then insert(1)Mistake: returns FalseFix: returns True
- The structure must keep an order, the order of use as in LRU Cache (LC 146), so no value may be moved into another's slot. See LRU Cache
Deep Dive
Why the last value can fill the hole
getRandom must pick every stored value with equal chance. random.choice(self.values) does that when self.values holds every value once and has no gaps. Each value then owns one slot, and each slot has a 1-in-n chance.
A list can drop only its last slot in O(1). self.values.pop(i) for another i shifts every later item, which costs O(N). So remove avoids the shift. It copies the last value into slot i, and then drops the last slot.
This is allowed because a set has no order. Moving one value to another slot changes nothing a caller can see.
Trace insert(1), insert(2), insert(3). Now values is [1, 2, 3] and index is {1: 0, 2: 1, 3: 2}. remove(2) reads i = 1 and last = 3. It sets values[1] = 3, sets index[3] = 1, deletes index[2], and pops. The result is values = [1, 3] and index = {1: 0, 3: 1}.
This is why the card's invariant says that self.values holds every stored value once with no gaps, and self.index[v] is the slot of v. After each call, both facts hold again.
The order of the two map writes
remove changes two map entries: self.index[last] = i and del self.index[val]. The first must come before the second.
Look at the case where val is the last value. In the trace, after remove(2), remove(3) has i = 1 and last = 3. Here last == val. The code sets values[1] = 3, which changes nothing. Then it sets index[3] = 1, which repeats the old entry. Then it deletes index[3]. The map no longer holds 3, and pop() drops the slot. The state is values = [1] and index = {1: 0}.
Now swap the two lines. del self.index[3] runs first, and self.index[3] = 1 writes the entry back. pop() removes the slot, but the map still says that 3 is stored. A later insert(3) returns False. With the correct order, insert(3) returns True and gives values = [1, 3].
The same order also keeps the card's other case safe. When val is not the last value, the two lines touch different keys, so their order does not matter.
Cost, and why the order of values may change
insert, remove and getRandom each do a fixed number of dictionary and list-end operations. Each is O(1) on average. The space is O(N) for values and index.
The values do not keep insertion order. After remove(2), the value 3 moved from slot 2 to slot 1. The problem asks only for a random pick, so this is fine. If you need the order, this card does not fit.
A check of the pick: in a run of 30,000 calls on values = [4, 5, 6], each value came up about 10,000 times. A skewed result would mean that a value owns two slots or that a slot is empty.
Duplicates: slots as a set
Insert Delete GetRandom O(1) - Duplicates allowed (LC 381) allows the same value many times, and a pick must follow the number of copies. The list then holds one slot per copy.
index[val] becomes the set of slots that hold val. remove takes any slot i from that set, copies last into slot i, moves the slot number of last from the end to i, and pops. Add i to the set of last first, and then discard the old last slot number. The order matters again when i is the last slot.
insert returns True only when the set for the value was empty before the call.
60-Second Pitch
"A uniform pick needs the values in a packed list, and O(1) delete needs the position of a value. So I keep values as a list with no gaps and index as a map from value to slot. insert appends and records the slot. remove reads the slot i of the value, copies the last value into slot i, fixes the map entry of that moved value, deletes the entry of the removed value, and pops the last slot. I write the moved value's entry before the deletion, because the removed value can be the last one. Each call is O(1) on average, and the space is O(N)."
Common Questions
- What is the time and space complexity?
insert,removeandgetRandomareO(1)on average. The word average is used because dictionary operations areO(1)on average. The space isO(N)forvaluesandindex.- Why not delete with
self.values.pop(i)? pop(i)shifts every later item one slot to the left, so it costsO(N). Copying the last value into slotiand popping the end costsO(1). It changes the order of the values, which a set does not need.- Why does
getRandomgive equal chances? random.choicepicks one slot with equal chance fromlen(self.values)slots. Each stored value owns exactly one slot, so each value has a 1-in-n chance. The invariant of no gaps and no repeated values is what makes this true.- How does the design change when duplicates are allowed?
Insert Delete GetRandom
O(1)- Duplicates allowed (LC 381) keeps one slot per copy, so a pick follows the copy counts.index[val]becomes a set of slots.removetakes any slot from the set, then moves the last value's slot number toi.insertreturnsTruewhen the set was empty before.- Why is the order of the two map writes important?
When the removed value is the last value,
last == val. Ifdel self.index[val]runs first, thenself.index[last] = iwrites the value back. The map then keeps a value that is no longer stored. Writingself.index[last] = ifirst, and deleting second, leaves the right state in both cases.
SWE Reasoning
Spot it
- A collection needs insert, remove and a uniform random pick, each in
O(1). - Order does not matter to the caller.
- A list gives
O(1)pick, and a map givesO(1)lookup, but neither does both. - Deleting from the middle of a list would cost
O(N). - The statement says "each element must have the same probability" or "proportional to the number of copies".
The invariant, step by step
insert(val)returnsFalseifvalis inself.index. Otherwise it writesself.index[val] = len(self.values)and appendsval. The new value takes the next free slot.remove(val)returnsFalseifvalis not inself.index. Otherwise it readsi = self.index[val]andlast = self.values[-1].- It writes
self.values[i] = last. Slotinow holdslast, and the last slot holds a second copy of it. - It writes
self.index[last] = i, and then runsdel self.index[val]. The map now matches the list, except for the extra last slot. self.values.pop()drops the extra last slot inO(1). Every stored value owns exactly one slot again, and there are no gaps.getRandomreturnsrandom.choice(self.values). With one slot per value, each value has a 1-in-n chance.
Traps
- Deleting before writing.
del self.index[val]beforeself.index[last] = iwritesvalback whenlast == val. A laterinsert(val)returnsFalse. - Popping before filling the hole. When
valis the only value,pop()empties the list, andself.values[i] = lastraisesIndexError. - Using
self.values.pop(i). It keeps the order but costsO(N), because later items shift. - Forgetting the map write for the moved value.
index[last]still names the old last slot. A laterremove(last)then reads a slot that does not exist or holds another value. - Picking from the map.
random.choice(list(self.index))builds a list on each call, which costsO(N). - Skipping the duplicate check in
insert. The same value then owns two slots, and its pick chance is doubled.
Practice
Builds on Hash Map, which you haven’t finished yet.
Array + Index Map (Swap-with-Last Delete)
Build a class, RandomizedSet, that stores a set of distinct integers and can hand back one of them at random. Every operation has to take O(1) time on average.
RandomizedSet()creates an empty set.bool insert(int val)addsvalif it is missing. It returnstruewhenvalwas added andfalsewhen it was already there.bool remove(int val)deletesvalif it is there. It returnstruewhenvalwas deleted andfalsewhen it was not in the set.int getRandom()returns one of the stored values, chosen so that every value in the set at that moment is equally likely.
insert(1) adds 1. remove(2) returns false: 2 is not stored. insert(2) adds 2, so the set is {1, 2} and the first getRandom() may return 1 or 2, each with probability 1/2 (LeetCode shows 2). remove(1) leaves {2}, insert(2) returns false because 2 is already stored, and the last getRandom() can only return 2.Insert Delete GetRandom
values list + index map ➔ move the last value into the gap ➔ O(1) delete and uniform pickO(1) on average.| 1 | import random |
| 2 | |
| 3 | |
| 4 | class RandomizedSet: |
| 5 | """Insert Delete GetRandom O(1): a gap-free list for the random pick, a map for each value's slot. |
| 6 | Time: O(1) average per call | Space: O(N) |
| 7 | """ |
| 8 | |
| 9 | def __init__(self) -> None: |
| 10 | self.values: list[int] = [] # Every value once, no gaps: random.choice is uniform |
| 11 | self.index: dict[int, int] = {} # value -> its slot in self.values |
| 12 | |
| 13 | def insert(self, val: int) -> bool: |
| 14 | # 1. Reject A Duplicate, Else Append At The Next Free Slot: |
| 15 | # The list is always gap-free, so its current length is exactly the slot the new value will occupy. |
| 16 | if val in self.index: |
| 17 | return False |
| 18 | self.index[val] = len(self.values) # The next free slot is the end |
| 19 | self.values.append(val) |
| 20 | return True |
| 21 | |
| 22 | def remove(self, val: int) -> bool: |
| 23 | # 1. Fill val's Slot With The Last Value, Then Drop The Last Slot: |
| 24 | # A set has no order, so moving the last value into the freed slot keeps the list gap-free |
| 25 | # without shifting anything; the moved value's own map entry must be fixed before val's entry is deleted. |
| 26 | if val not in self.index: |
| 27 | return False |
| 28 | i = self.index[val] |
| 29 | last = self.values[-1] |
| 30 | self.values[i] = last # Fill the hole with the last value |
| 31 | self.index[last] = i # Update the moved value's slot first... |
| 32 | del self.index[val] # ...then delete: right even when last == val |
| 33 | self.values.pop() # The last slot is now a copy: drop it in O(1) |
| 34 | return True |
| 35 | |
| 36 | def getRandom(self) -> int: |
| 37 | return random.choice(self.values) # One slot per value: each has a 1-in-n chance |
self.values holds every stored value exactly once with no gaps, and self.index[v] is always the slot of v in it.A pick where every value is equally likely needs the values packed in a list, random.choice(self.values), and a list can only drop its last slot without shifting the rest, so remove fills the hole with the last value instead of shifting: self.values[i] = last, then self.values.pop(). A set has no order, so moving one value changes nothing a caller can see, and self.index finds i with one lookup; the list stays gap-free, so every value keeps exactly one slot and the same 1-in-n chance. The trap is the order of the two map writes: self.index[last] = i must come before del self.index[val]. When val is itself the last value, last == val, and deleting first writes val straight back into the map at a slot that pop() removes, so a later insert(val) returns False.
See it run
Insert Delete GetRandom: swap with the last value, ops = insert(1), insert(2), insert(3), remove(2), remove(3), insert(3)
index {}values []
The row is the list values, one box per slot, numbered from 0. The readouts show the dictionary index.
newthe slot that insert just filledslot ithe slot of the value being removedlastthe last slot, which holds the value that movesi is lastthe value to remove is already in the last slot
The set is empty. values has no values, and index has no entries.
Gotcha 1 of 3
- The trap
Gotcha 1: Deleting
self.index[val]before writingself.index[last]Mistakedel self.index[val] self.index[last] = iFixself.index[last] = i del self.index[val]What happens: When
valis the last value,last == val. Deleting first removesval, thenself.index[last] = iwrites it back.pop()removes the slot, but the map still says thatvalis present. A laterinsert(val)then returns False.Example: insert(1), remove(1), then insert(1)Mistake: returns FalseFix: returns True
- The structure must keep an order, the order of use as in LRU Cache (LC 146), so no value may be moved into another's slot. See LRU Cache
Deep Dive
Why the last value can fill the hole
getRandom must pick every stored value with equal chance. random.choice(self.values) does that when self.values holds every value once and has no gaps. Each value then owns one slot, and each slot has a 1-in-n chance.
A list can drop only its last slot in O(1). self.values.pop(i) for another i shifts every later item, which costs O(N). So remove avoids the shift. It copies the last value into slot i, and then drops the last slot.
This is allowed because a set has no order. Moving one value to another slot changes nothing a caller can see.
Trace insert(1), insert(2), insert(3). Now values is [1, 2, 3] and index is {1: 0, 2: 1, 3: 2}. remove(2) reads i = 1 and last = 3. It sets values[1] = 3, sets index[3] = 1, deletes index[2], and pops. The result is values = [1, 3] and index = {1: 0, 3: 1}.
This is why the card's invariant says that self.values holds every stored value once with no gaps, and self.index[v] is the slot of v. After each call, both facts hold again.
The order of the two map writes
remove changes two map entries: self.index[last] = i and del self.index[val]. The first must come before the second.
Look at the case where val is the last value. In the trace, after remove(2), remove(3) has i = 1 and last = 3. Here last == val. The code sets values[1] = 3, which changes nothing. Then it sets index[3] = 1, which repeats the old entry. Then it deletes index[3]. The map no longer holds 3, and pop() drops the slot. The state is values = [1] and index = {1: 0}.
Now swap the two lines. del self.index[3] runs first, and self.index[3] = 1 writes the entry back. pop() removes the slot, but the map still says that 3 is stored. A later insert(3) returns False. With the correct order, insert(3) returns True and gives values = [1, 3].
The same order also keeps the card's other case safe. When val is not the last value, the two lines touch different keys, so their order does not matter.
Cost, and why the order of values may change
insert, remove and getRandom each do a fixed number of dictionary and list-end operations. Each is O(1) on average. The space is O(N) for values and index.
The values do not keep insertion order. After remove(2), the value 3 moved from slot 2 to slot 1. The problem asks only for a random pick, so this is fine. If you need the order, this card does not fit.
A check of the pick: in a run of 30,000 calls on values = [4, 5, 6], each value came up about 10,000 times. A skewed result would mean that a value owns two slots or that a slot is empty.
Duplicates: slots as a set
Insert Delete GetRandom O(1) - Duplicates allowed (LC 381) allows the same value many times, and a pick must follow the number of copies. The list then holds one slot per copy.
index[val] becomes the set of slots that hold val. remove takes any slot i from that set, copies last into slot i, moves the slot number of last from the end to i, and pops. Add i to the set of last first, and then discard the old last slot number. The order matters again when i is the last slot.
insert returns True only when the set for the value was empty before the call.
60-Second Pitch
"A uniform pick needs the values in a packed list, and O(1) delete needs the position of a value. So I keep values as a list with no gaps and index as a map from value to slot. insert appends and records the slot. remove reads the slot i of the value, copies the last value into slot i, fixes the map entry of that moved value, deletes the entry of the removed value, and pops the last slot. I write the moved value's entry before the deletion, because the removed value can be the last one. Each call is O(1) on average, and the space is O(N)."
Common Questions
- What is the time and space complexity?
insert,removeandgetRandomareO(1)on average. The word average is used because dictionary operations areO(1)on average. The space isO(N)forvaluesandindex.- Why not delete with
self.values.pop(i)? pop(i)shifts every later item one slot to the left, so it costsO(N). Copying the last value into slotiand popping the end costsO(1). It changes the order of the values, which a set does not need.- Why does
getRandomgive equal chances? random.choicepicks one slot with equal chance fromlen(self.values)slots. Each stored value owns exactly one slot, so each value has a 1-in-n chance. The invariant of no gaps and no repeated values is what makes this true.- How does the design change when duplicates are allowed?
Insert Delete GetRandom
O(1)- Duplicates allowed (LC 381) keeps one slot per copy, so a pick follows the copy counts.index[val]becomes a set of slots.removetakes any slot from the set, then moves the last value's slot number toi.insertreturnsTruewhen the set was empty before.- Why is the order of the two map writes important?
When the removed value is the last value,
last == val. Ifdel self.index[val]runs first, thenself.index[last] = iwrites the value back. The map then keeps a value that is no longer stored. Writingself.index[last] = ifirst, and deleting second, leaves the right state in both cases.
SWE Reasoning
Spot it
- A collection needs insert, remove and a uniform random pick, each in
O(1). - Order does not matter to the caller.
- A list gives
O(1)pick, and a map givesO(1)lookup, but neither does both. - Deleting from the middle of a list would cost
O(N). - The statement says "each element must have the same probability" or "proportional to the number of copies".
The invariant, step by step
insert(val)returnsFalseifvalis inself.index. Otherwise it writesself.index[val] = len(self.values)and appendsval. The new value takes the next free slot.remove(val)returnsFalseifvalis not inself.index. Otherwise it readsi = self.index[val]andlast = self.values[-1].- It writes
self.values[i] = last. Slotinow holdslast, and the last slot holds a second copy of it. - It writes
self.index[last] = i, and then runsdel self.index[val]. The map now matches the list, except for the extra last slot. self.values.pop()drops the extra last slot inO(1). Every stored value owns exactly one slot again, and there are no gaps.getRandomreturnsrandom.choice(self.values). With one slot per value, each value has a 1-in-n chance.
Traps
- Deleting before writing.
del self.index[val]beforeself.index[last] = iwritesvalback whenlast == val. A laterinsert(val)returnsFalse. - Popping before filling the hole. When
valis the only value,pop()empties the list, andself.values[i] = lastraisesIndexError. - Using
self.values.pop(i). It keeps the order but costsO(N), because later items shift. - Forgetting the map write for the moved value.
index[last]still names the old last slot. A laterremove(last)then reads a slot that does not exist or holds another value. - Picking from the map.
random.choice(list(self.index))builds a list on each call, which costsO(N). - Skipping the duplicate check in
insert. The same value then owns two slots, and its pick chance is doubled.