Coding Interview Patterns Module 29 – Design-a-data-structure problems
Frequency stack and a constant-time counter
Design a maximum frequency stack and a counter whose every call is O(1): an original DJ-booth problem, the scan that fails at 10⁵ calls, and what to notice.
What you will learn
- Implement a stack that pops the most frequent value in O(1), newest first on ties, with a stack per count level
- Prove the level design correct with an invariant, including the step after a level runs empty
- Compare the level stacks with a heap of (count, arrival) entries, with measured comparisons
- Build counters whose increment, decrement, maximum and minimum are all O(1), from linked count buckets
- Find tie-breaking and bookkeeping bugs with random tests against a scan
Before you start
What you’ll learn · Before you start
What you will learn
- Implement a stack that pops the most frequent value in O(1), newest first on ties, with a stack per count level
- Prove the level design correct with an invariant, including the step after a level runs empty
- Compare the level stacks with a heap of (count, arrival) entries, with measured comparisons
- Build counters whose increment, decrement, maximum and minimum are all O(1), from linked count buckets
- Find tie-breaking and bookkeeping bugs with random tests against a scan
Before you start
On this page
A maximum frequency stack is a stack whose pop() removes the value that occurs most often on it, and among
values that occur equally often, the one pushed most recently. A constant-time counter is its sibling: it counts
keys up and down and can name a key with the most or the fewest counts, each call in O(1). Both appear wherever a
system must keep serving “the most requested” while requests keep arriving: song requests, trending tags, busy
pages. Counting on every call is correct and quadratic overall. The rest of this opening sets out an original
request-box problem, counts what its brute force costs, and names the observation behind both O(1) designs.
The problem: a DJ’s request box
The problem
At a wedding, guests drop request slips for songs into a box. Write a class RequestBox() with two methods.
request(song) adds a slip for song. play() picks the song with the most slips in the box, takes out one of
its slips, the newest, and returns the song; when several songs tie for the most slips, it picks the one whose
newest slip arrived last. Limits: up to 2 × 10⁵ calls, song names of up to 30 characters, and play() is only
called when the box holds a slip.
The box is a frequency stack with songs as its values. Six requests, for waltz, salsa, waltz, salsa, tango and waltz, followed by six plays, give waltz, salsa, waltz, tango, salsa and waltz. The second play settles a tie: waltz and salsa both have two slips left, and salsa’s second slip is newer. In a second test, requests for jive and polka and one play give polka.
The example: six slips, and the order in which play() takes them out
Text description of the diagram
The box holds six slips, oldest first, in two rows of three. The first row: waltz, its 1st slip; salsa, its 1st slip; waltz, its 2nd slip. The second row: salsa, its 2nd slip; tango, its 1st slip; waltz, its 3rd slip.
An arrow labelled "most slips first; on a tie, the newest slip" leads to the order in which six calls of play() empty the box: waltz, salsa, waltz, tango, salsa, waltz. waltz goes first with three slips. Then waltz and salsa have two each, and salsa's second slip is newer, so salsa goes. Then waltz has two and the others one, so waltz goes again. Then all three songs have one slip, and tango's is the newest, so tango goes, followed by salsa and finally waltz.
The brute force: count every slip on every play
Keep the slips in a list. Each play() counts the slips of every song and remembers each song’s newest slip, then
takes out the winner’s newest slip. A request costs O(1), but a play reads every slip still in the box. The slowest
test makes n requests and then plays them all:
"""The first design that comes to mind: keep every slip in a list, and on each play() count the slips of every song
to find the winner. The answers are right; this program counts the slips each play() reads on the slowest test.
"""
class ScanBox:
def __init__(self):
self.slips = [] # songs in the order their slips arrived
self.reads = 0
def request(self, song):
self.slips.append(song)
def play(self):
count, newest = {}, {}
for i, song in enumerate(self.slips):
self.reads += 1
count[song] = count.get(song, 0) + 1
newest[song] = i
best = max(count, key=lambda s: (count[s], newest[s])) # most slips, then the newest slip
del self.slips[newest[best]] # take that song's newest slip out of the box
return best
box = ScanBox()
for song in ["waltz", "salsa", "waltz", "salsa", "tango", "waltz"]:
box.request(song)
print("example:", [box.play() for _ in range(6)])
# The slowest test: n requests spread over 100 songs, then n plays, each of which reads every slip left.
for n in (500, 1_000, 2_000):
box = ScanBox()
for i in range(n):
box.request(f"song-{i % 100}")
for _ in range(n):
box.play()
print(f"n = {n:,}: {n:,} plays read {box.reads:,} slips")
n = 100_000
print(f"n = {n:,}: {n:,} plays would read {n * (n + 1) // 2:,} slips") Output
example: ['waltz', 'salsa', 'waltz', 'tango', 'salsa', 'waltz'] n = 500: 500 plays read 125,250 slips n = 1,000: 1,000 plays read 500,500 slips n = 2,000: 2,000 plays read 2,001,000 slips n = 100,000: 100,000 plays would read 5,000,050,000 slips
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scan_box.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
n plays read about n²/2 slips: 5 × 10⁹ at n = 10⁵, some eight minutes of CPython.
What to notice
Number each song’s slips: its first slip, its second, and so on. A song with c slips holds exactly one slip of each number from 1 to c. The song to play always has the highest number in use, and among the songs that have it, the newest slip with that number wins. So group the slips by number, keep each group in arrival order, and the answer is the newest slip of the highest group. Three designs build on this:
- A stack per count level keeps each group as a stack and remembers the highest level in use.
- A heap of (count, arrival) entries pushes one entry per slip and pops the largest.
- A constant-time counter links its count buckets in a list, so increments, decrements and both extremes are O(1).
Stacks per count level, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the top drops by exactly one: the invariant and a dry run
- Random tests against the scan
- A heap version, and what it pays
- The code in five languages
- A constant-time counter: up, down, the most and the fewest
- Edge cases and common bugs
- Exercise with sample tests
- A stack that pops the most frequent value first
- A practice set with sample answers
- Check yourself
- Interview questions
- Why does a maximum frequency stack keep one stack per count level instead of one stack per value?
- In a frequency stack with one stack per count level, a pop sometimes empties the highest level. How does the design find the new highest level, why is that always right, and what breaks if pops may remove any value?
- Your team's frequency stack pushes (count, arrival time, value) into a heap and pops the largest. Compare it with stacks per count level on cost, memory and the bugs each invites, and say when you would keep the heap.
- Design live page-view counters with inc(page), dec(page), max_page() and min_page(), each in O(1), for a dashboard of a million pages. Why is a hash map with a heap, or a single max pointer, not enough?
- Key takeaways
7 runnable examples (C++, Go, Java, JavaScript, Python, check.py, request_box.py) · 1 exercise with sample tests · 3 interview answers · about 24 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
- heapq: Heap queue algorithm (The Python Standard Library) (Python Software Foundation)
- Mapping Types: dict (The Python Standard Library) (Python Software Foundation)
- The Python Language Reference: object.__hash__ (Python Software Foundation)
- Java SE 25 API: ArrayDeque (Oracle)
- Java SE 25 API: Map.merge (Oracle)
- std::unordered_map::operator[] (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: Index expressions (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress