Coding Interview Patterns Module 29 – Design-a-data-structure problems
Insert, delete and getRandom in O(1)
A set with insert, delete and getRandom in O(1) on average: an original raffle-box problem, its constraints, its examples, the brute force and its signal.
What you will learn
- Combine an array with a hash map of positions for O(1) insert, delete and random picks
- Delete by moving the last item into the hole, in the order that survives deleting the last item
- Test random picks without randomness, then with a seeded chi-square check
- Explain the modulo bias of rand() % n and choose an unbiased draw in five languages
- Support repeated items with a set of positions for each value
Before you start
What you’ll learn · Before you start
What you will learn
- Combine an array with a hash map of positions for O(1) insert, delete and random picks
- Delete by moving the last item into the hole, in the order that survives deleting the last item
- Test random picks without randomness, then with a seeded chi-square check
- Explain the modulo bias of rand() % n and choose an unbiased draw in five languages
- Support repeated items with a set of positions for each value
Before you start
On this page
Insert, delete and getRandom in O(1) asks for a set that adds an item, deletes an item and returns an item chosen uniformly at random, each in O(1) time on average. A hash set can insert and delete in O(1) but has no positions to pick from. An array gives getRandom in O(1) through a random position, but it cannot delete from its middle without shifting. This lesson states an original version, a raffle box, with its brute force and the signal that leads to the design.
The problem: a raffle box
The problem
At a school fair, families buy numbered raffle tickets, some ask for a refund later, and every hour a ticket is
drawn at random for a small prize; a drawn ticket stays in the box. Write RaffleBox. insert(t) adds ticket t
and returns true, or false if t is already in the box. remove(t) takes t out and returns true, or false if it is
not there. get_random() returns a ticket of the box, each with the same chance, and is never called on an empty
box. So that tests can check the draws, size() returns the number of tickets, and as r runs from 0 to
size() − 1, pick(r) returns every ticket exactly once; get_random() is pick of a uniformly random r.
Tickets are numbers from 1 to 10⁹, a test makes up to 2 × 10⁵ calls, and each call must be O(1) on average.
RaffleBox(),insert(501),insert(502),insert(501),remove(503),insert(503),remove(501),size()gives[null, true, true, false, false, true, true, 2]. Thenpick(0)andpick(1)are 502 and 503 in some order, andget_random()returns each of them with a chance of one half.RaffleBox(),insert(7),remove(7),remove(7),insert(7),size()gives[null, true, true, false, true, 1]: a ticket can leave and come back.
The brute force, and why it fails at these sizes
Each obvious box has one slow call. A list gives pick(r) in O(1), but insert must search the list for a copy, and
remove must find the ticket and shift every ticket after the hole. A set makes insert and remove O(1) on average,
but a set has no positions, so pick(r) walks through r of its tickets.
"""Two obvious raffle boxes, each with one slow call, and the work each call does counted.
ListBox keeps the tickets in a list: a draw reads one position, but insert and remove search the list, and a removal
shifts every ticket after the hole. SetBox keeps them in a set: insert and remove are O(1) on average, but a set has
no positions, so pick(r) walks through r of its tickets."""
import json
class ListBox:
def __init__(self):
self.tickets, self.work = [], 0
def insert(self, t):
self.work += len(self.tickets) # the search for t
if t in self.tickets:
return False
self.tickets.append(t)
return True
def remove(self, t):
self.work += len(self.tickets) # the search, then the shift of everything after the hole
if t not in self.tickets:
return False
self.tickets.remove(t)
return True
def size(self):
return len(self.tickets)
def pick(self, r):
self.work += 1
return self.tickets[r]
class SetBox:
def __init__(self):
self.tickets, self.work = set(), 0
def insert(self, t):
self.work += 1
if t in self.tickets:
return False
self.tickets.add(t)
return True
def remove(self, t):
self.work += 1
if t not in self.tickets:
return False
self.tickets.remove(t)
return True
def size(self):
return len(self.tickets)
def pick(self, r):
for i, t in enumerate(self.tickets): # no positions: walk to the r-th ticket of the iteration
self.work += 1
if i == r:
return t
def replay(box, ops, args):
return [None] + [getattr(box, op)(*a) for op, a in zip(ops[1:], args[1:])]
def fair_day(box_class, n):
"""n tickets sold, then 2n calls: a refund and a new sale (the refunds hit the oldest tickets), and a draw."""
box = box_class()
for t in range(1, n + 1):
box.insert(t)
start = box.work
for i in range(n // 2):
box.remove(i + 1)
box.insert(n + 1 + i)
box.pick(box.size() - 1 - i % box.size())
box.pick(i % box.size())
return box.work - start
if __name__ == "__main__":
ops = ["RaffleBox", "insert", "insert", "insert", "remove", "insert", "remove", "size"]
args = [[], [501], [502], [501], [503], [503], [501], []]
for box_class in (ListBox, SetBox):
box = box_class()
answers = replay(box, ops, args)
print(f"{box_class.__name__}: {json.dumps(answers)}, tickets {sorted(box.pick(r) for r in range(box.size()))}")
print()
print(f"{'tickets':>8} {'calls':>7} {'ListBox work':>13} {'per call':>9} {'SetBox work':>12} {'per call':>9}")
for n in (500, 1_000, 2_000, 4_000):
lw, sw = fair_day(ListBox, n), fair_day(SetBox, n)
print(f"{n:>8,} {2 * n:>7,} {lw:>13,} {lw / (2 * n):>9,.0f} {sw:>12,} {sw / (2 * n):>9,.0f}") Output
ListBox: [null, true, true, false, false, true, true, 2], tickets [502, 503]
SetBox: [null, true, true, false, false, true, true, 2], tickets [502, 503]
tickets calls ListBox work per call SetBox work per call
500 1,000 250,250 250 125,750 126
1,000 2,000 1,000,500 500 501,500 251
2,000 4,000 4,001,000 1,000 2,003,000 501
4,000 8,000 16,002,000 2,000 8,006,000 1,001
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both answer correctly, and on this test both do work in proportion to the number of tickets on every call: about 10¹⁰ steps at 10⁵ tickets and 2 × 10⁵ calls. With the planning figure of the constraints lesson, roughly 10⁸ steps a second for compiled code, that is well over a minute for one test.
What to notice. A uniform draw needs the tickets at positions 0 to n − 1 with no gaps, which an array gives. The array’s trouble is a hole in the middle, and nothing in the problem orders the tickets, so any ticket may fill a hole. Finding a ticket’s position is a look-up by value, which a hash map answers in O(1).
An array plus a map of positions Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- The invariant, and the ticket that fills its own hole
- A dry run
- The code in five languages
- Uniform draws, tested twice
- Five bugs reproduced, each shrunk to a few calls
- Repeated tickets: a set of positions per number
- Variants
- Exercise: the raffle box
- A raffle box with insert, remove and random draws in O(1)
- A practice set with answers
- Check yourself
- Interview questions
- Why can't a hash set on its own return a uniformly random element in O(1)?
- A C++ raffle draws a position with rand() % n. On a platform where RAND_MAX is 32,767, a statistics check with n = 20,000 tickets shows that some tickets win twice as often as others. Explain exactly which ones and why, and fix the draw.
- Tickets can now repeat: a family may hold three copies of number 7, and every copy must be as likely to win as any other. Change insert, remove and getRandom, keep each O(1) on average, and walk through removing a copy that is the last element of the array.
- A lucky-draw app picks a winning seat from 0 to n − 1, where n can be 10⁹, but k seats, up to 10⁵ of them, belong to staff and may never win. Each pick must take O(1) after setup, memory must be O(k), and every other seat must be equally likely. Design it.
- Key takeaways
1 diagram · 6 runnable examples (C++, Go, Java, JavaScript, Python, raffle_session.py, session.py) · 1 exercise with sample tests · 3 interview answers · about 25 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- random: randrange and notes on reproducibility (The Python Standard Library) (Python Software Foundation)
- Built-in Types: set.pop (Python Software Foundation)
- Math.random() (JavaScript reference) (MDN Web Docs)
- Random.nextInt(int) (Java SE 25 API) (Oracle)
- ThreadLocalRandom (Java SE 25 API) (Oracle)
- std::uniform_int_distribution (cppreference.com) (cppreference.com)
- std::rand and RAND_MAX (cppreference.com) (cppreference.com)
- Order of evaluation (cppreference.com) (cppreference.com)
- Package math/rand/v2 (The Go Authors)
- Critical Values of the Chi-Square Distribution (NIST/SEMATECH e-Handbook of Statistical Methods) (NIST)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress