Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Process insert / delete / 'does any value have frequency exactly x' queries. Keep TWO hash maps in lockstep: valueCount (value → how many times it appears) and freqOfFreq (a frequency → how many distinct values currently have that frequency). Every insert/delete adjusts both maps; the existence query becomes a single O(1) lookup freqOfFreq[x] > 0 instead of scanning all counts.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you realise query type 3 asks about frequencies (not values) and add a second 'frequency of frequencies' map so it's O(1), instead of scanning all counts.
Where candidates slip
Using only valueCount and scanning its values on every type-3 query (O(n) per query → O(n²) overall), or letting the two maps drift out of sync on delete.
How to stand out
Name the 'frequency of frequencies' map, keep both maps in sync with a decrement-old/increment-new step, and guard delete so counts never go negative and 0 buckets are never stored.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewQuery (type, x)type?1 insert2 deleteExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Single Map, Scan on Query 3
1Hint 1Explore hintHide hint
Inserts and deletes are trivial with one map: valueCount[x] up or down. The cost is hidden in the type-3 query.
2Hint 2Explore hintHide hint
For a type-3 query, you need to know whether ANY value has count exactly x. With only valueCount, how do you find out?
3Hint 3Explore hintHide hint
You'd scan every entry of valueCount looking for a count equal to x — O(d) per query. With up to 10⁵ queries that's O(q·d), too slow when many are type 3.
A · Core idea
Keep a single map valueCount from value to its current occurrence count. Inserts increment it, deletes decrement it (guarding against going below 0).
Why the inserts/deletes are fine. Each is a single hash-map update — O(1) average. Nothing about maintaining individual counts is slow.
Where it falls apart. A type-3 query asks 'does any value have count exactly x?'. With only valueCount, the only way to answer is to walk every entry and check whether its count equals x. That's O(d) per type-3 query, where d is the number of distinct values. If a large fraction of the q queries are type 3, the total balloons to O(q·d) — for q, d near 10⁵ that's ~10¹⁰ operations and times out.
The redundancy to remove. Across consecutive type-3 queries the count distribution barely changes — yet we re-scan it from scratch every time. Maintaining a running tally of 'how many values have each frequency' eliminates the rescans, making each type-3 query O(1).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
queries = [[1,1],[1,1],[3,2],[3,5]]Hash-map state
state
Continue scanning
value 1 has count 2
[1, 0]C · Code with teaching comments
Reference implementation previewpublic List<Integer> freqQuery(int[][] queries) { Map<Integer, Integer> valueCount = new HashMap<>(); List<Integer> out = new ArrayList<>(); for (int[] q : queries) { int type = q[0], x = q[1];Explore the full codeHide the full code
D · Why it works and complexityO(q × d) where d = distinct values timeO(d) spaceOpen the invariant and proofExplore the proofHide the proof
The slow part is recomputing a global fact (which frequencies are present) on every query when it changes by at most one per insert/delete. Maintaining that fact incrementally — a second map — is the leap to O(1) queries.
ComplexityO(q × d) where d = distinct values time, O(d) space — Insert/delete are O(1) with one hash map. Each type-3 query scans all d distinct values to test for count == x → O(d). With q queries, the worst case (many type-3) is O(q·d), up to ~10¹⁰.
E · Edge cases and pitfalls3Using containsValue / x in .values() on every type-3 query — O(d) each, O(q·d) total.Explore edge casesHide edge cases
Pitfalls
- Using containsValue / x in .values() on every type-3 query — O(d) each, O(q·d) total.
- Leaving a value at count 0 in the map — containsValue(0) could falsely match.
- Reading type-3 x as a value to look up rather than a frequency to test.
Approach 2 of 2 · Optimal
Two Hash Maps — Frequency of Frequencies (Optimal)
1Hint 1Explore hintHide hint
What if you tracked, for each frequency value, how many distinct numbers currently have that frequency? Then 'any value with frequency x?' is a single lookup.
2Hint 2Explore hintHide hint
Keep valueCount (value → count) AND freqOfFreq (count → number of values with that count). On insert/delete, move a value between frequency buckets: decrement its old bucket, increment its new one.
3Hint 3Explore hintHide hint
Type 3 x: return 1 if freqOfFreq[x] > 0 else 0. Never store a 0-count bucket, and never let valueCount go negative on a delete.
A · Core idea
Run two maps in lockstep:
valueCount: value → how many times it currently appears.freqOfFreq: a frequency → how many distinct values currently have exactly that frequency.
freqOfFreq is the 'frequency of frequencies' map, and it's what makes type-3 queries O(1).
Insert x. Let f = valueCount[x] (0 if absent). The value is leaving frequency bucket f and entering f+1, so: decrement freqOfFreq[f] (only if f > 0), set valueCount[x] = f + 1, and increment freqOfFreq[f+1]. Two bucket edits keep the maps consistent.
Delete x. Let f = valueCount[x]. If f == 0, the value isn't present — do nothing (deleting an absent value is a no-op, and decrementing would corrupt the maps). Otherwise the value moves from bucket f to f-1: decrement freqOfFreq[f], set valueCount[x] = f - 1, and increment freqOfFreq[f-1] only if `f-1 > 0` (a count of 0 means the value is gone — don't track a 0 bucket).
Query 3 x. Return 1 if freqOfFreq[x] > 0, else 0. One lookup — no scanning.
Why the two maps must stay synchronized. freqOfFreq is only correct if it's updated on every change to valueCount. The discipline is always 'decrement the old frequency bucket, increment the new one' as a value's count changes by exactly one. Skip either half and the maps drift, producing wrong answers later — the subtle bug graders look for.
Why never store a 0 bucket. freqOfFreq[x] > 0 is the existence test. If you ever set freqOfFreq[0] to a positive number (e.g. by incrementing f-1 when f-1 == 0), a future query 3 0 would wrongly report 1, and you'd be tracking a meaningless 'values with frequency 0'. Guard the +1 with f-1 > 0.
Why this is the version to ship. Every operation is O(1) average, so q queries run in O(q) total — comfortably within limits for 10⁵ queries. The cost is one extra map and the discipline of keeping both in sync.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
queries = [[1,5],[1,6],[3,2],[1,10],[1,10],[1,6],[2,5],[3,2]]Hash-map state
5→1, 6→1
Continue scanning
two values at frequency 1
[0, 1]C · Code with teaching comments
D · Why it works and complexityO(1) per query, O(q) total timeO(d) spaceOpen the invariant and proofExplore the proofHide the proof
The trick is maintaining a derived statistic (the distribution of counts) incrementally instead of recomputing it. Each insert/delete shifts exactly one value between two adjacent frequency buckets, so the update is O(1). This 'second map of frequencies' idea reappears in LFU cache eviction and the All O`one data-structure problem.
ComplexityO(1) per query, O(q) total time, O(d) space — Every query does a constant number of hash-map operations: insert/delete touch valueCount once and freqOfFreq twice; type-3 is one lookup. So O(1) average per query, O(q) total. Space is O(d) for distinct values plus the frequency buckets.
E · Edge cases and pitfalls6[[2,1],[3,1]] — Delete on empty is a no-op (f==0 → skip). freqOfFreq[1] = 0 → query outputs 0. No negative counts.Explore edge casesHide edge cases
Edge cases handled
[[2,1],[3,1]]Delete on empty is a no-op (f==0 → skip). freqOfFreq[1] = 0 → query outputs 0. No negative counts.[[1,3],[2,3],[3,1]]Insert 3 (freq 1), delete 3 (freq 0). freqOfFreq[1] decremented back to 0; f-1==0 so no 0-bucket stored → query 1 outputs 0.[[1,9],[1,9],[1,9],[3,3]]9 reaches frequency 3; freqOfFreq[3] = 1 → query outputs 1. Buckets 1 and 2 were vacated along the way.
Pitfalls
- Updating valueCount but forgetting to update freqOfFreq — the maps drift and queries go wrong.
- Incrementing freqOfFreq[f-1] when f-1 == 0 — pollutes the map with a meaningless 0 bucket.
- Decrementing valueCount on a delete when the value isn't present — pushes the count negative.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic List<Integer> freqQuery(int[][] queries) { Map<Integer, Integer> valueCount = new HashMap<>(); Map<Integer, Integer> freqOfFreq = new HashMap<>(); List<Integer> out = new ArrayList<>(); for (int[] q : queries) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Scanning values on every type-3 query
containsValue / 'x in .values()' is O(d). With many type-3 queries on 10⁵ inputs this becomes O(q·d) and times out. A second map makes it O(1).
Explore the code comparisonHide the code comparison
Scanning values on every type-3 query
containsValue / 'x in .values()' is O(d). With many type-3 queries on 10⁵ inputs this becomes O(q·d) and times out. A second map makes it O(1).
Don't write this
out.append(1 if x in value_count.values() else 0) # O(d) scanWrite this instead
out.append(1 if freq_of_freq.get(x, 0) > 0 else 0) # O(1) lookup2Maps drifting out of sync
If you change valueCount without moving the value between freqOfFreq buckets, freqOfFreq no longer reflects reality and later type-3 answers are wrong.
Explore the code comparisonHide the code comparison
Maps drifting out of sync
If you change valueCount without moving the value between freqOfFreq buckets, freqOfFreq no longer reflects reality and later type-3 answers are wrong.
Don't write this
valueCount.put(x, f + 1);
// forgot to update freqOfFreqWrite this instead
if (f > 0) freqOfFreq.merge(f, -1, Integer::sum);
valueCount.put(x, f + 1);
freqOfFreq.merge(f + 1, 1, Integer::sum);3Storing a frequency-0 bucket
Incrementing freqOfFreq[f-1] when f-1 is 0 records 'values with frequency 0', which is meaningless and would make a query '3 0' wrongly return 1.
Explore the code comparisonHide the code comparison
Storing a frequency-0 bucket
Incrementing freqOfFreq[f-1] when f-1 is 0 records 'values with frequency 0', which is meaningless and would make a query '3 0' wrongly return 1.
Don't write this
freq_of_freq[f - 1] = freq_of_freq.get(f - 1, 0) + 1 # even when f-1 == 0Write this instead
if f - 1 > 0:
freq_of_freq[f - 1] = freq_of_freq.get(f - 1, 0) + 1Revise
Carry the pattern into the interview
Pattern to remember
Frequency-of-frequencies — two synchronized hash maps
Code pattern to memorize
insert x: f=valueCount[x]; freqOfFreq[f]--; valueCount[x]=f+1; freqOfFreq[f+1]++
delete x: f=valueCount[x]; if f>0: freqOfFreq[f]--; valueCount[x]=f-1; if f-1>0: freqOfFreq[f-1]++
query x: return 1 if freqOfFreq[x] > 0 else 0Rules to remember
- 1Maintain a second map
freqOfFreq(count → number of values having that count) so query type 3 is O(1), not an O(n) scan. - 2On every insert/delete, decrement the old frequency bucket and increment the new one — keep both maps perfectly in sync.
- 3On delete, do nothing if the value's count is already 0 — never push a count negative.
- 4Never store a 0 frequency in
freqOfFreq. A count of 0 means 'no value has this frequency', so don't add afreqOfFreq[0]bucket. - 5Query 3 asks about a frequency, not a value: 'is there ANY value occurring exactly x times?'
Use this pattern when
- You need O(1) answers to 'does any element have count exactly x?'
- A stream of insert/delete with interleaved frequency questions
- Tracking a distribution of counts that changes incrementally
- Any 'frequency of frequencies' / mode-adjacent bookkeeping
Choose another pattern when
- Only ever ask for one specific value's count → a single map suffices, no second map
- Need the actual values at a frequency, not just existence → store sets, more memory
- Static data, queries only at the end → sort/count once, no incremental maps
- Need min/max frequency dynamically → a different structure (e.g. buckets + pointers, as in LFU)
Interview takeaway
When a question targets the *distribution of counts* rather than a single count, maintain a 'frequency of frequencies' map alongside the value map. Keeping the two synchronized turns an O(n) scan into an O(1) lookup — the same idea behind LFU caches and All O`one data structures.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical frequency-of-frequencies bookkeeping. When a query targets the distribution of counts (not a single count), maintain a second map (count → #values) in sync with the value map for O(1) answers. Same idea underpins LFU Cache and All O`one Data Structure.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- LFU CacheComing soonFrequency-of-frequencies taken further: buckets per frequency to evict the least-frequently-used in O(1).
- All O`one Data StructureComing soonMaintain min/max key frequency in O(1) with a doubly linked list of frequency buckets.
- Top K Frequent ElementsComing soonBucket values by frequency, then read buckets from the top — bucket sort on counts.
- Sort Characters By FrequencyComing soonCount, then order by count — the static cousin of this streaming problem.