2671. Frequency Tracker
Problem Description
You need to design a data structure that tracks numbers and can answer queries about how frequently numbers appear.
The FrequencyTracker
class should support the following operations:
-
FrequencyTracker()
: Creates a new frequency tracker that starts with no numbers stored. -
add(int number)
: Adds one occurrence ofnumber
to the data structure. If the number already exists, its count increases by one. -
deleteOne(int number)
: Removes one occurrence ofnumber
from the data structure. If the number doesn't exist or has zero occurrences, nothing happens. -
hasFrequency(int frequency)
: Checks if there's at least one number in the data structure that appears exactlyfrequency
times. Returnstrue
if such a number exists,false
otherwise.
The key challenge is to efficiently track both the count of each number and how many numbers have each specific frequency. The solution uses two hash tables:
cnt
: Maps each number to its occurrence countfreq
: Maps each frequency value to how many numbers have that frequency
When adding a number, the solution updates both hash tables - it decreases the count of numbers with the old frequency, increases the number's count, then increases the count of numbers with the new frequency.
Similarly, when deleting, it first checks if the number exists (count > 0), then updates both hash tables to reflect the frequency change.
The hasFrequency
operation simply checks if any numbers have the requested frequency by looking up freq[frequency]
.
Intuition
The straightforward approach would be to use a single hash table to count occurrences of each number. However, this makes the hasFrequency
operation inefficient - we'd need to iterate through all numbers to check if any has the target frequency, resulting in O(n) time complexity.
The key insight is that we need to answer frequency queries quickly. Instead of searching for a frequency when asked, we can maintain this information as we modify the data structure. This leads us to think: what if we track the frequencies themselves?
Consider what happens when we add a number:
- If a number appears 2 times and we add it again, it now appears 3 times
- This means one less number has frequency 2, and one more number has frequency 3
- The frequency distribution changes with each operation
This observation suggests we need a second hash table that answers: "How many numbers have frequency X?" By maintaining this information during add/delete operations, we can answer hasFrequency
queries in O(1) time.
The tricky part is keeping both hash tables synchronized. When we modify a number's count from old_count
to new_count
:
- We decrease
freq[old_count]
by 1 (one less number has the old frequency) - We increase
freq[new_count]
by 1 (one more number has the new frequency)
This dual hash table approach trades a bit of extra space and update complexity for constant-time frequency queries, making all operations O(1) on average.
Solution Approach
The solution uses two hash tables to efficiently track both the count of each number and the frequency distribution:
cnt
: A dictionary that maps each number to its occurrence countfreq
: A dictionary that maps each frequency value to how many numbers have that frequency
Let's walk through each operation:
Initialization
def __init__(self):
self.cnt = defaultdict(int)
self.freq = defaultdict(int)
Both hash tables are initialized as defaultdict(int)
which returns 0 for missing keys, simplifying our code by avoiding key existence checks.
Add Operation
def add(self, number: int) -> None:
self.freq[self.cnt[number]] -= 1
self.cnt[number] += 1
self.freq[self.cnt[number]] += 1
When adding a number:
- First, decrease the count of numbers having the current frequency:
self.freq[self.cnt[number]] -= 1
- Increment the number's count:
self.cnt[number] += 1
- Increase the count of numbers having the new frequency:
self.freq[self.cnt[number]] += 1
For example, if number 5 appears twice (cnt[5] = 2
), after adding it:
- Decrease
freq[2]
by 1 (one less number appears exactly twice) - Update
cnt[5]
to 3 - Increase
freq[3]
by 1 (one more number appears exactly three times)
Delete Operation
def deleteOne(self, number: int) -> None:
if self.cnt[number]:
self.freq[self.cnt[number]] -= 1
self.cnt[number] -= 1
self.freq[self.cnt[number]] += 1
The deletion follows similar logic but first checks if the number exists (self.cnt[number] > 0
):
- Decrease the count of numbers with the current frequency
- Decrement the number's count
- Increase the count of numbers with the new frequency
Note that when a number's count becomes 0, we still track it in freq[0]
, which doesn't affect our hasFrequency
queries since frequency queries are for positive values.
HasFrequency Operation
def hasFrequency(self, frequency: int) -> bool:
return self.freq[frequency] > 0
This simply checks if any numbers have the specified frequency by looking up freq[frequency]
. If the value is greater than 0, at least one number has that frequency.
The time complexity for all operations is O(1) on average due to hash table operations. The space complexity is O(n) where n is the number of unique numbers stored.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a sequence of operations to see how both hash tables work together:
Initial state:
cnt = {}
(tracks number → count)freq = {}
(tracks frequency → how many numbers have that frequency)
Operation 1: add(3)
- Before:
cnt[3] = 0
, sofreq[0] -= 1
→freq[0] = -1
- Update:
cnt[3] = 1
- After:
freq[1] += 1
→freq[1] = 1
- State:
cnt = {3: 1}
,freq = {0: -1, 1: 1}
Operation 2: add(3)
- Before:
cnt[3] = 1
, sofreq[1] -= 1
→freq[1] = 0
- Update:
cnt[3] = 2
- After:
freq[2] += 1
→freq[2] = 1
- State:
cnt = {3: 2}
,freq = {0: -1, 1: 0, 2: 1}
Operation 3: add(5)
- Before:
cnt[5] = 0
, sofreq[0] -= 1
→freq[0] = -2
- Update:
cnt[5] = 1
- After:
freq[1] += 1
→freq[1] = 1
- State:
cnt = {3: 2, 5: 1}
,freq = {0: -2, 1: 1, 2: 1}
Operation 4: hasFrequency(2)
- Check:
freq[2] > 0
→1 > 0
→ returnstrue
- (Number 3 appears exactly 2 times)
Operation 5: hasFrequency(1)
- Check:
freq[1] > 0
→1 > 0
→ returnstrue
- (Number 5 appears exactly 1 time)
Operation 6: deleteOne(3)
- Check:
cnt[3] = 2 > 0
, proceed with deletion - Before:
freq[2] -= 1
→freq[2] = 0
- Update:
cnt[3] = 1
- After:
freq[1] += 1
→freq[1] = 2
- State:
cnt = {3: 1, 5: 1}
,freq = {0: -2, 1: 2, 2: 0}
Operation 7: hasFrequency(2)
- Check:
freq[2] > 0
→0 > 0
→ returnsfalse
- (No number appears exactly 2 times anymore)
Operation 8: hasFrequency(1)
- Check:
freq[1] > 0
→2 > 0
→ returnstrue
- (Both numbers 3 and 5 appear exactly 1 time each)
Notice how the freq
table maintains accurate counts of how many numbers have each frequency, enabling O(1) frequency queries. The negative values in freq[0]
don't affect our queries since we only check positive frequencies.
Solution Implementation
1from collections import defaultdict
2
3class FrequencyTracker:
4 def __init__(self):
5 # Dictionary to store the count of each number
6 self.number_count = defaultdict(int)
7 # Dictionary to store how many numbers have a specific frequency
8 self.frequency_count = defaultdict(int)
9
10 def add(self, number: int) -> None:
11 """
12 Add a number to the tracker and update frequency counts.
13
14 Args:
15 number: The number to add to the tracker
16 """
17 # Decrease the count of numbers with the old frequency
18 old_frequency = self.number_count[number]
19 self.frequency_count[old_frequency] -= 1
20
21 # Increment the count of this number
22 self.number_count[number] += 1
23
24 # Increase the count of numbers with the new frequency
25 new_frequency = self.number_count[number]
26 self.frequency_count[new_frequency] += 1
27
28 def deleteOne(self, number: int) -> None:
29 """
30 Delete one occurrence of a number from the tracker if it exists.
31
32 Args:
33 number: The number to delete from the tracker
34 """
35 # Only delete if the number exists in the tracker
36 if self.number_count[number] > 0:
37 # Decrease the count of numbers with the old frequency
38 old_frequency = self.number_count[number]
39 self.frequency_count[old_frequency] -= 1
40
41 # Decrement the count of this number
42 self.number_count[number] -= 1
43
44 # Increase the count of numbers with the new frequency
45 new_frequency = self.number_count[number]
46 self.frequency_count[new_frequency] += 1
47
48 def hasFrequency(self, frequency: int) -> bool:
49 """
50 Check if any number in the tracker has the given frequency.
51
52 Args:
53 frequency: The frequency to check for
54
55 Returns:
56 True if at least one number has the given frequency, False otherwise
57 """
58 return self.frequency_count[frequency] > 0
59
60
61# Your FrequencyTracker object will be instantiated and called as such:
62# obj = FrequencyTracker()
63# obj.add(number)
64# obj.deleteOne(number)
65# param_3 = obj.hasFrequency(frequency)
66
1class FrequencyTracker {
2 // Map to store the frequency count of each number
3 private Map<Integer, Integer> numberToFrequency = new HashMap<>();
4 // Map to store how many numbers have a specific frequency
5 private Map<Integer, Integer> frequencyToCount = new HashMap<>();
6
7 public FrequencyTracker() {
8 }
9
10 public void add(int number) {
11 // Get the current frequency of the number (0 if not present)
12 int currentFrequency = numberToFrequency.getOrDefault(number, 0);
13
14 // Decrease the count of numbers having the current frequency
15 if (currentFrequency > 0) {
16 frequencyToCount.merge(currentFrequency, -1, Integer::sum);
17 }
18
19 // Increment the frequency of the number
20 int newFrequency = currentFrequency + 1;
21 numberToFrequency.put(number, newFrequency);
22
23 // Increase the count of numbers having the new frequency
24 frequencyToCount.merge(newFrequency, 1, Integer::sum);
25 }
26
27 public void deleteOne(int number) {
28 // Get the current frequency of the number
29 int currentFrequency = numberToFrequency.getOrDefault(number, 0);
30
31 // Only delete if the number exists (frequency > 0)
32 if (currentFrequency > 0) {
33 // Decrease the count of numbers having the current frequency
34 frequencyToCount.merge(currentFrequency, -1, Integer::sum);
35
36 // Decrement the frequency of the number
37 int newFrequency = currentFrequency - 1;
38 numberToFrequency.put(number, newFrequency);
39
40 // If new frequency is greater than 0, increase the count for that frequency
41 if (newFrequency > 0) {
42 frequencyToCount.merge(newFrequency, 1, Integer::sum);
43 }
44 }
45 }
46
47 public boolean hasFrequency(int frequency) {
48 // Check if any number has the specified frequency
49 return frequencyToCount.getOrDefault(frequency, 0) > 0;
50 }
51}
52
53/**
54 * Your FrequencyTracker object will be instantiated and called as such:
55 * FrequencyTracker obj = new FrequencyTracker();
56 * obj.add(number);
57 * obj.deleteOne(number);
58 * boolean param_3 = obj.hasFrequency(frequency);
59 */
60
1class FrequencyTracker {
2public:
3 FrequencyTracker() {
4 // Constructor - maps are automatically initialized as empty
5 }
6
7 void add(int number) {
8 // Before incrementing the count, decrease the frequency count for the current frequency
9 frequencyCount[numberCount[number]]--;
10
11 // Increment the count for this number
12 numberCount[number]++;
13
14 // After incrementing the count, increase the frequency count for the new frequency
15 frequencyCount[numberCount[number]]++;
16 }
17
18 void deleteOne(int number) {
19 // Only delete if the number exists (count > 0)
20 if (numberCount[number] > 0) {
21 // Before decrementing the count, decrease the frequency count for the current frequency
22 frequencyCount[numberCount[number]]--;
23
24 // Decrement the count for this number
25 numberCount[number]--;
26
27 // After decrementing the count, increase the frequency count for the new frequency
28 frequencyCount[numberCount[number]]++;
29 }
30 }
31
32 bool hasFrequency(int frequency) {
33 // Check if there's at least one number with the given frequency
34 return frequencyCount[frequency] > 0;
35 }
36
37private:
38 // Map to store the count of each number
39 // Key: number, Value: how many times the number appears
40 unordered_map<int, int> numberCount;
41
42 // Map to store the count of each frequency
43 // Key: frequency, Value: how many numbers have this frequency
44 unordered_map<int, int> frequencyCount;
45};
46
47/**
48 * Your FrequencyTracker object will be instantiated and called as such:
49 * FrequencyTracker* obj = new FrequencyTracker();
50 * obj->add(number);
51 * obj->deleteOne(number);
52 * bool param_3 = obj->hasFrequency(frequency);
53 */
54
1// Map to store the count of each number
2const numberCount: Map<number, number> = new Map();
3
4// Map to store how many numbers have a specific frequency
5const frequencyCount: Map<number, number> = new Map();
6
7/**
8 * Adds a number to the frequency tracker
9 * Updates both the count of the number and the frequency distribution
10 * @param number - The number to add to the tracker
11 */
12function add(number: number): void {
13 // Get current frequency of the number (default to 0 if not exists)
14 const currentFrequency = numberCount.get(number) || 0;
15
16 // Decrease the count of numbers with the current frequency
17 frequencyCount.set(currentFrequency, (frequencyCount.get(currentFrequency) || 0) - 1);
18
19 // Increment the count of the number
20 numberCount.set(number, currentFrequency + 1);
21
22 // Increase the count of numbers with the new frequency
23 frequencyCount.set(currentFrequency + 1, (frequencyCount.get(currentFrequency + 1) || 0) + 1);
24}
25
26/**
27 * Deletes one occurrence of a number from the frequency tracker
28 * Only deletes if the number exists in the tracker
29 * @param number - The number to delete from the tracker
30 */
31function deleteOne(number: number): void {
32 // Get current frequency of the number (default to 0 if not exists)
33 const currentFrequency = numberCount.get(number) || 0;
34
35 // Only delete if the number exists (frequency > 0)
36 if (currentFrequency > 0) {
37 // Decrease the count of numbers with the current frequency
38 frequencyCount.set(currentFrequency, (frequencyCount.get(currentFrequency) || 0) - 1);
39
40 // Decrement the count of the number
41 numberCount.set(number, currentFrequency - 1);
42
43 // Increase the count of numbers with the new frequency
44 frequencyCount.set(currentFrequency - 1, (frequencyCount.get(currentFrequency - 1) || 0) + 1);
45 }
46}
47
48/**
49 * Checks if any number has the specified frequency
50 * @param frequency - The frequency to check for
51 * @returns true if at least one number has the specified frequency, false otherwise
52 */
53function hasFrequency(frequency: number): boolean {
54 // Check if there's at least one number with the given frequency
55 return (frequencyCount.get(frequency) || 0) > 0;
56}
57
Time and Space Complexity
Time Complexity: Each operation (add
, deleteOne
, and hasFrequency
) has a time complexity of O(1)
.
-
The
add
method performs three dictionary operations: one read fromself.cnt[number]
, and three updates toself.freq
andself.cnt
. All dictionary operations in Python using hash tables have averageO(1)
time complexity. -
The
deleteOne
method first checks if the number exists (O(1)
), then performs similar dictionary operations asadd
if the condition is met. All operations areO(1)
. -
The
hasFrequency
method simply performs a dictionary lookup and comparison, which isO(1)
.
Space Complexity: The space complexity is O(n)
, where n
is the number of distinct numbers added to the tracker.
-
The
self.cnt
dictionary stores at mostn
distinct numbers as keys with their counts as values. -
The
self.freq
dictionary stores frequency counts as keys. In the worst case, when all numbers have different frequencies, this could also store up ton
entries. However, the number of distinct frequencies is bounded by the total number of distinct numbers. -
Therefore, the total space used is proportional to the number of distinct numbers, giving us
O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Update Frequency Count Before Modifying Number Count
A critical pitfall is updating the number count before adjusting the frequency count, which leads to incorrect frequency tracking.
Incorrect Implementation:
def add(self, number: int) -> None:
# WRONG: Incrementing count first
self.cnt[number] += 1
self.freq[self.cnt[number]] -= 1 # This uses the NEW count, not the old one!
self.freq[self.cnt[number]] += 1 # This would increment the same frequency
This bug occurs because after incrementing cnt[number]
, we lose track of the old frequency. The operation freq[cnt[number]] -= 1
would incorrectly decrement the NEW frequency instead of the OLD frequency.
Correct Approach:
def add(self, number: int) -> None:
# Update frequency count BEFORE changing the number count
self.freq[self.cnt[number]] -= 1 # Uses old count
self.cnt[number] += 1 # Now update the count
self.freq[self.cnt[number]] += 1 # Uses new count
2. Not Checking for Existence Before Deletion
Another common mistake is forgetting to check if a number exists before attempting to delete it.
Incorrect Implementation:
def deleteOne(self, number: int) -> None:
# WRONG: No existence check
self.freq[self.cnt[number]] -= 1
self.cnt[number] -= 1
self.freq[self.cnt[number]] += 1
This would incorrectly modify frequency counts even when the number doesn't exist (count is 0), leading to negative counts in the frequency map and corrupted state.
Correct Approach:
def deleteOne(self, number: int) -> None:
if self.cnt[number] > 0: # Check existence first
self.freq[self.cnt[number]] -= 1
self.cnt[number] -= 1
self.freq[self.cnt[number]] += 1
3. Using Regular Dictionaries Instead of DefaultDict
Using regular dictionaries requires explicit key existence checks, making the code more verbose and error-prone.
Error-Prone Implementation:
def add(self, number: int) -> None:
# Need to check if keys exist
if number not in self.cnt:
self.cnt[number] = 0
if self.cnt[number] not in self.freq:
self.freq[self.cnt[number]] = 0
self.freq[self.cnt[number]] -= 1
self.cnt[number] += 1
if self.cnt[number] not in self.freq:
self.freq[self.cnt[number]] = 0
self.freq[self.cnt[number]] += 1
Better Solution:
Using defaultdict(int)
automatically handles missing keys by returning 0, making the code cleaner and less error-prone.
4. Not Handling Frequency of Zero
Some implementations might try to "clean up" by removing entries when frequency becomes 0, but this adds unnecessary complexity.
Overly Complex:
def deleteOne(self, number: int) -> None:
if self.cnt[number] > 0:
self.freq[self.cnt[number]] -= 1
if self.freq[self.cnt[number]] == 0:
del self.freq[self.cnt[number]] # Unnecessary cleanup
self.cnt[number] -= 1
if self.cnt[number] == 0:
del self.cnt[number] # Unnecessary cleanup
else:
self.freq[self.cnt[number]] += 1
Simpler Solution:
Let the defaultdict handle zero values naturally. Since hasFrequency
only checks positive frequencies, having freq[0]
entries doesn't affect correctness and keeps the code simpler.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!