Facebook Pixel

2671. Frequency Tracker

MediumDesignHash Table
Leetcode Link

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:

  1. FrequencyTracker(): Creates a new frequency tracker that starts with no numbers stored.

  2. add(int number): Adds one occurrence of number to the data structure. If the number already exists, its count increases by one.

  3. deleteOne(int number): Removes one occurrence of number from the data structure. If the number doesn't exist or has zero occurrences, nothing happens.

  4. hasFrequency(int frequency): Checks if there's at least one number in the data structure that appears exactly frequency times. Returns true 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 count
  • freq: 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].

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. We decrease freq[old_count] by 1 (one less number has the old frequency)
  2. 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:

  1. cnt: A dictionary that maps each number to its occurrence count
  2. freq: 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:

  1. First, decrease the count of numbers having the current frequency: self.freq[self.cnt[number]] -= 1
  2. Increment the number's count: self.cnt[number] += 1
  3. 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):

  1. Decrease the count of numbers with the current frequency
  2. Decrement the number's count
  3. 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 Evaluator

Example 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, so freq[0] -= 1freq[0] = -1
  • Update: cnt[3] = 1
  • After: freq[1] += 1freq[1] = 1
  • State: cnt = {3: 1}, freq = {0: -1, 1: 1}

Operation 2: add(3)

  • Before: cnt[3] = 1, so freq[1] -= 1freq[1] = 0
  • Update: cnt[3] = 2
  • After: freq[2] += 1freq[2] = 1
  • State: cnt = {3: 2}, freq = {0: -1, 1: 0, 2: 1}

Operation 3: add(5)

  • Before: cnt[5] = 0, so freq[0] -= 1freq[0] = -2
  • Update: cnt[5] = 1
  • After: freq[1] += 1freq[1] = 1
  • State: cnt = {3: 2, 5: 1}, freq = {0: -2, 1: 1, 2: 1}

Operation 4: hasFrequency(2)

  • Check: freq[2] > 01 > 0 → returns true
  • (Number 3 appears exactly 2 times)

Operation 5: hasFrequency(1)

  • Check: freq[1] > 01 > 0 → returns true
  • (Number 5 appears exactly 1 time)

Operation 6: deleteOne(3)

  • Check: cnt[3] = 2 > 0, proceed with deletion
  • Before: freq[2] -= 1freq[2] = 0
  • Update: cnt[3] = 1
  • After: freq[1] += 1freq[1] = 2
  • State: cnt = {3: 1, 5: 1}, freq = {0: -2, 1: 2, 2: 0}

Operation 7: hasFrequency(2)

  • Check: freq[2] > 00 > 0 → returns false
  • (No number appears exactly 2 times anymore)

Operation 8: hasFrequency(1)

  • Check: freq[1] > 02 > 0 → returns true
  • (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 from self.cnt[number], and three updates to self.freq and self.cnt. All dictionary operations in Python using hash tables have average O(1) time complexity.

  • The deleteOne method first checks if the number exists (O(1)), then performs similar dictionary operations as add if the condition is met. All operations are O(1).

  • The hasFrequency method simply performs a dictionary lookup and comparison, which is O(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 most n 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 to n 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More