Design and implement a data structure that tracks the frequency of string keys and supports the following operations efficiently:
Example 1:
` Operations: ["inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"] Arguments: [["hello"], ["hello"], [], [], ["world"], [], []] Output: [null, null, "hello", "hello", null, "hello", "world"] Explanation:
Example 2:
` Operations: ["inc", "inc", "inc", "dec", "dec", "getMaxKey", "getMinKey"] Arguments: [["a"], ["b"], ["a"], ["a"], ["a"], [], []] Output: [null, null, null, null, null, "b", "b"] Explanation:
Hint 1: Data Structure Choice Consider using a combination of hash maps and a doubly linked list. You'll need to track both the frequency of each key and maintain an ordered structure of frequency buckets.
Hint 2: Frequency Buckets Create nodes that represent frequency levels, where each node contains all keys with that specific frequency count. Link these nodes in sorted order by frequency. This allows O(1) access to min and max frequencies.
Hint 3: Key Operations When incrementing or decrementing a key, you need to move it from one frequency bucket to another. Maintain a hash map that stores the current frequency of each key and another that maps frequencies to their bucket nodes in the linked list.
Full Solution `` Explanation:
The solution uses a combination of hash maps and a doubly linked list to achieve O(1) time complexity for all operations:
Data Structures:
key_freq: Maps each key to its current frequency countfreq_nodes: Maps each frequency to its FrequencyNode in the linked list- Doubly linked list of FrequencyNode objects, sorted by frequency, with dummy head and tail
FrequencyNode Structure:
- Each node represents a frequency level and contains a set of all keys with that frequency
- Nodes are linked in ascending order by frequency
inc(key):
- Look up current frequency, increment it
- Remove key from old frequency bucket, delete bucket if empty
- Add key to new frequency bucket, creating it if needed
- Insert new buckets in sorted position
dec(key):
- Similar to inc but decrements frequency
- If frequency becomes 0, remove key entirely from tracking
getMaxKey() / getMinKey():
- Simply access the last/first node in the linked list (before tail/after head)
- Return any key from that node's set
Time Complexity: O(1) average for all operations
- Hash map operations are O(1)
- Linked list insertions/deletions are O(1) with direct node references
- Set operations (add/remove/iter) are O(1) average
Space Complexity: O(N) where N is the number of unique keys currently stored
class FrequencyNode:
"""Represents a frequency bucket containing all keys with a specific count."""
def __init__(self, freq):
self.freq = freq
self.keys = set() # Keys with this frequency
self.prev = None
self.next = None
class FrequencyTracker:
def __init__(self):
# Map from key to its current frequency
self.key_freq = {}
# Map from frequency to the FrequencyNode
self.freq_nodes = {}
# Dummy head and tail for doubly linked list of frequency buckets
self.head = FrequencyNode(0)
self.tail = FrequencyNode(float('inf'))
self.head.next = self.tail
self.tail.prev = self.head
def _insert_node_after(self, new_node, prev_node):
"""Insert new_node after prev_node in the doubly linked list."""
next_node = prev_node.next
prev_node.next = new_node
new_node.prev = prev_node
new_node.next = next_node
next_node.prev = new_node
def _remove_node(self, node):
"""Remove node from the doubly linked list."""
prev_node = node.prev
next_node = node.next
prev_node.next = next_node
next_node.prev = prev_node
def inc(self, key: str) -> None:
# Get current frequency (0 if key doesn't exist)
curr_freq = self.key_freq.get(key, 0)
new_freq = curr_freq + 1
# Update key frequency map
self.key_freq[key] = new_freq
# Remove key from old frequency bucket if it exists
if curr_freq > 0:
curr_node = self.freq_nodes[curr_freq]
curr_node.keys.remove(key)
# If bucket becomes empty, remove it
if not curr_node.keys:
self._remove_node(curr_node)
del self.freq_nodes[curr_freq]
# Find or create the new frequency bucket
if new_freq not in self.freq_nodes:
new_node = FrequencyNode(new_freq)
self.freq_nodes[new_freq] = new_node
# Insert new node in sorted position
# Find the node to insert after
if curr_freq > 0 and curr_freq in self.freq_nodes:
insert_after = self.freq_nodes[curr_freq]
else:
# Find the correct position
insert_after = self.head
while insert_after.next.freq < new_freq:
insert_after = insert_after.next
self._insert_node_after(new_node, insert_after)
# Add key to new frequency bucket
self.freq_nodes[new_freq].keys.add(key)
def dec(self, key: str) -> None:
# If key doesn't exist, do nothing
if key not in self.key_freq:
return
curr_freq = self.key_freq[key]
# Remove key from current frequency bucket
curr_node = self.freq_nodes[curr_freq]
curr_node.keys.remove(key)
# If bucket becomes empty, remove it
if not curr_node.keys:
self._remove_node(curr_node)
del self.freq_nodes[curr_freq]
# Handle new frequency
new_freq = curr_freq - 1
if new_freq == 0:
# Remove key completely
del self.key_freq[key]
else:
# Update key frequency
self.key_freq[key] = new_freq
# Find or create new frequency bucket
if new_freq not in self.freq_nodes:
new_node = FrequencyNode(new_freq)
self.freq_nodes[new_freq] = new_node
# Insert before current position (since new_freq < curr_freq)
insert_after = curr_node.prev if curr_freq in self.freq_nodes else self.head
while insert_after.next.freq < new_freq:
insert_after = insert_after.next
self._insert_node_after(new_node, insert_after)
# Add key to new frequency bucket
self.freq_nodes[new_freq].keys.add(key)
def getMaxKey(self) -> str:
# Max frequency node is just before tail
if self.tail.prev == self.head:
return ""
max_node = self.tail.prev
# Return any key from the max frequency bucket
return next(iter(max_node.keys))
def getMinKey(self) -> str:
# Min frequency node is just after head
if self.head.next == self.tail:
return ""
min_node = self.head.next
# Return any key from the min frequency bucket
return next(iter(min_node.keys))