We need to build a data structure that tracks how many times each string has been incremented and, at any moment, can return a string with the highest count and one with the lowest, all in O(1) time.
A hash map covers the count updates: incrementing and decrementing are constant-time map operations. Finding the min and max in O(1) is the hard part. A heap gives O(log n) insert and delete. A balanced tree needs O(log n) for rebalancing. Neither meets the requirement.
Full sorting is unnecessary, though. Only the minimum and maximum matter, and a count changes by exactly 1 per operation. When a key's count goes from c to c+1, the key moves to the neighboring position in count order; nothing else shifts. That property is what makes O(1) possible.
At most 5 * 10^4 calls -- The cost that matters is per call. The problem requires O(1) average time per operation, which rules out heaps and balanced trees at O(log n).key consists of lowercase English letters -- Keys are regular strings, no special handling needed.dec is only called on existing keys -- We don't need to handle decrementing a non-existent key.Store each key's count in a hash map. Incrementing and decrementing are O(1) map updates. For getMaxKey and getMinKey, scan all entries and return the key with the highest or lowest count.
This is correct, but every getMaxKey or getMinKey call costs O(n), which violates the O(1) requirement. It still serves as a baseline: the map already answers count updates in O(1), so the min/max query is the only operation left to fix.
counts that maps each key to its integer count.inc(key): increment counts[key] by 1. If the key doesn't exist, set it to 1.dec(key): decrement counts[key] by 1. If it reaches 0, remove the key.getMaxKey(): iterate through all entries in the map and return the key with the highest count. Return "" if the map is empty.getMinKey(): iterate through all entries and return the key with the lowest count. Return "" if the map is empty.Input:
After inc("hello"), inc("hello"), the map holds one entry:
getMaxKey() scans the map and finds "hello" with count 2. getMinKey() also returns "hello" since it is the only key.
After inc("leet"), the map gains a second entry:
getMaxKey() scans and returns "hello" (count 2). getMinKey() scans and returns "leet" (count 1). The returned sequence matches the expected output: [null, null, null, "hello", "hello", null, "hello", "leet"]. Each scan visited every entry, which is the cost the next approach removes.
inc and dec, O(n) for getMaxKey and getMinKey. Hash map operations are O(1), but finding min/max requires scanning all n keys.Every call to getMaxKey or getMinKey scans the entire hash map, recomputing the answer from scratch even though the last inc or dec changed one key's count by 1. The next approach keeps keys grouped by count in sorted order, so the current min and max always sit at the two ends of a list.
A count moves from c to c+1 or from c to c-1, never anywhere else. A fully sorted structure is overkill; a key only ever needs to move between adjacent "count buckets."
The structure is a doubly-linked list where each node represents one count value and holds the set of keys that currently have that count. The list stays sorted by count, smallest at the head and largest at the tail. getMinKey reads the node next to the head, getMaxKey reads the node next to the tail, both O(1).
Incrementing a key from count c to c+1 removes it from bucket c and adds it to bucket c+1. If bucket c+1 does not exist, we create a new node right after bucket c, an O(1) insertion in a doubly-linked list. If bucket c becomes empty after the removal, we splice that node out, also O(1).
A hash map from each key to its bucket node locates the key's current bucket without searching. Every operation is then a hash map lookup plus an O(1) list splice plus an O(1) set update.
The list stays sorted by count even though no operation ever compares more than two neighbors. Assume the list is sorted before an operation. When a key moves from bucket c to c+1 and no c+1 bucket exists, the node after bucket c holds a count greater than c (sorted order) and not equal to c+1 (the code checked), so it is at least c+2. Inserting the new c+1 bucket directly after bucket c therefore preserves the order. The symmetric argument covers dec, which inserts a c-1 bucket directly before bucket c. The sentinels (count 0 at the head, infinity at the tail) guarantee every real bucket has a neighbor on both sides, so the same insertion logic works at the ends of the list.
keyToNode that maps each key to its current bucket node in the linked list.inc(key):curr (count c), move it to a node with count c+1. If the next node has count c+1, add the key there. Otherwise, create a new node with count c+1 and insert it right after curr.dec(key):curr (count c).curr.getMaxKey(): return any key from the node right before the tail sentinel.getMinKey(): return any key from the node right after the head sentinel.The trace starts with the operations from Example 1: two increments of "hello", min/max queries, an increment of "leet", and two more queries. Two dec("hello") calls follow, showing the two decrement cases: moving a key into an existing neighbor bucket and removing a key whose count reaches 0.
Through the Example 1 portion, the queries return "hello", "hello", then "hello" and "leet", matching the expected output. No step touched more than two list nodes: every inc and dec works on the key's current bucket and one neighbor, and every query reads a single node next to a sentinel.
inc and dec each do a hash map lookup O(1), linked list insert/delete O(1), and set add/remove O(1). getMaxKey accesses tail.prev and gets any element from its set, O(1). getMinKey accesses head.next similarly, O(1).